Android自定义实现循环滚轮控件WheelView

首先呈上Android循环滚轮效果图:

现在很多地方都用到了滚轮布局WheelView,比如在选择生日的时候,风格类似系统提供的DatePickerDialog,开源的控件也有很多,不过大部分都是根据当前项目的需求绘制的界面,因此我就自己写了一款比较符合自己项目的WheelView。
首先这个控件有以下的需求
 1、能够循环滚动,当向上或者向下滑动到临界值的时候,则循环开始滚动
 2、中间的一块有一块半透明的选择区,滑动结束时,哪一块在这个选择区,就选择这快。
 3、继承自View进行绘制

然后进行一些关键点的讲解: 
1、整体控件继承自View,在onDraw中进行绘制。整体包含三个模块,整个View、每一块的条目、中间选择区的条目(额外绘制一块灰色区域)。 
2、通过动态设置或者默认设置的可显示条目数,在最上和最下再各加入一块,意思就是一共绘制showCount+2个条目。 
3、当最上面的条目数滑动超过条目高度的一半时,进行动态条目更新:将最下面的条目删除加入第一个条目、将第一个条目删除加入最下面的条目。 
4、外界可设置条目显示数、字体大小、颜色、选择区提示文字(图中那个年字)、默认选择项、padding补白等等。 
5、在onTouchEvent中,得到手指滑动的渐变值,动态更新当前所有的条目。 
6、在onMeasure中动态计算宽度,所有条目的宽度、高度、起始Y坐标等等。 
7、通过当前条目和被选择条目的坐标,超过一半则视为被选择,并且滑动到对应的位置。

下面的是WheelView代码,主要是计算初始值、得到外面设置的值:

package cc.wxf.view.wheel;

import android.content.Context;
import android.graphics.Canvas;
import android.graphics.Color;
import android.graphics.Paint;
import android.util.AttributeSet;
import android.view.MotionEvent;
import android.view.View;

import java.util.ArrayList;
import java.util.List;

/**
 * Created by ccwxf on 2016/3/31.
 */
public class WheelView extends View {

 public static final int FONT_COLOR = Color.BLACK;
 public static final int FONT_SIZE = 30;
 public static final int PADDING = 10;
 public static final int SHOW_COUNT = 3;
 public static final int SELECT = 0;
 //总体宽度、高度、Item的高度
 private int width;
 private int height;
 private int itemHeight;
 //需要显示的行数
 private int showCount = SHOW_COUNT;
 //当前默认选择的位置
 private int select = SELECT;
 //字体颜色、大小、补白
 private int fontColor = FONT_COLOR;
 private int fontSize = FONT_SIZE;
 private int padding = PADDING;
 //文本列表
 private List<String> lists;
 //选中项的辅助文本,可为空
 private String selectTip;
 //每一项Item和选中项
 private List<WheelItem> wheelItems = new ArrayList<WheelItem>();
 private WheelSelect wheelSelect = null;
 //手点击的Y坐标
 private float mTouchY;
 //监听器
 private OnWheelViewItemSelectListener listener;

 public WheelView(Context context) {
 super(context);
 }

 public WheelView(Context context, AttributeSet attrs) {
 super(context, attrs);
 }

 public WheelView(Context context, AttributeSet attrs, int defStyleAttr) {
 super(context, attrs, defStyleAttr);
 }

 /**
 * 设置字体的颜色,不设置的话默认为黑色
 * @param fontColor
 * @return
 */
 public WheelView fontColor(int fontColor){
 this.fontColor = fontColor;
 return this;
 }

 /**
 * 设置字体的大小,不设置的话默认为30
 * @param fontSize
 * @return
 */
 public WheelView fontSize(int fontSize){
 this.fontSize = fontSize;
 return this;
 }

 /**
 * 设置文本到上下两边的补白,不合适的话默认为10
 * @param padding
 * @return
 */
 public WheelView padding(int padding){
 this.padding = padding;
 return this;
 }

 /**
 * 设置选中项的复制文本,可以不设置
 * @param selectTip
 * @return
 */
 public WheelView selectTip(String selectTip){
 this.selectTip = selectTip;
 return this;
 }

 /**
 * 设置文本列表,必须且必须在build方法之前设置
 * @param lists
 * @return
 */
 public WheelView lists(List<String> lists){
 this.lists = lists;
 return this;
 }

