前言

WheelView想必大家或多或少都有一定了解, 它是一款3D滚轮控件,效果类似IOS 上面的UIpickerview 。按照国际惯例,先放一张效果图:

以上是Android-PickerView 的demo演示图,它有时间选择和选项选择,并支持一二三级联动,支持自定义样式。由于saiwu-bigkoo(吴哥)已经转行不干编程了,项目现由我主导更新维护。目前我更新了3.x + 的版本,修复了若干问题,并重构了项目,让开发者使用起来更加灵活方便,定制化更强。另外我还创建了一个组织,希望有兴趣的小伙伴也能加入进来为这个项目添加一份力量。在这里特别要感谢各位使用者提的建议和Issue,以及为这个项目贡献代码的伙伴们 totcw、saiwu-bigkoo。关于它的介绍和使用详情,这里就不过多阐述,有兴趣请参考我的另外一篇文章:Android-PickerView系列之介绍与使用篇(一)

好了,闲话就说到这,开始进入正文,本篇文章的主要内容是讲解WheelView的实现原理以及源代码,大致分以下几个步骤:

一、实现原理
二、自定义控件
三、onMeasure 测量
四、onDraw 绘制
五、onTouchEvent监听

一、实现原理

上面我们看到的GIF图中,控件中间滚轮部分的布局,有多个WheelView, 一个WheelView 就是一个3D滚轮,我画了一张图方便大家更为直观地理解:

从上图中我们可以看到,每一项Item都是在圆弧上面, 假设我们设置的WheelView它的可见Item数目为11,那么圆的半个周长就等于 10项Item的高度。我们看到的第一象限和第四象限,它是可见区域,即Item所显示的位置。其中,每项Item的高度 ItemHeight 等于两条分隔线的高度,具体如下图所示:


(为什么要画得那么详细,因为这些参数在绘制过程中需要用到)

因此,我们可得以下结论:

1.每项Item 的高度是由文字大小以及间距倍数控制的, itemHeight = lineSpacingMultiplier * maxTextHeight;
2.圆周长 C = 2 (itemHeight *(itemsVisible - 1))
2.根据圆周长公式 C= 2πR, 可推导出圆半径R = C/2π ,圆直径 L = C/π;

二、自定义控件

  1. 创建一个WheelView 类继承自 View,覆盖onDraw、onMeasure、onTouchEvent方法.
  2. 在构造方法中初始化数据;
  3. 在构造方法中初始化三个画笔Paint,分别用于绘制选中项、未选中项、分隔线。
 private void initPaints() {paintOuterText = new Paint();paintOuterText.setColor(textColorOut);paintOuterText.setAntiAlias(true);paintOuterText.setTypeface(Typeface.MONOSPACE);paintOuterText.setTextSize(textSize);paintCenterText = new Paint();paintCenterText.setColor(textColorCenter);paintCenterText.setAntiAlias(true);paintCenterText.setTextScaleX(1.1F);paintCenterText.setTypeface(Typeface.MONOSPACE);paintCenterText.setTextSize(textSize);paintIndicator = new Paint();paintIndicator.setColor(dividerColor);paintIndicator.setAntiAlias(true);if (android.os.Build.VERSION.SDK_INT >= 11) {setLayerType(LAYER_TYPE_SOFTWARE, null);}}

三、onMeasure 测量

