当前位置: 移动技术网 > 移动技术>移动开发>Android > Android自定义实现循环滚轮控件WheelView

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

2019年07月24日  | 移动技术网移动技术  | 我要评论

首先呈上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实现,可以非常高灵活度地定制各自的需求。

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

如对本文有疑问, 点击进行留言回复!!

相关文章:

验证码:
移动技术网