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

知识的领域是无限的,我们的学习也是无限期的。这篇文章主要讲述Android自己定义实现循环滚轮控件WheelView相关的知识,希望能为你提供帮助。
首先呈上效果图
Android自己定义实现循环滚轮控件WheelView



如今非常多地方都用到了滚轮布局WheelView,比方在选择生日的时候,风格 类似 系统提供的DatePickerDialog,开源的控件也有非常多,只是大部分都是依据当前项目的需求绘制的界面,因此我就自己写了一款比較符合自己项目的WheelView。

首先这个控件有下面的需求:
1、可以循环滚动。当向上或者向下滑动到临界值 的时候,则循环開始滚动
2、中间的一块有一块半透明的选择区,滑动结束时,哪一块在这个选择区,就选择这快。
3、继承自View进行绘制


然后进行一些关键点的解说:
1、总体控件继承自View。在onDraw中进行绘制。总体包括三个模块,整个View、每一块的条目、中间选择区的条目(额外绘制一块灰色区域)。

2、通过动态设置或者默认设置的可显示条目数,在最上和最下再各增加一块。意思就是一共绘制showCount+ 2个条目。
3、当最上面的条目数滑动超过条目高度的一半时,进行动态条目更新:将最以下的条目删除增加第一个条目、将第一个条目删除增加最以下的条目。
4、外界可设置条目显示数、字体大小、颜色、选择区提示文字(图中那个年字)、默认选择项、padding补白等等。

5、在onTouchEvent中,得到手指滑动的渐变值 ,动态更新当前全部的条目。
【Android自己定义实现循环滚轮控件WheelView】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); } } }



源码就三个文件,非常easy。凝视也非常具体,接下来就是使用文件了:
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实现,能够很高灵活度地定制各自的需求。
demoproject就不提供了,使用很easy。















    推荐阅读