 /**
 * 设置显示行数,不设置的话默认为3
 * @param showCount
 * @return
 */
 public WheelView showCount(int showCount){
 if(showCount % 2 == 0){
  throw new IllegalStateException("the showCount must be odd");
 }
 this.showCount = showCount;
 return this;
 }

 /**
 * 设置默认选中的文本的索引,不设置默认为0
 * @param select
 * @return
 */
 public WheelView select(int select){
 this.select = select;
 return this;
 }

 /**
 * 最后调用的方法,判断是否有必要函数没有被调用
 * @return
 */
 public WheelView build(){
 if(lists == null){
  throw new IllegalStateException("this method must invoke after the method [lists]");
 }
 return this;
 }

 @Override
 protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
 //得到总体宽度
 width = MeasureSpec.getSize(widthMeasureSpec) - getPaddingLeft() - getPaddingRight();
 // 得到每一个Item的高度
 Paint mPaint = new Paint();
 mPaint.setTextSize(fontSize);
 Paint.FontMetrics metrics = mPaint.getFontMetrics();
 itemHeight = (int) (metrics.bottom - metrics.top) + 2 * padding;
 //初始化每一个WheelItem
 initWheelItems(width, itemHeight);
 //初始化WheelSelect
 wheelSelect = new WheelSelect(showCount / 2 * itemHeight, width, itemHeight, selectTip, fontColor, fontSize, padding);
 //得到所有的高度
 height = itemHeight * showCount;
 super.onMeasure(widthMeasureSpec, MeasureSpec.makeMeasureSpec(height, MeasureSpec.EXACTLY));
 }

 /**
 * 创建显示个数+2个WheelItem
 * @param width
 * @param itemHeight
 */
 private void initWheelItems(int width, int itemHeight) {
 wheelItems.clear();
 for(int i = 0; i < showCount + 2; i++){
  int startY = itemHeight * (i - 1);
  int stringIndex = select - showCount / 2 - 1 + i;
  if(stringIndex < 0){
  stringIndex = lists.size() + stringIndex;
  }
  wheelItems.add(new WheelItem(startY, width, itemHeight, fontColor, fontSize, lists.get(stringIndex)));
 }
 }

 @Override
 public boolean onTouchEvent(MotionEvent event) {
 switch (event.getAction()){
  case MotionEvent.ACTION_DOWN:
  mTouchY = event.getY();
  return true;
  case MotionEvent.ACTION_MOVE:
  float dy = event.getY() - mTouchY;
  mTouchY = event.getY();
  handleMove(dy);
  break;
  case MotionEvent.ACTION_UP:
  handleUp();
  break;
 }
 return super.onTouchEvent(event);
 }

 /**
 * 处理移动操作
 * @param dy
 */
 private void handleMove(float dy) {
 //调整坐标
 for(WheelItem item : wheelItems){
  item.adjust(dy);
 }
 invalidate();
 //调整
 adjust();
 }

 /**
 * 处理抬起操作
 */
 private void handleUp(){
 int index = -1;
 //得到应该选择的那一项
 for(int i = 0; i < wheelItems.size(); i++){
  WheelItem item = wheelItems.get(i);
  //如果startY在selectItem的中点上面,则将该项作为选择项
  if(item.getStartY() > wheelSelect.getStartY() && item.getStartY() < (wheelSelect.getStartY() + itemHeight / 2)){
  index = i;
  break;
  }
  //如果startY在selectItem的中点下面,则将上一项作为选择项
  if(item.getStartY() >= (wheelSelect.getStartY() + itemHeight / 2) && item.getStartY() < (wheelSelect.getStartY() + itemHeight)){
  index = i - 1;
  break;
  }
 }
 //如果没找到或者其他因素,直接返回
 if(index == -1){
  return;
 }
 //得到偏移的位移
 float dy = wheelSelect.getStartY() - wheelItems.get(index).getStartY();
 //调整坐标
 for(WheelItem item : wheelItems){
  item.adjust(dy);
 }
 invalidate();
 // 调整
 adjust();
 //设置选择项
 int stringIndex = lists.indexOf(wheelItems.get(index).getText());
 if(stringIndex != -1){
  select = stringIndex;
  if(listener != null){
  listener.onItemSelect(select);
  }
 }
 }

 /**
 * 调整Item移动和循环显示
 */
 private void adjust(){
 //如果向下滑动超出半个Item的高度,则调整容器
 if(wheelItems.get(0).getStartY() >= -itemHeight / 2 ){
  //移除最后一个Item重用
  WheelItem item = wheelItems.remove(wheelItems.size() - 1);
  //设置起点Y坐标
  item.setStartY(wheelItems.get(0).getStartY() - itemHeight);
  //得到文本在容器中的索引
  int index = lists.indexOf(wheelItems.get(0).getText());
  if(index == -1){
  return;
  }
  index -= 1;
  if(index < 0){
  index = lists.size() + index;
  }
  //设置文本
  item.setText(lists.get(index));
  //添加到最开始
  wheelItems.add(0, item);
  invalidate();
  return;
 }
 //如果向上滑超出半个Item的高度,则调整容器
 if(wheelItems.get(0).getStartY() <= (-itemHeight / 2 - itemHeight)){
  //移除第一个Item重用
  WheelItem item = wheelItems.remove(0);
  //设置起点Y坐标
  item.setStartY(wheelItems.get(wheelItems.size() - 1).getStartY() + itemHeight);
  //得到文本在容器中的索引
  int index = lists.indexOf(wheelItems.get(wheelItems.size() - 1).getText());
  if(index == -1){
  return;
  }
  index += 1;
  if(index >= lists.size()){
  index = 0;
  }
  //设置文本
  item.setText(lists.get(index));
  //添加到最后面
  wheelItems.add(item);
  invalidate();
  return;
 }
 }

 /**
 * 得到当前的选择项
 */
 public int getSelectItem(){
 return select;
 }

 @Override
 protected void onDraw(Canvas canvas) {
 //绘制每一项Item
 for(WheelItem item : wheelItems){
  item.onDraw(canvas);
 }
 //绘制阴影
 if(wheelSelect != null){
  wheelSelect.onDraw(canvas);
 }
 }

 /**
 * 设置监听器
 * @param listener
 * @return
 */
 public WheelView listener(OnWheelViewItemSelectListener listener){
 this.listener = listener;
 return this;
 }

 public interface OnWheelViewItemSelectListener{
 void onItemSelect(int index);
 }
}

