美文网首页AndroidAndroid 自定义viewAndroid自定义View
【造轮子系列】转轮选择工具——WheelView

【造轮子系列】转轮选择工具——WheelView

作者: 十个雨点 | 来源:发表于2016-07-03 14:20 被阅读4216次

转载注明出处:简书-十个雨点

实现转轮的选择功能,效果见下图:

普通绘制示意图

上图所示是一个3格的滚轮,其中标示了几个重要的高度,从图中可以看出每一个待选项绘制位置是如何计算的。需要注意的是,y+m的起点并不是画面中的顶点,而是从第一个待选项的顶点算起的(也就是可能超出了绘制区域)。其中tH是根据normalTextSize和selectedTextSize和文字的内容计算出来的,具体计算步骤请看源码

couldSelected示意图

上图标示了如何计算couldSelected的结果,需要注意的是,N是int型的,因此N/2的结果其实是下取整的,故N/2*uH!=N*uH/2。如果不明白,去看看java的运算符优先级和隐式的类型转换吧。

从图中可以看出,couldSelected的范围其实刚好就是第一个待选项(含)和第三个待选项(含)之间的范围。而如果滚轮中不止3格,而是5格、7格,则couldSelected的范围 就是正中间那项的上下各一项的文字之间的范围。

selected示意图

上图标示了如何计算selected的结果,可以看出,selected的范围刚好是正中间那格的范围,文字的任何一部分进入这一格内的时候,这一项就被选中了。

现在你应该理解了这些数值的判断依据了,但你可能会问,如果有两个待选项都在这个范围内,selected怎么判断?那么使用时会使上方的那个item被选中,而事实上本项目在计算过程中已经基本排除了这种可能性了,结合前面介绍的slowMove和noEmpty函数的源码可以更好的理解couldSelected和selected的作用,以及整个选择和滚动的逻辑,具体实现还是请移步源码

如何处理滑动的过程中的点击操作

系统的NumberPicker和一些其他的开源项目对滑动时的点击处理得不够理想。在滑动的过程中快速点击,很大的几率出现最终结果不居中的情况:

现存滚轮工具的问题

其实这就是我自己造轮子的原因。这种情况主要是以下两点设计上的缺陷导致的:

  • 滚动动画本身的实现方式上有问题。在每次快速滑动的时候(goonMove的实现)新建一个Thread来进行计算,这样做有个好处在于,多次快速滚动的时候,可以通过多个线程同步计算,产生加速滚动的感觉。
  • 没有在每一次滚动结束的时候,都进行一次让滚轮归位的操作。这些项目中,动画的实现方式,往往是在动画开始的时候就计算好了最终要滚动的距离,而由于滚动动画是在线程中迭代计算的,所以在计算的过程中再次进行微小的扰动,就会导致整个滚动产生偏差,形成上图中错位的结果。

于是我针对这两点做了对应的处理。

  • 首先使用了HandlerThread和Handler来进行动画的计算,这样就使得同时只有一个线程进行滚动计算,也减少了频繁创建线程的开销。然后在onTouchEvent函数中做了打断当前滚动的判断,打断滚动很简单,就只是把当前动画的位置设置为新的动画的起点。这样在滚轮快速滚动过程中再次点击的时候,就相当于一次新的滚动,与上一次滚动就没有关系了。但是这就需要使用其他方法来产生加速滚动的效果,详见goonMove函数源码 。

  • 通过使用HandlerThread,能保证在每次滚动的结束都调用slowMove函数和noEmpty函数(而且不会有同步问题),在这两个函数中,会再次计算当前滚轮的状态,从而确保在动画停止的时候肯定有一项被选中,且被选中项处于滚轮正中间的位置。说白了,就是通过重复计算的方式,确保最终效果。

如何调优性能

