Android-PickerView系列之源码解析篇(二)
前言
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/π;
二、自定义控件
- 创建一个WheelView 类继承自 View,覆盖onDraw、onMeasure、onTouchEvent方法.
- 在构造方法中初始化数据;
- 在构造方法中初始化三个画笔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系列之源码解析篇(二)相关推荐
- android handler2--消息队列源码解析
android handler2–消息队列源码解析 1.Looper 对于Looper主要是prepare()和loop()两个方法. 首先看prepare()方法 public static fin ...
- Tightly Coupled LiDAR Inertial Odometry and Mapping源码解析(二)
Tightly Coupled LiDAR Inertial Odometry and Mapping源码解析(二) 3. Joint optimization 3.1 Marginalization ...
- Android 双开沙箱 VirtualApp 源码分析(二)
Android 双开沙箱 VirtualApp 源码分析(二) VA 初始化 先看一下代码: VirtualCore.startup public void startup(Context conte ...
- PyCrypto密码学库源码解析(二)RSA参数生成
Python Crypto库源码解析(二) RSA参数生成 * 版权声明 * 引用请注明出处,转载请联系: h0.1c@foxmail.com 本文主要讲解pycrypto库中RSA参数生成的实现方法 ...
- netty依赖_Netty系列之源码解析(一)
接下来的时间灯塔君持续更新Netty系列一共九篇 当前:Netty 源码解析(一)开始 Netty 源码解析(二): Netty 的 Channel Netty 源码解析(三): Netty 的 Fu ...
- Android手游 “2048” 源码解析
转载请写明出处:http://blog.csdn.net/big_heart_c 下面所解析的源码是来自极客学院"Android 2048 "中的源码,读者可以从 https:// ...
- Android Hawk数据库的源码解析,Github开源项目,基于SharedPreferences的的存储框架
今天看了朋友一个项目用到了Hawk,然后写了这边文章 一.了解一下概念 Android Hawk数据库github开源项目 Hawk是一个非常便捷的数据库.操作数据库只需一行代码,能存任何数据类型. ...
- android网络框架retrofit源码解析二
注:源码解析文章参考了该博客:http://www.2cto.com/kf/201405/305248.html 前一篇文章讲解了retrofit的annotation,既然定义了,那么就应该有解析的 ...
- 谷歌BERT预训练源码解析(二):模型构建
版权声明:本文为博主原创文章,遵循 CC 4.0 BY-SA 版权协议,转载请附上原文出处链接和本声明. 本文链接:https://blog.csdn.net/weixin_39470744/arti ...
最新文章
- Python 21 Flask(二)上下文管理详解
- 面向对象编程基础 (一)
- Oracle的悲观锁和乐观锁
- SAP-检查多个关联字段输入,以及有条件地调用模块
- Oracle char 查询问题
- 利用python计算偏差-方差权衡
- python编写请求参数带文件_转载:如何编写一个带命令行参数的Python文件
- Codeforces Round #631 (Div. 2) D. Dreamoon Likes Sequences 思维 + 组合数学
- Unity(一)必然事件
- Linux 下的推迟执行
- Oracle数据库管理›oracle内部的jdk版本
- eureka上的微服务不能通过服务名调用_掌门教育微服务体系 Solar | 阿里巴巴 Nacos 企业级落地上篇...
- MySQL-第三篇SQL语句基础(2)数据库约束
- 用Java实现文本编辑器:创建、浏览、编辑文件;剪贴、复制、粘贴;保存、另存为;字符统计;自动换行
- 数据库实验报告-实验一:SQL语言
- UCenter Home 正式开源 促进国内SNS网站发展
- gyb的常用lazyCopy
- #今日论文推荐#罗格斯大学王灏:保持孩子般的好奇心
- 国产免费倾斜摄影模型在线发布平台,一键查看、编辑、分享场景!
- HP DL380 G7安装部署VM ESXI6.5.0
热门文章
- 蒲公英内测托管平台是干什么的?
- 图像处理之混合空间增强——(Java:拉普拉斯锐化、Sobel边缘检测、均值滤波、伽马变换)
- 河南专升本公共英语语法考点分析---非谓语动词
- 主力用计算机吸筹,主力进场 主力吸筹
- MySQLdb._exceptions.OperationalError: (1045, “Access denied for user ‘root‘@‘localhost‘ (using passw
- 10 本最值得阅读的网络安全书籍推荐
- [jobdu]二进制中1的个数
- python甜橙歌曲音乐网站平台源码
- mysql 取24小时数据_mysql获取24小时前数据
- RuoYi-Flowable 工作流管理平台