1.计算最大length的Text的宽高度

 private void measureTextWidthHeight() {Rect rect = new Rect();for (int i = 0; i < adapter.getItemsCount(); i++) {String s1 = getContentText(adapter.getItem(i));paintCenterText.getTextBounds(s1, 0, s1.length(), rect);int textWidth = rect.width();if (textWidth > maxTextWidth) {maxTextWidth = textWidth;}paintCenterText.getTextBounds("\u661F\u671F", 0, 2, rect); // "星期"的字符编码,用它作为标准高度int textHeight = rect.height();if (textHeight > maxTextHeight) {maxTextHeight = textHeight;}}itemHeight = lineSpacingMultiplier * maxTextHeight;//item的高度}

2.计算圆的半径和直径,求出WheelView控件的宽高度

//周长公式 C= 2πR//半圆的周长 = item高度乘以item数目-1halfCircumference = (int) (itemHeight * (itemsVisible - 1));//整个圆的周长除以PI得到直径,这个直径用作控件的总高度measuredHeight = (int) ((halfCircumference * 2) / Math.PI);//求出半径radius = (int) (halfCircumference / Math.PI);//计算控件宽度,这里支持weightmeasuredWidth = MeasureSpec.getSize(widthMeasureSpec);

3.计算两条分隔线和Label文字的基线位置

        //计算两条横线 和 选中项Label的基线centerY 位置firstLineY = (measuredHeight - itemHeight) / 2.0F;secondLineY = (measuredHeight + itemHeight) / 2.0F;centerY = secondLineY - (itemHeight-maxTextHeight)/2.0f - CENTERCONTENTOFFSET;

对于centerY 为什么要减去CENTERCONTENTOFFSET(偏移量),因为Canvas.drawText方法中的坐标参数Y,并不是文字的底部位置,而是基线位置,所以我们要微调一下位置,让显示居中:


注:图片来源于http://blog.csdn.net/zly921112/article/details/50401976

4.初始化默认显示的item的position,即选中项位置

if (initPosition == -1) {if (isLoop) {initPosition = (adapter.getItemsCount() + 1) / 2;} else {initPosition = 0;}}preCurrentIndex = initPosition;

四、onDraw 绘制

经过以上几个步骤之后,我们绘制控件所需要的各个属性值也基本上计算好了,接下来就开始在onDraw方法中进行绘制

1.绘制两条横线

 //绘制中间两条横线canvas.drawLine(0.0F, firstLineY, measuredWidth, firstLineY, paintIndicator);canvas.drawLine(0.0F, secondLineY, measuredWidth, secondLineY, paintIndicator);

2.绘制Label文字

        //显示单位Label,label不为空则进行绘制if (label != null&& !label.equals("")) {int drawRightContentStart = measuredWidth - getTextWidth(paintCenterText, label);//绘制文字,靠右并留出空隙canvas.drawText(label, drawRightContentStart - CENTERCONTENTOFFSET, centerY, paintCenterText);}

3.绘制item内容文字

终于到最核心的地方了——绘制有3D滚轮效果的Item文字。绘制它的两个关键因素:

  • item平移距离translateY。
  • 文字Y轴的缩放率 scaleY。

在绘制之前,我们需要温习一下 弧和圆、弧度、以及三角函数等概念以及它们的计算公式,对于公式有不了解的地方可以自行google 百度一下,这里就不多阐述了:

  • 弧度和角度的换算公式 α = n*π/180 (α为弧度,n为角度)
  • 弧度的计算公式 α = L / R ( 弧长/半径 )
  • 正弦和余弦转换公式 cosα = sin( π/2-α )

由于我们在onDraw方法里面,translateY和scaleY都是是通过弧度 α 计算得到的,因此需要从弧度α开始入手
  
  由计算公式 α = L / R 可知,我们若要得到某项Item的弧度,则需要知道弧长和半径,由于之前我们已经通过计算获取到了半径值,所以现在需要计算弧长。
  
  弧长L = itemHeight * counter - itemHeightOffset; 即 Item的高度乘以该项Item所在Position位置,再减去已滑动距离的偏移量(itemHeightOffset < itemHeight ),计算出了弧长L,则可计算出每项Item对应的弧度α,计算出了α之后,根据三角函数可计算出平移距离translateY,如下图 :

  • radius (半径)
  • h2=cosα * maxTextHeight/2
  • h1=sinα * radius

由上图可知,item从位置F3E3移动到 A2B2的时,平移距离 translateY = radius - h2 -h1;

求出了translateY 之后,我们还需要求出压缩率scaleY:

由上图可知 scaleY = cosn,由于代码里面参数是弧度制代表的数值,所以我们用弧度制表达 scaleY = cosα;

计算好了,我们开始撸代码,由于篇幅问题,就只贴出了部分关键代码,如下:

 /*省略部分...*/counter = 0;//position位置while (counter < itemsVisible) {canvas.save();// 弧长 L = itemHeight * counter - itemHeightOffset// 求弧度 α = L / r  (弧长/半径)double radian = ((itemHeight * counter - itemHeightOffset)) / radius;// 弧度转换成角度(把半圆以Y轴为轴心向右转90度,使其处于第一象限及第四象限float angle = (float) (90D - (radian / Math.PI) * 180D);//item第一项,从90度开始,逐渐递减到 -90度// 九十度以上的不绘制if (angle >= 90F || angle <= -90F) {canvas.restore();} else {String contentText = getContentText(visibles[counter]);reMeasureTextSize(contentText);//计算开始绘制的位置measuredCenterContentStart(contentText);measuredOutContentStart(contentText);float translateY = (float) (radius - Math.cos(radian) * radius - (Math.sin(radian) * maxTextHeight) / 2D);//根据Math.sin(radian)来更改canvas坐标系原点,然后缩放画布,使得文字高度进行缩放,形成弧形3d视觉差canvas.translate(0.0F, translateY);canvas.scale(1.0F, (float) Math.sin(radian));

“等等,大兄弟,刚刚不是说‘scaleY = cosα ’吗”,怎么缩放文字的代码是这样的:

canvas.scale(1.0F, (float) Math.sin(radian));

别急别急,这是由于为了使Item的显示位置处于第一象限及第四象限,我们把半圆以原点为中心,顺时针旋转了90度。所以我们的角度 angle = (float) (90D - (radian / Math.PI) * 180D);(使角度的取值范围为[-90°, 90°])

而根据:
1.弧度和角度转换公式 α = angle*π/180
2.正弦和余弦转换公式 cosα = sin( π/2-α )

我们就把cos函数 给转化成sin函数了。

4.文字自适应大小

由于item的文字长度是不固定的,所以会存在文字长度过长而导致绘制超过Wheelview的宽度,因此这里需要做一下处理,当文字长度超过measuredWidth的时候,重设字体大小让其能完全显示:

/*** 根据文字的长度 重新设置文字的大小 让其能完全显示* @param contentText*/private void reMeasureTextSize(String contentText) {Rect rect = new Rect();paintCenterText.getTextBounds(contentText, 0, contentText.length(), rect);int width = rect.width();int size = textSize;while (width > measuredWidth) {size--;//设置2条横线中间的文字大小paintCenterText.setTextSize(size);paintCenterText.getTextBounds(contentText, 0, contentText.length(), rect);width = rect.width();}//设置2条横线外面的文字大小paintOuterText.setTextSize(size);}

五、onTouchEvent监听

一次静态图形绘制完成了,但是我们需要根据滑动距离让item文字动态地变换,以达到需求,那么如何根据滑动距离来控制UI变化呢?

别着急,之前已经说了定义WheelView 的时候需要覆盖onTouchEvent方法,必然是有它的道理的。我们通过onTouchEvent方法,然后:

1.分别处理MotionEvent.ACTION_DOWN、ACTION_MOVE、ACTION_UP 这三个事件,获取到滑动距离并记录下来。
2.根据获取的滑动距离、计算停止滑动时item所需要的偏移量 ,边界处理等工作。
3.最后调用invalidate()方法通知系统更新UI,相当于重新调用了onDraw()方法,重新绘制UI,实现3D滚轮效果。

由于篇幅问题,就只贴一下部分关键代码了:

@Overridepublic boolean onTouchEvent(MotionEvent event) {boolean eventConsumed = gestureDetector.onTouchEvent(event);switch (event.getAction()) {//按下case MotionEvent.ACTION_DOWN:startTime = System.currentTimeMillis();cancelFuture();previousY = event.getRawY();break;//滑动中case MotionEvent.ACTION_MOVE:float dy = previousY - event.getRawY();previousY = event.getRawY();totalScrollY = (int) (totalScrollY + dy);// 边界处理。if (!isLoop) {float top = -initPosition * itemHeight;float bottom = (adapter.getItemsCount() - 1 - initPosition) * itemHeight;if (totalScrollY - itemHeight * 0.3 < top) {top = totalScrollY - dy;} else if (totalScrollY + itemHeight * 0.3 > bottom) {bottom = totalScrollY - dy;}if (totalScrollY < top) {totalScrollY = (int) top;} else if (totalScrollY > bottom) {totalScrollY = (int) bottom;}}break;//完成滑动,手指离开屏幕case MotionEvent.ACTION_UP:default:// 弧长 L = α*R// 反余弦公式:arccos(cosα)= α// 由于之前是有向右偏移90度,所以 实际弧度范围为// α2 =π/2-α (α=[0,π] α2 = [-π/2,π/2])// 根据正弦余弦转换公式 cosα = sin(π/2-α)// 因此 cosα = sin(π/2-α) = sinα2 = (radius - y) / radius// 所以弧长 L = arccos(cosα)*R = arccos((radius - y) / radius)*Rif (!eventConsumed) {float y = event.getY();double L = Math.acos((radius - y) / radius) * radius;int circlePosition = (int) ((L + itemHeight / 2) / itemHeight);float extraOffset = (totalScrollY % itemHeight + itemHeight) % itemHeight;mOffset = (int) ((circlePosition - itemsVisible / 2) * itemHeight - extraOffset);if ((System.currentTimeMillis() - startTime) > 120) {// 处理拖拽事件smoothScroll(ACTION.DAGGLE);} else {// 处理条目点击事件smoothScroll(ACTION.CLICK);}}break;}invalidate();return true;}

结尾语

以上是WheelView的实现原理以及绘制过程的讲解,但是实际使用中我们往往需要多个Wheelview 并设置联动来实现我们的功能,所以Android-PickerView,这个项目就是对其进行了很好的封装,提供了两种选择器,一种是时间选择器(timePicker),一种是选择选择器(optionPicker)。

完整项目代码请到Github下载,这里是地址链接:Android-PickerView。

Android-PickerView系列之源码解析篇(二)相关推荐

  1. android handler2--消息队列源码解析

    android handler2–消息队列源码解析 1.Looper 对于Looper主要是prepare()和loop()两个方法. 首先看prepare()方法 public static fin ...

  2. Tightly Coupled LiDAR Inertial Odometry and Mapping源码解析(二)

    Tightly Coupled LiDAR Inertial Odometry and Mapping源码解析(二) 3. Joint optimization 3.1 Marginalization ...

  3. Android 双开沙箱 VirtualApp 源码分析(二)

    Android 双开沙箱 VirtualApp 源码分析(二) VA 初始化 先看一下代码: VirtualCore.startup public void startup(Context conte ...

  4. PyCrypto密码学库源码解析(二)RSA参数生成

    Python Crypto库源码解析(二) RSA参数生成 * 版权声明 * 引用请注明出处,转载请联系: h0.1c@foxmail.com 本文主要讲解pycrypto库中RSA参数生成的实现方法 ...

  5. netty依赖_Netty系列之源码解析(一)

    接下来的时间灯塔君持续更新Netty系列一共九篇 当前:Netty 源码解析(一)开始 Netty 源码解析(二): Netty 的 Channel Netty 源码解析(三): Netty 的 Fu ...

  6. Android手游 “2048” 源码解析

    转载请写明出处:http://blog.csdn.net/big_heart_c 下面所解析的源码是来自极客学院"Android 2048 "中的源码,读者可以从 https:// ...

  7. Android Hawk数据库的源码解析,Github开源项目,基于SharedPreferences的的存储框架

    今天看了朋友一个项目用到了Hawk,然后写了这边文章 一.了解一下概念 Android Hawk数据库github开源项目 Hawk是一个非常便捷的数据库.操作数据库只需一行代码,能存任何数据类型. ...

  8. android网络框架retrofit源码解析二

    注:源码解析文章参考了该博客:http://www.2cto.com/kf/201405/305248.html 前一篇文章讲解了retrofit的annotation,既然定义了,那么就应该有解析的 ...

  9. 谷歌BERT预训练源码解析(二):模型构建

    版权声明:本文为博主原创文章,遵循 CC 4.0 BY-SA 版权协议,转载请附上原文出处链接和本声明. 本文链接:https://blog.csdn.net/weixin_39470744/arti ...

最新文章

  1. Python 21 Flask(二)上下文管理详解
  2. 面向对象编程基础 (一)
  3. Oracle的悲观锁和乐观锁
  4. SAP-检查多个关联字段输入,以及有条件地调用模块
  5. Oracle char 查询问题
  6. 利用python计算偏差-方差权衡
  7. python编写请求参数带文件_转载:如何编写一个带命令行参数的Python文件
  8. Codeforces Round #631 (Div. 2) D. Dreamoon Likes Sequences 思维 + 组合数学
  9. Unity(一)必然事件
  10. Linux 下的推迟执行
  11. Oracle数据库管理›oracle内部的jdk版本
  12. eureka上的微服务不能通过服务名调用_掌门教育微服务体系 Solar | 阿里巴巴 Nacos 企业级落地上篇...
  13. MySQL-第三篇SQL语句基础(2)数据库约束
  14. 用Java实现文本编辑器:创建、浏览、编辑文件;剪贴、复制、粘贴;保存、另存为;字符统计;自动换行
  15. 数据库实验报告-实验一:SQL语言
  16. UCenter Home 正式开源 促进国内SNS网站发展
  17. gyb的常用lazyCopy
  18. #今日论文推荐#罗格斯大学王灏:保持孩子般的好奇心
  19. 国产免费倾斜摄影模型在线发布平台,一键查看、编辑、分享场景!
  20. HP DL380 G7安装部署VM ESXI6.5.0

热门文章

  1. 蒲公英内测托管平台是干什么的?
  2. 图像处理之混合空间增强——(Java:拉普拉斯锐化、Sobel边缘检测、均值滤波、伽马变换)
  3. 河南专升本公共英语语法考点分析---非谓语动词
  4. 主力用计算机吸筹,主力进场 主力吸筹
  5. MySQLdb._exceptions.OperationalError: (1045, “Access denied for user ‘root‘@‘localhost‘ (using passw
  6. 10 本最值得阅读的网络安全书籍推荐
  7. [jobdu]二进制中1的个数
  8. python甜橙歌曲音乐网站平台源码
  9. mysql 取24小时数据_mysql获取24小时前数据
  10. RuoYi-Flowable 工作流管理平台