然后是每一个条目类,根据当前的坐标进行绘制,根据渐变值改变坐标等:

package cc.wxf.view.wheel;

import android.graphics.Canvas;
import android.graphics.Paint;
import android.graphics.RectF;

/**
 * Created by ccwxf on 2016/3/31.
 */
public class WheelItem {
 // 起点Y坐标、宽度、高度
 private float startY;
 private int width;
 private int height;
 //四点坐标
 private RectF rect = new RectF();
 //字体大小、颜色
 private int fontColor;
 private int fontSize;
 private String text;
 private Paint mPaint = new Paint(Paint.ANTI_ALIAS_FLAG);

 public WheelItem(float startY, int width, int height, int fontColor, int fontSize, String text) {
 this.startY = startY;
 this.width = width;
 this.height = height;
 this.fontColor = fontColor;
 this.fontSize = fontSize;
 this.text = text;
 adjust(0);
 }

 /**
 * 根据Y坐标的变化值,调整四点坐标值
 * @param dy
 */
 public void adjust(float dy){
 startY += dy;
 rect.left = 0;
 rect.top = startY;
 rect.right = width;
 rect.bottom = startY + height;
 }

 public float getStartY() {
 return startY;
 }

 /**
 * 直接设置Y坐标属性,调整四点坐标属性
 * @param startY
 */
 public void setStartY(float startY) {
 this.startY = startY;
 rect.left = 0;
 rect.top = startY;
 rect.right = width;
 rect.bottom = startY + height;
 }

 public void setText(String text) {
 this.text = text;
 }

 public String getText() {
 return text;
 }

 public void onDraw(Canvas mCanvas){
 //设置钢笔属性
 mPaint.setTextSize(fontSize);
 mPaint.setColor(fontColor);
 //得到字体的宽度
 int textWidth = (int)mPaint.measureText(text);
 //drawText的绘制起点是左下角,y轴起点为baseLine
 Paint.FontMetrics metrics = mPaint.getFontMetrics();
 int baseLine = (int)(rect.centerY() + (metrics.bottom - metrics.top) / 2 - metrics.bottom);
 //居中绘制
 mCanvas.drawText(text, rect.centerX() - textWidth / 2, baseLine, mPaint);
 }
}

