【造轮子系列】转轮选择工具——WheelView
转载注明出处:简书-十个雨点
实现转轮的选择功能,效果见下图:
普通绘制示意图上图所示是一个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,结合本文,基本上一看就明白了。