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实现,可以非常高灵活度地定制各自的需求。
以上就是本文的全部内容,希望对大家的学习有所帮助,也希望大家多多支持。