最后是选择项,就是额外得在中间区域绘制一块灰色区域:

package cc.wxf.view.wheel;

import android.graphics.Canvas;
import android.graphics.Color;
import android.graphics.Paint;
import android.graphics.Rect;

/**
 * Created by ccwxf on 2016/4/1.
 */
public class WheelSelect {
 //黑框背景颜色
 public static final int COLOR_BACKGROUND = Color.parseColor("#77777777");
 //黑框的Y坐标起点、宽度、高度
 private int startY;
 private int width;
 private int height;
 //四点坐标
 private Rect rect = new Rect();
 //需要选择文本的颜色、大小、补白
 private String selectText;
 private int fontColor;
 private int fontSize;
 private int padding;
 private Paint mPaint = new Paint(Paint.ANTI_ALIAS_FLAG);

 public WheelSelect(int startY, int width, int height, String selectText, int fontColor, int fontSize, int padding) {
 this.startY = startY;
 this.width = width;
 this.height = height;
 this.selectText = selectText;
 this.fontColor = fontColor;
 this.fontSize = fontSize;
 this.padding = padding;
 rect.left = 0;
 rect.top = startY;
 rect.right = width;
 rect.bottom = startY + height;
 }

 public int getStartY() {
 return startY;
 }

 public void setStartY(int startY) {
 this.startY = startY;
 }

 public void onDraw(Canvas mCanvas) {
 //绘制背景
 mPaint.setStyle(Paint.Style.FILL);
 mPaint.setColor(COLOR_BACKGROUND);
 mCanvas.drawRect(rect, mPaint);
 //绘制提醒文字
 if(selectText != null){
  //设置钢笔属性
  mPaint.setTextSize(fontSize);
  mPaint.setColor(fontColor);
  //得到字体的宽度
  int textWidth = (int)mPaint.measureText(selectText);
  //drawText的绘制起点是左下角,y轴起点为baseLine
  Paint.FontMetrics metrics = mPaint.getFontMetrics();
  int baseLine = (int)(rect.centerY() + (metrics.bottom - metrics.top) / 2 - metrics.bottom);
  //在靠右边绘制文本
  mCanvas.drawText(selectText, rect.right - padding - textWidth, baseLine, mPaint);
 }
 }
}

源代码就三个文件,很简单,注释也很详细,接下来就是使用文件了:

 final WheelView wheelView = (WheelView) findViewById(R.id.wheelView);
 final List<String> lists = new ArrayList<>();
 for(int i = 0; i < 20; i++){
  lists.add("test:" + i);
 }
 wheelView.lists(lists).fontSize(35).showCount(5).selectTip("年").select(0).listener(new WheelView.OnWheelViewItemSelectListener() {
  @Override
  public void onItemSelect(int index) {
  Log.d("cc", "current select:" + wheelView.getSelectItem() + " index :" + index + ",result=" + lists.get(index));
  }
 }).build();

这个控件说简单也简单,说复杂也挺复杂,从最基础的onDraw实现,可以非常高灵活度地定制各自的需求。

以上就是本文的全部内容,希望对大家的学习有所帮助,也希望大家多多支持我们。

(0)

