欢迎您访问程序员文章站本站旨在为大家提供分享程序员计算机编程知识!
您现在的位置是: 首页  >  移动技术

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

程序员文章站 2024-03-04 19:03:54
首先呈上android循环滚轮效果图:   现在很多地方都用到了滚轮布局wheelview,比如在选择生日的时候,风格类似系统提供的datepickerd...

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

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

现在很多地方都用到了滚轮布局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实现,可以非常高灵活度地定制各自的需求。

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