说实话,我对性能调优方面并没有深入研究,所以本项目的性能可能并不算好,但是性能优化的基本逻辑还是有的,也就是减少不必要的计算,本项目中有两处:

  • 在绘制每个item的时候,需要先根据normalTextSize、selectedTextSize、文字内容和item的位置计算tH,但是如果normalTextSize和selectedTextSize相等的情况下,则每次计算的tH都一样,所以我设置了一个boolean来标示是否以及计算过了,计算过就无需反复计算了。
  • 在绘制每个item之前,先调用isInView函数,判断当前item是否在显示区域内,如果不在,则直接跳过该item的计算和绘制,可以大幅提高动画的流畅度。注意下面代码中注释行和非注释行的区别。
/**
 * 是否在可视界面内
 * @return
 */
public  synchronized boolean isInView() {
//    if (y + move > controlHeight || ((float)y + (float)move + (float)unitHeight / 2 + (float)textRect.height() / 2f) < 0)
    if (y + move > controlHeight || ((float)y + (float)move + (float)unitHeight  ) < 0)//放宽判断的条件,否则就不能在onDraw的开头执行,而要到计算完tH以后才能判断了。
        return false;
    return true;
}

更多性能调优请移步这篇:WheelView的改进

源码

WheelView
源码会继续更新,博客可能会跟不上源码的进度,以源码为准。

tips:源码中比较核心的函数就是前面介绍过的onTouchEvent,goonMove,slowMove,noEmpty,couldSelected和selected,结合本文,基本上一看就明白了。

相关文章

网友评论

  • wodezhzh:想问下如果不想让数据循环怎么处理?就是滚到最后一条就不让往下滚了,要怎么处理?
    十个雨点:github上的readme有写,用isCyclic参数来指定是否循环,这里的不够新了
  • Judy_li:楼主,如果itemNumber设置为2的话,设置item的属性都不会起作用
    十个雨点:@Judy_li 额,因为都是以奇数来处理的,中间一个,上下各n个,合起来2n+1个,哈哈
  • 851aebc894f8:博主实现item之间联动使用的是Map集合匹配,字符串对应,那么当应用到日期上时(例如1900-01-01)这样的三级联动,如果还是使用字符串分割的话那个字符串量也太多了...
    我现在就在做这个,有几个要改动的地方
    1.三级联动的话就需要两个集合嵌套了 Map<年份,月日集合>——》Map<月,日>;
    2.因为数字量较大,需要尽可能精简string内容,年份数量固定不用改,月份12个,每个月几天按照“大月31天”,“小月30”,“2月28”,“闰年2月29”分为四类进行加载
    3.关于闰月2月的日期,需要两层筛选(年份%4==0&&月份==2)——》29
    (年份%4!=0&&月份==2)——》28

    请问还有更好的方法么,我觉得这个很麻烦.....
    十个雨点:@Ksiem 间距的话使用margin应该就可以了吧,没图确实不容易理解你的需求,你可以在github提issue
    851aebc894f8:@十个雨点 已经实现正确的年月日逻辑了,在原po基础上改动了内容,谢谢你的demon,关于wheelView的样式问题我还在修改,不能贴图片确实挺麻烦的..
    十个雨点:@Ksiem 联动那个只是一个demo,我是用来演示的,并没有做为一个完整的解决方案,我觉得用你的第二种方法是挺不错的,或者每次切换完成再重新计算来判断也行,比较判断闰年的计算量不高
  • 851aebc894f8:感谢博主,这个刚好解决了我的问题,请问博主,怎么设置选中项的背景色呢? 除了在视图中让他们挤在一起没办法消除wheelView中间的间隔了么
    851aebc894f8:间隔指的是wheelView之间的间隙(露出背景色的地方),简书没办法评论图片
    十个雨点:@Ksiem 背景色用普通的background就可以,消除间隔我不太理解什么意思
  • 捡淑:mark
  • a738e9bd3d17:不错
    十个雨点:@小伙挺帅 谢谢支持
  • 菲利柯斯:有github吗?
    十个雨点:@菲利柯斯 文中到处都是github的超链接啊

本文标题:【造轮子系列】转轮选择工具——WheelView

本文链接:https://www.haomeiwen.com/subject/lsjadttx.html