这次的分享也长得挺好看的,但是不敢说“不太复杂”。虽然没有用到很高级的API(这篇分享还是以canvas的各个API调用为主),但是在涉及到计算的地方,确实是有些复杂(繁琐)。让我们看看这次的效果:
效果说明
这里主要说明两点:
除了中间旋转的图片,所有其它都是“画”上去的。包括上面的唱针(就是那个摆动的手臂,在唱片机/留声机里面的叫法,好像是唱针)都是绘制上去的,笔者看过其它博客实现方式,有直接拿ImageView或者一个唱针的Bitmap来旋转的,好坏不评价,但是从学习的角度来讲,自己绘制比较过瘾。
关于动画,每一次的绘制都是静态的,所谓的动画都是通过不断重绘,在重绘的同时改变旋转角度来实现的,最终的动画不过是眼睛的感觉而已。接下来看设计思路。
设计思路——绘制唱针
如图所示,唱针分为三个部分,第一个部分是旋转支点,就是顶部的两个重叠的圆,第二个部分是手臂,一长一短的两节,第三个部分就是头部,头部也有一长一短两段。但实际上这三个部分与绘制有关的API只需要两个:canvas.drawCircle()、canvas.drawLine()。第一个部分只要绘制两个同心圆即可,手臂和头都可以用canvas.drawLine()来绘制,只要改变Paint的宽度,就能有手臂、头的效果。
这里主要说一下唱针这四条线段的绘制方法,一个比较直观的做法是定下第一条线段起点的坐标,然后根据四条线段的长度,拐角角度,通过三角函数算出各个点坐标,然后就能绘制啦。。。。想想都觉得心好累,如果你选的不是诸如30°、45°、60°、90°这种比较特殊的角度,其他的角度算起来各种麻烦,就算你选的是特殊角度,算起来也很麻烦,还容易出错。那有没有方便的做法?有。只要利用canvas.translate()和canvas.rotate()方法(即平移和旋转坐标系),就可以只关注线段长度,彻底抛弃三角函数,来看看流程示意图:
根据图片所示流程,就可以得出代码了:
/** * 绘制旋转了指定角度的唱针。 * 说明一下旋转了指定角度什么意思,看上面的流程图可以知道, * 长的那段手臂和垂直方向是成角15°的,实际上这个角度不是一成不变的, * 通过控制这个角度变化,可以达到唱针处于播放/暂停状态或者在两个状态之间摆动的效果。 */private void drawNeedle(Canvas canvas, int degree){ // 移动坐标到水平中点 canvas.save(); canvas.translate(halfMeasureWidth, 0); // 准备绘制唱针手臂 needlePaint.setStrokeWidth(20); needlePaint.setColor(Color.parseColor("#C0C0C0")); // 绘制第一段臂 canvas.rotate(degree); canvas.drawLine(0, 0, 0, longArmLength, needlePaint); // 绘制第二段臂 canvas.translate(0, longArmLength); canvas.rotate(-30); canvas.drawLine(0, 0, 0, shortArmLength, needlePaint); // 绘制唱针头 // 绘制第一段唱针头 canvas.translate(0, shortArmLength); needlePaint.setStrokeWidth(40); canvas.drawLine(0, 0, 0, longHeadLength, needlePaint); // 绘制第二段唱针头 canvas.translate(0, longHeadLength); needlePaint.setStrokeWidth(60); canvas.drawLine(0, 0, 0, shortHeadLength, needlePaint); canvas.restore(); // 两个重叠的圆形,即唱针顶部的旋转点 canvas.save(); canvas.translate(halfMeasureWidth, 0); needlePaint.setStyle(Paint.Style.FILL); needlePaint.setColor(Color.parseColor("#C0C0C0")); canvas.drawCircle(0, 0, bigCircleRadius, needlePaint); needlePaint.setColor(Color.parseColor("#8A8A8A")); canvas.drawCircle(0, 0, smallCircleRadius, needlePaint); canvas.restore(); }
设计思路——绘制唱片
唱片分为两个部分,一个是黑色圆环,一个是圆形图片。
黑色圆环很好办,绘制一个空心圆就行,Paint宽度调大一些,就有圆环的效果了。
比较特殊的是图片,图片素材本身肯定是矩形的,那么怎么绘制一个圆形的图片呢?Canvas提供有裁切绘制区域的API——canvas.clipPath(),它接受一个Path对象,然后Path有添加圆形的方法——path.addCircle(),这两个API结合起来,就可以裁切出一个圆形区域,此时调用canvas.drawBitmap()方法,只有处于该圆形区域的内容会被绘制,其它内容不会绘制,这样就能达到绘制圆形图片的效果了。接下来是绘制唱针的代码:
// 绘制旋转了指定角度的唱片(类似唱针,唱片里面的图片是会旋转不同角度的)private void drawDisk(Canvas canvas, float degree){ // 移动坐标系到唱针下方合适位置,然后旋转指定角度 canvas.save(); canvas.translate(halfMeasureWidth, pictureRadius+ringWidth+longArmLength); canvas.rotate(degree); // 绘制圆环 canvas.drawCircle(0, 0, pictureRadius+ringWidth/2, discPaint); // 绘制图片 canvas.clipPath(clipPath); canvas.drawBitmap(bitmap, srcRect, dstRect, discPaint); canvas.restore(); }
由于在onDraw()方法里面不适合创建对象,所以把Path的初始化代码单独拿出来:
Path clipPath = new Path(); clipPath.addCircle(0, 0, pictureRadius, Path.Direction.CW);
至此,唱片机的静态样式就绘制完成了,接下来看动态效果。
设计思路——动画效果的实现
唱针角度的选择
关于动画效果,需要先说一下唱针的角度问题。在笔者的设计里,当唱针第一段手臂和垂直方向成角15°时,表明处于播放状态,当唱针第一段手臂和垂直方向成角45°时,表明处于暂停状态。在两者之间就属于播放/暂停切换的动画。又由于在canvas.rotate()方法里面,逆时针角度是负数,顺时针是正数,所以得出的两个角度常量是这样的:
// 播放状态时唱针的旋转角度private static final int PLAY_DEGREE = -15; // 暂停状态时唱针的旋转角度private static final int PAUSE_DEGREE = -45;
重绘的触发点
既然要支持动画,就要不断地重绘,那么怎么判断什么时候需要重绘?我们既要绘制唱针,又要绘制唱片,难道要在两个函数分别判断?其实不用。仔细观察本文开头的动态效果图,你会发现,其实只有处于暂停的状态时,才停止重绘(即停止动画)。其它的不论出于播放状态还是播放/暂停切换状态,都要保持不断重绘。那么重绘的触发点就有了:
@Overrideprotected void onDraw(Canvas canvas) { super.onDraw(canvas); // ........省略绘制唱片和唱针的代码........ // 如果唱针当前角度大于暂停状态下的角度(注意了由于是负数所以是大于), // 继续重绘 if(needleDegreeCounter > PAUSE_DEGREE){ invalidate(); } }
角度计数器
主要说的是用来控制唱针和唱片角度的计数器,也是实现动画效果的关键计数器。由于唱针播放/暂停状态之间相差了30°,所以每次变化的角度,就选择30的约数,笔者选择了3°。对于唱片,控制唱片每次旋转角度变化,就等于控制了整个唱片机看起来的旋转速度,虽然一周是360°,但是唱片每次变化角度不需要是360°的约数,因为每次叠加之后都会对360取余数,以达到循环转动的效果。现在可以来看看关于动画的几部分关键代码:
@Overrideprotected void onDraw(Canvas canvas) { super.onDraw(canvas); halfMeasureWidth = getMeasuredWidth()>>1; // 绘制唱片 drawDisk(canvas); // 绘制唱针 drawNeedle(canvas); // 根据唱针当前角度判断是否继续重绘 if(needleDegreeCounter > PAUSE_DEGREE){ invalidate(); } }// 绘制唱片,该方法主要是控制旋转角度用的private void drawDisk(Canvas canvas){ // 这里的diskRotateSpeed变量就是唱片每次变化角度,就是旋转速度的意思 diskDegreeCounter = diskDegreeCounter%360+diskRotateSpeed; // 该方法就是前面讨论唱片绘制时说的“绘制旋转了指定角度的唱片”方法 drawDisk(canvas, diskDegreeCounter); }// 绘制唱针,该方法主要是控制唱针旋转角度用的private void drawNeedle(Canvas canvas){ // 根据播放/暂停状态控制唱针角度的加/减变化 if(isPlaying){ if(needleDegreeCounter < PLAY_DEGREE){ needleDegreeCounter+=3; } } else { if(needleDegreeCounter > PAUSE_DEGREE){ needleDegreeCounter-=3; } } // // 该方法就是前面讨论唱针绘制时说的“绘制旋转了指定角度的唱针”方法 drawNeedle(canvas, needleDegreeCounter); }
至此,关于整个View的静态、动画效果的介绍,都说完了,但是事情还没完,还有一些很关键的细节问题。接下来继续。
灵活适配——与尺寸相关的问题
整个View的绘制牵扯到若干尺寸(或者说叫做长度),比如图片的半径,圆环半径,唱针四个部分的长度。还有一个很关键的问题就是,唱针和唱片之间的位置关系,我们来看下播放状态的静态图。
可以看到,播放状态下的唱针头,是要刚好摆放在圆环上的,既不能没压到圆环上,也不能压到旋转的图片上。这就要求我们前面说的那几个尺寸之间有一定的计算关系,否则的话就会出现下面这些bug:
第一个情况:唱针压到图片上了
第二个情况:唱针离唱片太远了
那么现在问题来了,到底尺寸该怎么设计,怎样的长度关系才能避免这些bug呢?从理论的层面来讲,笔者也没有什么推理论证的办法,不过笔者经过反复尝试,最终得到一个还算合适的尺寸关系,在笔者能搞到的三台屏幕大小和分辨率不同的手机上测试,效果还可以。这里直接放笔者的结果吧:
/** * 尺寸关系设计说明: * 1、唱片有两个主要尺寸:中间图片的半径、黑色圆环的宽度。 * 黑色圆环的宽度 = 图片半径的一半。 * 2、唱针分为“手臂”和“头”,手臂分两段,一段长的一段短的,头也是一段长的一段短的。 * 唱针四个部分的尺寸求和 = 唱片中间图片的半径+黑色圆环的宽度(即整个唱片的半径) * 唱针各部分长度比例————长的手臂:短的手臂:长的头:短的头 = 8:4:2:1 * 3、唱片黑色圆环顶部(即唱片顶部)到唱针顶端的距离 = 唱针长的手臂的长度。 */
基于以上的尺寸关系,整个View的各个尺寸,基本都依赖于唱片的图片半径,只要确定了唱片里的图片半径,其他各个尺寸都能算出来。那么这个图片半径怎么确定?没有什么特别的法则,因为这个变量是要提供给调用者来设置的,也可以提供给xml属性,当然了我们也会给他一个默认数值。
至此,关于尺寸适配的问题,也说完了。
添加对xml属性的支持
为了让这个唱片机功能更饱满(完善),我们来加上xml属性,首先在res/values/路径下新建attrs.xml文件,然后添加标签作为属性声明:
<declare-styleable name="GramophoneView"> <!--图片半径,不是整个唱片的半径,只是图片的半径--> <attr name="picture_radius" format="dimension"/> <!--图片资源id--> <attr name="src" format="reference"/> <!--唱片旋转速度,实际上在Java代码里面被当做唱片旋转时每次变化的角度--> <attr name="disk_rotate_speed" format="float"/></declare-styleable>
一个还没有解决的问题
这是一个关于图片加载的问题。有相关经验的同学都知道,对于尺寸未知的图片,在加载到内存之前,需要进行各种运算,主要是为了避免内存和性能的问题。这是笔者在这个View里面没有解决的问题,但是笔者也不打算解决,因为关于图片加载、尺寸计算、优化这些知识点,几篇博客都讲不完,在这里讲这些,还是算了吧。。。。笔者的这个demo里面选的都是尺寸不太大的图片,所以运行起来不会出大事,如果读者要将这个demo用于项目里,可套用一些第三方框架处理图片问题。
小结
总体来讲调用的API不算复杂,主要是应用了一些技巧,复杂的地方都在尺寸相关的计算上。大概总结如下几点:
整个View主要包括绘制唱针、唱片、实现动画效果、适配尺寸四个问题。
唱针的绘制主要通过canvas.rotate()和canvas.translate()方法代替复杂的三角函数运算。唱片的实现主要通过canvas.clipPath()来实现圆形图片绘制。
动画效果的本质是不断重绘,重点在于对角度计数器的计算以及对关键角度的控制和判断。
尺寸适配主要是在唱针和唱片各个尺寸之间建立联系,难点在于当唱片设置了不同大小(半径)时,唱针和唱片之间仍然能保持合适的位置关系。
不足之处在于剩下一个没有处理的问题:图片加载。
共同学习,写下你的评论
评论加载中...
作者其他优质文章