Android自定义Drawable第十五式之啡常OK
前言
在上一篇的自定义Drawable中,我们学习了如何在Canvas上draw
一个射箭的动画,不过那个动画是以线条为主的,多看几眼可能就会觉得没味道了,那么在本篇文章,将和同学们一起做一个看起来更耐看,更丝滑的动画。
先看看效果图:
哈哈,小手是不是很可爱? O不OK?。
这个动画看上去挺难,但实际上还没有上一篇的射箭动画复杂。我们等下还会用上一些技巧,来简化画各个元素的步骤。
初步分析
先看看茄子同学画的这张图:
和上一篇的方式一样:先把各个组成部分拆开。
那这个杯子就可以拆分成:杯身、手柄、杯底、手柄底、还有咖啡。
- 咖啡的话,我们能很直观的看出来,就是一个咖啡色的实心圆形,再加上边缘的【透明~白色】放射渐变;
- 杯身其实也是一个圆形,只是它的直径比咖啡要大一点;
- 手柄看上去是一个旋转了45°的圆角矩形;
- 杯底和手柄底,其实也就是偏移一下位置,改一下颜色,重新画杯身和手柄罢了;
不过我们在画的时候,顺序刚好和上面的顺序相反,因为咖啡的圆形是在最上面,而杯底和手柄底则在最底层。
现在来看看手要怎么画:
看上去好像挺难,先不管,来拆分一下吧:
- 两只竖起来像K型的手指,看着是两个圆角矩形;
- 拇指和食指组成的O型手势,可以用一个圆弧来做;
- 保持垂直的手臂,其实就是一个矩形;
画手指技巧
如果手指和刚刚的手柄用圆角矩形
来画的话,就会很麻烦,因为除了要计算[l, t, r, b]
之外,还要计算和处理旋转角度。
那应该用哪种方式呢?
熟悉Paint的同学会知道一个叫Cap的东西,它可以改变线条端点的样式,一共有三种,分别是:BUTT、ROUND、SQUARE。默认情况下是第一个,但因为现在我们要把线条的端点变成圆,也就是要用第二个了。
来测试一下:
//设置端点样式为圆形mPaint.strokeCap = Paint.Cap.ROUND//线条mPaint.style = Paint.Style.STROKE//白色mPaint.color = Color.WHITE//加大线宽mPaint.strokeWidth = 100F//画线canvas.drawLine(100F, 100F, 800F, 800F, mPaint)
看看效果:
emmm,没错了,等下画手柄和手指,都可以用这个方法来做,这样就方便了很多。
创建Drawable
像上次那样,先创建一个类继承自Drawable,然后把最基本的几个方法重写(因为我们这次要做的是搅拌咖啡的效果,名字就叫CoffeeDrawable了):
class CoffeeDrawable(private var width: Int, private var height: Int) : Drawable() {private var paint = Paint()init {initPaint()updateSize(width, height)}private fun initPaint() = paint.run {isAntiAlias = truestrokeCap = Paint.Cap.ROUNDstrokeJoin = Paint.Join.ROUND}fun updateSize(width: Int, height: Int) {this.width = widththis.height = height}override fun draw(canvas: Canvas) {}override fun getIntrinsicWidth() = widthoverride fun getIntrinsicHeight() = heightoverride fun getOpacity() = PixelFormat.TRANSLUCENToverride fun setAlpha(alpha: Int) {paint.alpha = alpha}override fun setColorFilter(colorFilter: ColorFilter?) {paint.colorFilter = colorFilter}
}
画杯
好,先来画静态的杯子。刚刚分析过,杯子大致就是圆形 + 粗线条(手柄) 的组合,那么在画的时候就需要先定义以下变量:
- 中心点坐标:
centerX
,centerY
(因为杯子是在Drawable的中心处); - 杯子半径
cupRadius
、咖啡半径coffeeRadius
、手柄宽度cupHandleWidth
; - 最后一个,杯底的偏移量
cupBottomOffset
;
为了能适应各种尺寸的Drawable容器,这些变量应该基于Drawable的width
或height
来动态计算,而不是随便指定某个值。
这样的话,当Drawable尺寸变大时,我们的杯子也能跟着变大,缩小时,也能跟着缩小:
fun updateSize(width: Int, height: Int) {//水平中心点centerX = width / 2F//垂直中心点centerY = height / 2F//杯子半径cupRadius = width / 12F//咖啡半径coffeeRadius = cupRadius * .95F//杯子手柄宽度cupHandleWidth = cupRadius / 3F//杯底偏移量cupBottomOffset = cupHandleWidth / 2}
可以看到,杯子的半径指定为Drawable宽度的1/12
,咖啡的半径则取杯子半径的95%
,手柄宽度是杯半径的1/3
,而杯底的偏移量则是手柄宽度的一半。
看看怎么画:
private fun drawCup(canvas: Canvas) {/// 先画底部,所以是先偏移canvas.translate(0F, cupBottomOffset)//杯底颜色paint.color = -0xFFA8B5//要画实心的圆paint.style = Paint.Style.FILL//画杯底canvas.drawCircle(centerX, centerY, cupRadius, paint)//手柄是线条paint.style = Paint.Style.STROKE//宽度paint.strokeWidth = cupHandleWidth//画手柄底部canvas.drawLine(centerX, centerY, centerX + cupRadius, centerY + cupRadius, paint)/// 画完之后,偏移回来,继续画上面一层canvas.translate(0F, -cupBottomOffset)//杯身颜色paint.color = Color.WHITEpaint.style = Paint.Style.FILL//画杯身canvas.drawCircle(centerX, centerY, cupRadius, paint)//画手柄paint.style = Paint.Style.STROKEcanvas.drawLine(centerX, centerY, centerX + cupRadius, centerY + cupRadius, paint)//咖啡颜色paint.color = -0x81A4C2paint.style = Paint.Style.FILL//画咖啡canvas.drawCircle(centerX, centerY, coffeeRadius, paint)}
我们先将画布向下偏移指定距离,画完底部两个元素(杯底,手柄底)之后,重新把画布偏移回原来位置,然后开始画杯身、手柄还有咖啡。
里面的颜色,在这里为了方便理解就直接写死了,正常情况应该用变量保存起来,方便动态修改。
还可以看到,画手柄时,直接从中心处延伸了一条线出来,那么这条线的长度就是一个腰长为cupRadius
的直角等腰三角形的底边长度。
好,来看看效果:
emmm,还差个边缘渐变的效果。
想一下,这个渐变的结束颜色的RGB,一定要跟杯身的一样,才不会有违和感。而且还要半透明,因为如果色值完全一样的话,就会和杯壁混在一起,显得很笨重。
所以我们要先把杯身颜色
变成半透明,然后再生成一个RadialGradient对象:
private fun initCoffeeShader() {if (coffeeRadius > 0) {//半透明val a = 128//把rgb先取出来val r = Color.red(cupBodyColor)val g = Color.green(cupBodyColor)val b = Color.blue(cupBodyColor)//获得一个半透明的颜色val endColor = Color.argb(a, r, g, b)//渐变色,从全透明到半透明val colors = intArrayOf(Color.TRANSPARENT, endColor)//全透明的范围从中心出发,到距离边缘的30%处结束,然后慢慢过渡到半透明val stops = floatArrayOf(.7F, 1F)coffeeShader = RadialGradient(centerX, centerY, coffeeRadius, colors, stops, Shader.TileMode.CLAMP)}}
加入到上面的updateSize
方法中:
fun updateSize(width: Int, height: Int) {............initCoffeeShader()invalidateSelf()}
在drawCup
方法中draw出来:
private fun drawCup(canvas: Canvas) {............paint.shader = coffeeShadercanvas.drawCircle(centerX, centerY, coffeeRadius, paint)paint.shader = null}
好,来看看现在的效果:
OK啦。
画手
跟着前面的思路:O型手指是圆弧、K型手指是线条、手臂是矩形。
那应该怎么定位这些元素呢?根据什么来定位?
我们知道,画圆弧需要提供一个矩形[l, t, r, b]
,那K型手指(线条)的一个端点,它的x
坐标就可以对齐这个矩形的右边,y
轴可以取矩形的top
+ height / 2
,也就是垂直居中。
手臂的话,可以先决定好宽度
,然后它的right
像K手指一样,与O手指矩形的右边相对齐,y
轴相对于O手指矩形垂直居中就行了。
那么整只手的架构,就像这样:
emmm,等下draw的时候,除手臂矩形是实心之外其他地方只需要加大线条宽度就行了(红色框不用,现在画出来只是为了方便理解)。
来看看代码怎么写:
首先是尺寸的定义,等下要用到:手指宽度
、K手指长度
x2(因为K手势的两只手指长度是不同的),O手势的半径
,手臂宽度
。
//手指宽度fingerWidth = cupHandleWidth//第二根手指长度finger2Length = cupRadius * 1.2F//第一根手指长度finger1Length = finger2Length * .8F//手指O形状半径fingerORadius = cupRadius / 2F//手臂宽度armWidth = cupRadius
跟前面一样,都是计算的相对尺寸:
- 手指的宽度,我们指定它跟咖啡杯手柄的宽度一样;
- 第二根手指长度,是咖啡杯半径的1.2倍;
- 第一根手指长度比第二根短了20%;
- O型手指的O半径,取咖啡杯半径的一半;
- 手臂的宽度,直接跟咖啡杯半径一样大;
接着按刚刚的思路画,首先初始化那个矩形:
private fun updateOFingerRect() {//o手指的中心点坐标val oCenterX = width / 2Fval oCenterY = height / 2F//根据o手指的半径来计算出矩形的边界val left = oCenterX - fingerORadiusval top = oCenterY - fingerORadiusval right = left + fingerORadius * 2val bottom = top + fingerORadius * 2//更新矩形尺寸oFingerRect.set(left, top, right, bottom)}
有了矩形之后,开始根据这个矩形来画圆弧:
private fun updateOFingersPath() {//预留开口角度为30度val reservedAngle = 30F//起始角度val startAngle = 180 + reservedAngle//扫过的角度val sweepAngle = 360 - reservedAngle * 2oFingersPath.reset()oFingersPath.addArc(oFingerRect, startAngle, sweepAngle)}
预留的开口角度现在写死为30度,等下我们会根据搅拌棒的宽度来动态计算这个值。
接下来到K手势了:
private fun updateKFingersPath() {//o手指的中心点坐标val oCenterY = height / 2FkFingersPath.reset()//第一根手指kFingersPath.moveTo(oFingerRect.right, oCenterY)kFingersPath.rLineTo(-fingerWidth, -finger1Length)//第二根手指kFingersPath.moveTo(oFingerRect.right, oCenterY)kFingersPath.rLineTo(0F, -finger2Length)}
两只手指的起始点,都像刚刚说的那样,在O手势矩形的右边,并且垂直居中。
定位了起点之后,会向上拉(-fingerLength
)。
两条线除了上拉的高度不同之外,其中一条线的结束点还向左边偏移了一个手指宽度的距离,避免重叠。
最后是手臂的Path:
private fun updateArmPath() {val oCenterY = height / 2Fval halfFingerWidth = fingerWidth / 2val left = oFingerRect.right - armWidth + halfFingerWidthval top = oCenterYval right = oFingerRect.right + halfFingerWidth//底部直接对齐Drawable的底部,看上去就像是从底部伸出来的样子val bottom = height.toFloat()armPath.reset()armPath.addRect(left, top, right, bottom, Path.Direction.CW)}
可以看到手臂的矩形向右偏移了半个手指宽度,这是为了能对齐手指线条的右边。
因为线条在增加宽度时,是向两侧扩展的,我们把矩形向右偏移宽度的1/2,就刚好能对齐了。
好,现在把手指和手臂都draw出来:
override fun draw(canvas: Canvas) {drawHand(canvas)}private fun drawHand(canvas: Canvas) {//初始化各个元素updateOFingerRect()updateOFingersPath()updateKFingersPath()updateArmPath()//画手臂drawArm(canvas)//画手指drawOKFingers(canvas)}private fun drawArm(canvas: Canvas) {paint.style = Paint.Style.FILLpaint.color = -0x16386ccanvas.drawPath(armPath, paint)}private fun drawOKFingers(canvas: Canvas) {paint.style = Paint.Style.STROKEpaint.strokeWidth = fingerWidthcanvas.drawPath(oFingersPath, paint)canvas.drawPath(kFingersPath, paint)}
看看效果:
emmm,现在手臂的矩形,凸出了一部分,我们要把它给剪掉(差集运算,手臂矩形Path - O型手指Path)。
有同学可能会想:op运算不是只能计算封闭的Path的吗?你一条弧线怎么减?
虽然现在看上去只是一条弧线,但当你用作op运算的时候,它的形状是闭合的,就像是偷偷调用了close
方法一样。
来修改下updateArmPath
方法:
private fun updateArmPath() {............//剪掉与O形状手指所重叠的地方armPath.op(oFingersPath, Path.Op.DIFFERENCE)}
很简单,就在方法的最后加上这句就行了。
看看现在的效果:
棒~
画搅拌棒
现在手和杯都已经画出来了,接下来我们要借助一样东西把它们连接在一起,这个东西就是搅拌棒。
来看看茄子同学画的这张图:
跟前面的思路一样,搅拌棒同样也可以用一条线来实现。
先把搅拌棒和手连在一起:
可以看到,这条线右边的端点,是在O形状手指(圆弧)的左侧,并和它垂直居中。
来看看代码怎么写,先是更新搅拌棒坐标的方法:
private fun updateStickLocation() {stickStartPoint.set(centerX, centerY)//结束点先和起始点一样stickEndPoint.set(stickStartPoint)//结束点再向右偏移一个杯半径的距离stickEndPoint.offset(cupRadius, 0F)}
stickStartPoint
和stickEndPoint
分别是搅拌棒起始点和结束点的PointF对象实例。
我们暂时把搅拌棒的起始点放到Drawable的中心位置上,长度暂定为一个杯半径的距离。
搅拌棒定位好了之后,接着还要把手安上去,这一步很简单,只需要更新一下oCenterX
和oCenterY
(O形状手指的中心点坐标)就行了,因为刚刚在画手的时候,O形状手指、K形状手指、手臂都是基于这两个局部变量来定位的:
修改以下三个方法:
private fun updateOFingerRect() {val oCenterX = stickEndPoint.x + fingerORadiusval oCenterY = stickEndPoint.y............//向左偏移半个手指宽度的距离 val halfFingerWidth = fingerWidth / 2left -= halfFingerWidthright -= halfFingerWidthoFingerRect.set(left, top, right, bottom)}private fun updateKFingersPath() {val oCenterY = stickEndPoint.y...... ...... } private fun updateArmPath() {val oCenterY = stickEndPoint.y...... ...... }
我们分别把手的各个元素(O手指、K手指、手臂)的基准点都进行了重新定位:由原来的Drawable中心点([width / 2F, height / 2F]
)改成了搅拌棒的结束点[stickEndPoint.x, stickEndPoint.y]
。
在updateOFingerRect
方法的最后,还将矩形向左偏移了半个手指宽度的距离,好让搅拌棒的结束点在两手指的中间处。
好,现在把搅拌棒画上:
override fun draw(canvas: Canvas) {//更新搅拌棒坐标点updateStickLocation()//画搅拌棒drawStick(canvas)drawHand(canvas)}private fun drawStick(canvas: Canvas) {paint.color = Color.WHITEpaint.style = Paint.Style.STROKEpaint.strokeWidth = coffeeStickWidthcanvas.drawLine(stickStartPoint.x, stickStartPoint.y, stickEndPoint.x, stickEndPoint.y, paint)}
看看效果:
emmm,现在看上去两只手指都没有碰到搅拌棒,是因为在画O形状手指时,那个预留的开口角度写死为30度了,这是不对的,正确的做法应该是要根据搅拌棒宽度来动态计算。
那应该怎么计算呢?
来看看这张图:
这就很容易看出,这个开口角度可以借助反三角函数来得到。
现在已知的条件,是对边和斜边,所以要用asin来计算:
修改一下updateOFingersPath
方法:
private fun updateOFingersPath() {//对边val opposite = coffeeStickWidth / 2 + fingerWidth / 2//斜边val hypotenuse = fingerORadius.toDouble()//预留开口角度 = asin(对边 / 斜边)val reservedAngle = Math.toDegrees(asin(opposite / hypotenuse)).toFloat()............}
这样就行了,现在的预留开口角度(reservedAngle
)会根据搅拌棒的宽度来动态计算,当它变大时,这个角度也会跟着变大。
好,现在来把咖啡杯和搅拌棒连接起来,看看要怎么连:
可以看到,搅拌棒的端点现在是根据那两个黄色的圆来定位的,所以在确定好两个圆的圆心坐标
和半径
之后,就能借助cos
和sin
来根据旋转角度动态计算出端点的坐标值了。
还可以看出,左边大圆的圆心
和咖啡杯的圆心
位置是一样的,也就是Drawable的中心点了。右边的小圆,它的圆心坐标就是大圆圆心
向右偏移一个咖啡杯半径
的距离。
大圆的半径其实就是咖啡杯半径
的1/2,小圆是1/3。
好,有了这些数据之后,我们再来修改一下updateStickLocation
方法:
private fun updateStickLocation() {//大圆半径val startRadius = cupRadius / 2//小圆半径val endRadius = cupRadius / 3//根据半径和旋转角度得到起始点的原始坐标值stickStartPoint.set(getPointByAngle(startRadius, stickAngle))//偏移到大圆的圆心坐标上stickStartPoint.offset(centerX, centerY)//根据半径和旋转角度得到结束点的原始坐标值stickEndPoint.set(getPointByAngle(endRadius, stickAngle))//偏移到小圆的圆心坐标上stickEndPoint.offset(centerX + cupRadius, centerY)}
就按刚刚说的那样做,先是根据半径(startRadius
, endRadius
)和旋转角度stickAngle
(现在是0)得到坐标值,然后偏移到目标圆的圆心坐标上。
可以看到里面是通过一个getPointByAngle
方法来计算坐标的,在上一篇的射箭动画中也用到了这个方法。
来看看它是怎样的:
private val tempPoint = PointF()private fun getPointByAngle(radius: Float, angle: Float): PointF {//先把角度转成弧度val radian = angle * Math.PI / 180//x轴坐标值val x = (radius * cos(radian)).toFloat()//y轴坐标值val y = (radius * sin(radian)).toFloat()tempPoint.set(x, y)return tempPoint}
好,看看现在的效果:
OKOK。
因为刚刚我们已经把手的各个元素改成以搅拌棒的结束点(stickEndPoint
)为基准了,所以现在更新搅拌棒的坐标之后,手的坐标也会跟着变。
搅拌咖啡
现在想要让它动起来太简单了,只需要不断更新搅拌棒坐标所依赖的stickAngle
就行:
//旋转一圈的时长private var stirringDuration = 1000L//开始时间private var stirringStartTime = 0Fprivate fun updateStickAngle() {if (stirringStartTime > 0) {val playTime = SystemClock.uptimeMillis() - stirringStartTime//得到当前进度var percent = playTime / stirringDurationif (percent >= 1F) {percent = 1F//转完一圈,重新开始stirringStartTime = SystemClock.uptimeMillis().toFloat()}//逆时针旋转所以是负数stickAngle = percent * -360F}}
还是跟上一篇一样的思路:记录起始时间
和时长
,然后计算出当前进度
,再用当前进度
* 总距离
,现在的距离就是-360,也就是每一次播放动画都逆时针旋转一圈。
可以看到,里面还判断了当前进度
是否>=1,如果是的话,证明本次动画已经播放完成,准备下一次动画的播放。
在开始动画前,我们还应该先定义两个状态,好让Drawable能根据不同的状态做出不同的行为:
private var state = 0companion object {//普通状态const val STATE_NORMAL = 0//搅拌中const val STATE_STIRRING = 1}
好,现在在draw
方法的最后,加上状态判断,并在里面调用刚刚的updateStickAngle
方法:
override fun draw(canvas: Canvas) {............if (state == STATE_STIRRING) {updateStickAngle()invalidateSelf()}}
就差一个start
方法来启动动画了:
fun start() {if (state != STATE_STIRRING) {//更新状态state = STATE_STIRRING//重置角度stickAngle = 0F//标记开始时间stirringStartTime = SystemClock.uptimeMillis().toFloat()//通知重绘invalidateSelf()}}
看看效果:
emmm,动是动起来了,但看着好像很僵硬,因为现在手的各个元素的运动轨迹都是一样的。
我们可以在不同的元素上分别制造一些偏移,好让它们看上去更有活力一点,比如:
- O形状手指所对应的矩形,在每次水平偏移时,它的
right
可以只偏移一半,left
则正常偏移,这样的话,O形状手指就会随着矩形一起被拉伸,形成一个手指伸缩的效果; - K形手势的两只手指,在更新位置时还可以把搅拌棒起始点所对应的圆的
y
轴偏移量(正弦波)拿过来,应用到x
轴上;
好,就按着这个思路修改一下:
首先是updateOFingerRect
方法:
private fun updateOFingerRect() {............//如果是搅拌状态,则取搅拌棒x轴偏移量的一半val rightOffset = if (state == STATE_STIRRING) {(stickEndPoint.x - centerX - cupRadius) / 2} else {halfFingerWidth}//将原来的halfFingerWidth换成rightOffsetright -= rightOffset......}
接着是updateKFingersPath
方法:
private fun updateKFingersPath() {......val finger1Offset = stickStartPoint.y - centerYval finger2Offset = finger1Offset / 2kFingersPath.reset()//第一根手指kFingersPath.moveTo(oFingerRect.right, oCenterY)kFingersPath.rLineTo(-finger1Offset - fingerWidth, -finger1Length)//第二根手指kFingersPath.moveTo(oFingerRect.right, oCenterY)kFingersPath.rLineTo(-finger2Offset, -finger2Length)}
新增的finger1Offset
和finger2Offset
,分别是K手势两只手指的结束点要偏移的距离,finger1Offset
取搅拌棒起始点的y
轴偏移量,而finger2Offset
则取finger1Offset
的一半,使得两根手指各有不同的摆动速度和幅度。
在lineTo
时,两根手指的x
轴都分别减去了对应的偏移量,这样就能随着搅拌棒端点的旋转而摆动起来了。
运行一下看看效果:
不错不错。
水涟漪
在文章开头的预览图中可以看到,在搅拌的时候会有一个涟漪效果,这个效果是怎么做的呢?
其实也就是一个圆弧,我们可以用Path来实现。不过这个圆弧在搅拌动画刚开始时是慢慢延长而不是突然出现的,所以要动态去更新Path。
细心的同学还会发现,这条涟漪是头大尾细的,还有不透明度也是从头到尾逐渐变小(越来越透明)。
但因为现在没有API可以直接画这样的线条,所以我们还需要先把画好圆弧的Path分解成坐标数组,来给圆弧上的每一个点设置不同的透明度,还有借助上一篇的那个缩放辅助类ScaleHelper来实现头大尾细的效果。
好,先来把Path搞定:
//涟漪是否完全展开private var rippleFulled = falseprivate fun updateRipplePath() {val halfSize = cupRadius / 2val left = centerX - halfSizeval right = centerX + halfSizeval top = centerY - halfSizeval bottom = centerY + halfSizevar sweepAngle: Floatif (rippleFulled) {sweepAngle = 180F} else {//因为现在的stickAngle为负数(逆时针),所以要取负数//涟漪拉伸的速度是搅拌速度的一半,所以要/2sweepAngle = -stickAngle / 2if (sweepAngle >= 180) {sweepAngle = 180F//标记已满rippleFulled = true}}ripplePath.reset()ripplePath.addArc(left, top, right, bottom, stickAngle, sweepAngle)}
圆弧扫过的最大角度,我们指定为180度,也就是半圆了。
接着还要用搅拌棒的旋转角度stickAngle
来作为圆弧的起点,结束点取旋转角度的一半,也就是当搅拌棒刚好旋转了一圈时,这条圆弧也刚好完全伸展开,完全伸展开之后,就保持这个长度继续跟着搅拌棒转圈了。
Path准备好之后,看看要怎么把它画出来:
private val scaleHelper = ScaleHelper(1F, 0F, .2F, 1F)private fun drawRipple(canvas: Canvas) {paint.style = Paint.Style.FILLpaint.color = stickColor//以最小缩放时的直径为精确度(确保在最小圆点之间也不会有空隙)val precision = (coffeeStickWidth * .2F).toInt()val points = decomposePath(ripplePath, precision)//一半的透明度=128,但因为精度是coffeeStickWidth的1/5(0.2),//也就是Path上一段长度为coffeeStickWidth的路径范围内最多会有5个点//也就是会有5个半透明的点在叠加,为了保持这个透明度不变,还要用128 * 2 或 / 5val baseAlpha = 128F * .2Fval length = points.sizevar i = 0while (i < length) {//当前遍历的进度val fraction = i.toFloat() / length//小点的半径(因为是半径,所以要/2)val radius = coffeeStickWidth * scaleHelper.getScale(fraction) / 2//设置透明度paint.alpha = (baseAlpha * (1 - fraction)).toInt()//画点canvas.drawCircle(points[i], points[i + 1], radius, paint)//坐标点数组格式为【x,y,x,y,....】,所以每次+2i += 2}}
可以看到在开头就创建了一个ScaleHelper对象的实例,里面传的四个参数的意思是:在线条的0%
处缩放100%
,100%
处缩放到20%
。也就是从大到小了,小到原尺寸的20%。
接着调用decomposePath
方法把Path分解成坐标点数组,然后遍历这个数组,并在里面画圆点,画圆点之前还给paint
设置了透明度,这个透明度是根据当前遍历的进度来计算的。
那个本来是半透明的baseAlpha
,为什么要 * 0.2 呢?
因为现在的圆弧是一个一个圆点堆出来的,如果有透明度的话,那么圆点和圆点之间重叠的部分,它的透明度就会累加,这样画出来的线条,就不是半透明了。
为了避免这种情况,我们事先计算出一个正常大小的圆点范围内最多能有几个圆点存在(取决于分解Path时的精度),然后把透明度调整为:即使多个圆点重叠,基准透明度也能够保持半透明(128)。
嗯,那个decomposePath
方法,也是从上一篇中拿过来的:
private fun decomposePath(path: Path, precision: Int): FloatArray {if (path.isEmpty) {return FloatArray(0)}val pathMeasure = PathMeasure(path, false)val pathLength = pathMeasure.lengthval numPoints = (pathLength / precision).toInt() + 1val points = FloatArray(numPoints * 2)val position = FloatArray(2)var index = 0var distance: Floatfor (i in 0 until numPoints) {distance = i * pathLength / (numPoints - 1)pathMeasure.getPosTan(distance, position, null)points[index] = position[0]points[index + 1] = position[1]index += 2}return points}
好,现在在draw
方法中的updateStickLocation
方法调用之前,加上刚刚的updateRipplePath
和drawRipple
方法:
override fun draw(canvas: Canvas) {......updateRipplePath()drawRipple(canvas)updateStickLocation()......}
看看最终的效果(为了能看清涟漪效果特意加大了尺寸):
太棒了!
其实还有个边界渐变透明的动画和手的进出场动画,不过这两个动画都很简单的,就留给同学们自己去实现啦。
说一下思路:
- 渐变透明: 在画完杯之后,
setShader
之前不断更新paint
的alpha
就行了; - 进出场:利用刚刚的
decomposePath
方法把一条路径事先分解成坐标点数组,然后把这些坐标点应用到搅拌棒的两端点上就行了(手也会跟随搅拌棒的坐标变更而变更的);
好了,本篇文章到此结束,有错误的地方请指出,谢谢大家!
Github地址:https://github.com/wuyr/CoffeeDrawable 欢迎Star
Android自定义Drawable第十五式之啡常OK相关推荐
- Android自定义Drawable第十四式之百步穿杨
前言 Emmmm,看标题大概就能猜到,这次我们要做的是一个射箭的效果. 在这篇文章中,同学们可以学到: 在自定义Drawable里流畅地draw各种动画: 画一条粗细不一的线段: 一个炫酷的射箭效果: ...
- Android自定义ViewGroup第十二式之年年有鱼
前言 先来看两张效果图: 哈哈,就是这样了. 前段时间在鸿神的群里看到有群友截了一张QQ空间的图,问它那个是怎么实现的: 在好友动态的列表中多了个Header,这个Header有一叠卡片的效果,上面的 ...
- Android自定义Behavior第十六式之空中楼阁
前言 & 初步分析 上个月鸿神在群里推荐一位同学的Flutter版WanAndroid项目的时候发现了一个炫酷的效果: 嗯,就是一个下拉进入二楼的效果,但因为这个项目是用Flutter做的,无 ...
- Android 自定义UI 实战 02 流式布局
Android 自定义UI 实战 02 流式布局-- 自定义ViewGroup 第二章 自定义ViewGroup 流式布局 文章目录 Android 自定义UI 实战 02 流式布局-- 自定义Vie ...
- Android自定义View之实现流式布局
Android自定义View之实现流式布局 运行效果 流式布局 把子控件从左到右摆放,如果一行放不下,自动放到下一行 自定义布局流程 1. 自定义属性:声明,设置,解析获取自定义值 在attr.xml ...
- 一个项目玩转 Android 自定义 Drawable。
DreamDrawable 项目地址:yanbober/DreamDrawable 简介: 一个项目玩转 Android 自定义 Drawable. 更多:作者 提 Bug 标签: 一个项目玩转 ...
- Android UI开发第二十五篇——分享一篇自定义的 Action Bar
Action Bar是android3.0以后才引入的,主要是替代3.0以前的menu和tittle bar.在3.0之前是不能使用Action Bar功能的.这里引入了自定义的Action Bar, ...
- Android 天气APP(十五)增加城市搜索、历史搜索记录
上一篇:Android 天气APP(十四)修复UI显示异常.优化业务代码逻辑.增加详情天气显示 添加城市 新版------------------- 一.推荐城市数据 二.推荐城市item布局和适配器 ...
- android自定义Drawable实现炫酷UI-锦鲤游泳效果
一.实现效果: 当点击屏幕的时候,屏幕中的锦鲤会身体摆动并且游到屏幕点击处,如下图: 效果分析: 1.小鱼的身体各个部件都是简单的半透明几何图形. 2.各个部件都可以活动. 3.从头到尾方向的部件摆动 ...
最新文章
- web实现QQ第三方登录 开放平台-web实现QQ第三方登录
- Eclipse RCP 中将窗口始终保持在最前
- 跨域(CORS)请求问题[No 'Access-Control-Allow-Origin' header is present on the requested resource]常见解决方案
- LiveVideoStackCon 2020 漫游指南
- iOS面试总结(待完善)
- 软考初级——操作系统
- 数仓安全:用Alter default privilege解决共享schema权限
- 3位黑洞发现者获2020年诺贝尔物理学奖
- Python开发过程中错误解决记录【持续更新记录,欢迎交流】
- 计算机电子书 2016 BiliDrive 备份
- 黑盒测试VS白盒测试
- 计算机弹奏简谱成都,赵雷《成都》简谱,分享给大家
- 解析DNA甲基化临床科研 | 无论什么科室,一定要有project的经典视角|易基因
- html左右滑轮标签,css样式支持左右滑动要点
- Lory的编程之旅就此启动
- 同时查询京东多个快递物流,并分析中转延误
- 你画我猜 计算机题目,你画我猜:你知道这些题目的答案是什么吗?
- PMP官方教材(PMBOK第五版中文电子版)
- 电子计算机主机房国标,根据国标GB50174-93《电子计算机机房设计规范》.ppt
- Windows下编译FFmpeg 32位和64位DLL