相关推荐

  • Android自定义可循环的滚动选择器CycleWheelView

    最近碰到个项目要使用到滚动选择器,原生的NumberPicker可定制性太差,不大符合UI要求. 网上开源的WheelView是用ScrollView写的,不能循环滚动,而且当数据量很大时要加载的Item太多,性能非常低. 然后,还是自己写一个比较靠谱,用的是ListView实现的.写完自己体验了一下,性能不错,再大的数据也不怕了. 感觉不错,重新封装了一下,提供了一些接口可以直接按照自己的需求定制,调用方法在MainActivity中. 补个图片: 不多说了,直接上代码: CycleWheel

  • Android省市区三级联动控件使用方法实例讲解

    最近有需求需要实现省市区三级联动,但是发现之前的实现不够灵活,自己做了一些优化.为了方便以后使用,抽离出来放在了github上WheelView.同时把其核心库放在了JCenter中了,可以直接引用.也可以参考项目中的Demo进行引用 下面介绍一下如何使用 如果用的是AndroidStudio那么直接在build.gradle文件中添加依赖: dependencies { compile 'chuck.WheelItemView:library:1.0.1' } 成功引入库之后,可以在需要弹出省

  • 轻松实现Android仿淘宝地区选择功能

    最近用淘宝客户端的时候,编辑地址的时候有个地区选择的功能.看上面的效果觉得挺酷,滚动的时候,是最后一个从下面飞上来挨着前一个.就自己鼓捣一个出来玩玩. 说了效果可能不太直观,下面上两张图看看效果 淘宝地区选择效果 再来一张自己的效果 gif的效果可能不太好,大家自己用Android手机打开淘宝看看 实现分析 展示很简单,ListView就可以了.对于动画效果,只需要在getView的时候获取到要展示的View,通过属性动画修改translationY就ok啦.由于地区选择是一个界面,所以这里还用

  • android-wheel控件实现三级联动效果

    本文实例为大家分享了android wheel省市县三级联动效果,供大家参考,具体内容如下 在github上面有一个叫做 Android-wheel 的开源控件, 代码地址:https://github.com/maarek/android-wheel 源码下载地址:http://xiazai.jb51.net/201610/yuanma/AndroidCascadeMaster(jb51.net).rar 主界面布局 activity_main.xml <LinearLayout xmlns:

  • Android使用android-wheel实现省市县三级联动

    今天没事跟群里面侃大山,有个哥们说道Android Wheel这个控件,以为是Andriod内置的控件,google一把,发现是个github上的一个控件. 下载地址:https://code.google.com/p/android-wheel/    发现很适合做省市县三级联动就做了一个. 先看下效果图: 1.首先导入github上的wheel项目 2.新建个项目,然后选择记得右键->Properties->Android中将wheel添加为lib: 上面两个步骤是导入所有开源项目的过程了

  • Android自定义wheelview随机选号效果

    先看下利用wheelview实现滚动随机选择号码效果: 直接上代码 首页就是dialog显示不在描述 主要看dialog代码 package com.yskj.jh.wheeldemo; import android.app.Dialog; import android.content.Context; import android.os.Bundle; import android.view.View; import android.view.ViewGroup; import android

  • Android自定义WheelView地区选择三级联动

    本文实例为大家分享了WheelView地区选择三级联动的具体代码,供大家参考,具体内容如下 1. 效果 最近需要做一个地区选择的功能,但是在网上和github上找了很久都没找到满意的,然后朋友推荐了一个给我,我花了点时间把代码大致看懂并改成我想要的,并写上我的理解.效果如图: 2. 注意 a. 首先我们要明白,网上这写三级联动的demo,不管是把数据库文件放在raw还是assets中,我们都要进行复制,将这个文件复制到app目录下,即 /data/data/"+context.getPackag

  • Android实现三级联动下拉框 下拉列表spinner的实例代码

    主要实现办法:动态加载各级下拉值的适配器 在监听本级下拉框,当本级下拉框的选中值改变时,随之修改下级的适配器的绑定值              XML布局: 复制代码 代码如下: <LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"    xmlns:tools="http://schemas.android.com/tools"    android:layout_w

  • Android自定义实现循环滚轮控件WheelView

    首先呈上Android循环滚轮效果图: 现在很多地方都用到了滚轮布局WheelView,比如在选择生日的时候,风格类似系统提供的DatePickerDialog,开源的控件也有很多,不过大部分都是根据当前项目的需求绘制的界面,因此我就自己写了一款比较符合自己项目的WheelView. 首先这个控件有以下的需求:  1.能够循环滚动,当向上或者向下滑动到临界值的时候,则循环开始滚动  2.中间的一块有一块半透明的选择区,滑动结束时,哪一块在这个选择区,就选择这快.  3.继承自View进行绘制 然

  • android自定义WaveView水波纹控件

    本文实例为大家分享了android自定义WaveView水波纹控件的使用方法,供大家参考,具体内容如下 Github Repository and libaray WaveView水波纹控件 首先看下演示demo demo中可以看到不同高度,不同速度,不同幅度的水波纹:你可以通过view的参数直接控制view的表现形式. 引入你的工程 在项目的根目录下的build.gradle文件中添加如下代码: allprojects { repositories { ... maven { url 'htt

  • Android自定义View之组合控件实现类似电商app顶部栏

    本文实例为大家分享了Android自定义View之组合控件,仿电商app顶部栏的相关代码,供大家参考,具体内容如下 效果图: 分析:左右两边可以是TextView和Button,设置drawableTop即可,中间的看着像是EditText,但是用过淘宝天猫等类似app的话会发现点击搜索不是在当前Activit进行搜索的,是跳转到另外的页面进行的,所以用TextView然后设置背景即可. 实现流程 参数列表: 设置属性文件:values下建立attrs.xml文件,添加需要自定义的属性. <?x

  • Android 自定义底部上拉控件的实现方法

    前言 又到了新的一月,今天提供一个Android自定义底部上拉布局的实现,起因是自己在项目中需要实现这样一个控件,干脆自己写一个练练手. 写完了觉得能想到的需求都基本有了(可能会有其它需求,不过基本上改吧改吧就行了),又花了一点时间直接放到了Github上托管,希望能给您一些参考价值: SlideBottomLayout-Android 简单易上手的Android底部上拉控件 先看一下实现效果: 分析一下这种控件的基本需求有以下几种: 1.有一个部分是能够作为把手(就是图中的handle,)进行

  • android自定义圆形倒计时显示控件

    本文实例为大家分享了android自定义圆形倒计时显示控件的具体代码,供大家参考,具体内容如下 先上效果图 - 倒计时结束 代码块 attr.xml 控件需要用到的属性: <?xml version="1.0" encoding="utf-8"?> <resources> <declare-styleable name="CountDownView"> <!--颜色--> <attr name

  • Android自定义view实现输入控件

    本文实例为大家分享了Android自定义view实现输入控件的具体代码,供大家参考,具体内容如下 网络上大部分的输入控件都是多个EditText组合而成,本例中采用的是: 单个EditText作为输入的捕捉控件 多个ImageView的子类作为显示的控件,绘制EditText中的数据 如上图: 输入前和输入后输入框需要发生响应的改变 点击自定义控件要弹出软键盘 EditText数据捕捉,以及EditView不能操作(如果可以操作,数据处理会混乱) 输完后会得到相应的提示 ImageView的子类

  • Android自定义顶部导航栏控件实例代码

    下面一段代码给大家介绍了android 自定义顶部导航栏控件功能,具体代码如下所示: class HeaderBar @JvmOverloads constructor( context: Context, attrs: AttributeSet? = null, defStyleAttr: Int = 0 ) : FrameLayout(context, attrs, defStyleAttr) { //重写构造方法 在java里面 我们一般是重写三个构造方法//在kotlin中 我们可以使用

  • Android 自定义日期段选择控件功能(开始时间-结束时间)

    开发中碰到个需求,需要在一个空间中选择完成开始和结束时间.实现的过程走的是程序员开发的老路子,找到轮子后自己改吧改吧就成了. 当时做的时候有几个需求:1.当天为最大的结束日期,2.最大选择范围1年,3.开始时间和结束时间可以为同一天.如有其他需求实现,可以参考代码改进一下.先上效果图: 视频点击后的虚影是屏幕录制的原因.实现步骤:(如有缺失什么资源,请告知.开始时间和结束时间显示自己布局内添加就可以) 1.自定义控件属性 <declare-styleable name="MyCalenda

  • Android自定义view实现倒计时控件

    本文实例为大家分享了Android自定义view实现倒计时控件的具体代码,供大家参考,具体内容如下 直接上代码 自定义TextView 文字展示 public class StrokeTextView extends TextView { private TextView borderText = null;///用于描边的TextView private Context mContext; public StrokeTextView(Context context) { super(conte

  • Android自定义滑动接听电话控件组实例

    本文根据组件开发思想,首先介绍android自定义控件,然后将自定义的控件封装为jar包.最为实现滑动接听电话控件组. 一.目录结构 二.运行效果 三.代码实现 首先,自定义一个类IncomingPhone继承RelativeLayout public IncomingPhone(Context context, AttributeSet attrs) { super(context, attrs); mContext = context; TextView textView = new Tex

随机推荐