前言

在上一篇的自定义Drawable中,我们学习了如何在Canvasdraw一个射箭的动画,不过那个动画是以线条为主的,多看几眼可能就会觉得没味道了,那么在本篇文章,将和同学们一起做一个看起来更耐看,更丝滑的动画。
先看看效果图:

哈哈,小手是不是很可爱? O不OK?。
这个动画看上去挺难,但实际上还没有上一篇的射箭动画复杂。我们等下还会用上一些技巧,来简化画各个元素的步骤。

初步分析

先看看茄子同学画的这张图:

和上一篇的方式一样:先把各个组成部分拆开。
那这个杯子就可以拆分成:杯身手柄杯底手柄底、还有咖啡

  1. 咖啡的话,我们能很直观的看出来,就是一个咖啡色的实心圆形,再加上边缘的【透明~白色】放射渐变;
  2. 杯身其实也是一个圆形,只是它的直径比咖啡要大一点;
  3. 手柄看上去是一个旋转了45°的圆角矩形;
  4. 杯底和手柄底,其实也就是偏移一下位置,改一下颜色,重新画杯身手柄罢了;

不过我们在画的时候,顺序刚好和上面的顺序相反,因为咖啡的圆形是在最上面,而杯底和手柄底则在最底层。

现在来看看手要怎么画:

看上去好像挺难,先不管,来拆分一下吧:

  • 两只竖起来像K型的手指,看着是两个圆角矩形;
  • 拇指和食指组成的O型手势,可以用一个圆弧来做;
  • 保持垂直的手臂,其实就是一个矩形;

画手指技巧

如果手指和刚刚的手柄圆角矩形来画的话,就会很麻烦,因为除了要计算[l, t, r, b]之外,还要计算和处理旋转角度。

那应该用哪种方式呢?
熟悉Paint的同学会知道一个叫Cap的东西,它可以改变线条端点的样式,一共有三种,分别是:BUTTROUNDSQUARE。默认情况下是第一个,但因为现在我们要把线条的端点变成圆,也就是要用第二个了。

来测试一下:

 //设置端点样式为圆形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}
}

画杯

好,先来画静态的杯子。刚刚分析过,杯子大致就是圆形 + 粗线条(手柄) 的组合,那么在画的时候就需要先定义以下变量:

  • 中心点坐标:centerXcenterY(因为杯子是在Drawable的中心处);
  • 杯子半径cupRadius、咖啡半径coffeeRadius、手柄宽度cupHandleWidth
  • 最后一个,杯底的偏移量cupBottomOffset

为了能适应各种尺寸的Drawable容器,这些变量应该基于Drawable的widthheight来动态计算,而不是随便指定某个值。
这样的话,当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)}

stickStartPointstickEndPoint分别是搅拌棒起始点结束点的PointF对象实例。
我们暂时把搅拌棒的起始点放到Drawable的中心位置上,长度暂定为一个杯半径的距离。

搅拌棒定位好了之后,接着还要把安上去,这一步很简单,只需要更新一下oCenterXoCenterY(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)会根据搅拌棒的宽度来动态计算,当它变大时,这个角度也会跟着变大。

好,现在来把咖啡杯搅拌棒连接起来,看看要怎么连:

可以看到,搅拌棒的端点现在是根据那两个黄色的圆来定位的,所以在确定好两个圆的圆心坐标半径之后,就能借助cossin来根据旋转角度动态计算出端点的坐标值了。
还可以看出,左边大圆圆心咖啡杯圆心位置是一样的,也就是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,动是动起来了,但看着好像很僵硬,因为现在手的各个元素的运动轨迹都是一样的。
我们可以在不同的元素上分别制造一些偏移,好让它们看上去更有活力一点,比如:

  1. O形状手指所对应的矩形,在每次水平偏移时,它的right可以只偏移一半,left则正常偏移,这样的话,O形状手指就会随着矩形一起被拉伸,形成一个手指伸缩的效果;
  2. 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)}

新增的finger1Offsetfinger2Offset,分别是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方法调用之前,加上刚刚的updateRipplePathdrawRipple方法:

    override fun draw(canvas: Canvas) {......updateRipplePath()drawRipple(canvas)updateStickLocation()......}

看看最终的效果(为了能看清涟漪效果特意加大了尺寸):

太棒了!

其实还有个边界渐变透明的动画和手的进出场动画,不过这两个动画都很简单的,就留给同学们自己去实现啦。
说一下思路:

  • 渐变透明: 在画完杯之后,setShader之前不断更新paintalpha就行了;
  • 进出场:利用刚刚的decomposePath方法把一条路径事先分解成坐标点数组,然后把这些坐标点应用到搅拌棒的两端点上就行了(手也会跟随搅拌棒的坐标变更而变更的);

好了,本篇文章到此结束,有错误的地方请指出,谢谢大家!

Github地址:https://github.com/wuyr/CoffeeDrawable 欢迎Star

Android自定义Drawable第十五式之啡常OK相关推荐

  1. Android自定义Drawable第十四式之百步穿杨

    前言 Emmmm,看标题大概就能猜到,这次我们要做的是一个射箭的效果. 在这篇文章中,同学们可以学到: 在自定义Drawable里流畅地draw各种动画: 画一条粗细不一的线段: 一个炫酷的射箭效果: ...

  2. Android自定义ViewGroup第十二式之年年有鱼

    前言 先来看两张效果图: 哈哈,就是这样了. 前段时间在鸿神的群里看到有群友截了一张QQ空间的图,问它那个是怎么实现的: 在好友动态的列表中多了个Header,这个Header有一叠卡片的效果,上面的 ...

  3. Android自定义Behavior第十六式之空中楼阁

    前言 & 初步分析 上个月鸿神在群里推荐一位同学的Flutter版WanAndroid项目的时候发现了一个炫酷的效果: 嗯,就是一个下拉进入二楼的效果,但因为这个项目是用Flutter做的,无 ...

  4. Android 自定义UI 实战 02 流式布局

    Android 自定义UI 实战 02 流式布局-- 自定义ViewGroup 第二章 自定义ViewGroup 流式布局 文章目录 Android 自定义UI 实战 02 流式布局-- 自定义Vie ...

  5. Android自定义View之实现流式布局

    Android自定义View之实现流式布局 运行效果 流式布局 把子控件从左到右摆放,如果一行放不下,自动放到下一行 自定义布局流程 1. 自定义属性:声明,设置,解析获取自定义值 在attr.xml ...

  6. 一个项目玩转 Android 自定义 Drawable。

    DreamDrawable 项目地址:yanbober/DreamDrawable 简介: 一个项目玩转 Android 自定义 Drawable. 更多:作者   提 Bug 标签: 一个项目玩转 ...

  7. Android UI开发第二十五篇——分享一篇自定义的 Action Bar

    Action Bar是android3.0以后才引入的,主要是替代3.0以前的menu和tittle bar.在3.0之前是不能使用Action Bar功能的.这里引入了自定义的Action Bar, ...

  8. Android 天气APP(十五)增加城市搜索、历史搜索记录

    上一篇:Android 天气APP(十四)修复UI显示异常.优化业务代码逻辑.增加详情天气显示 添加城市 新版------------------- 一.推荐城市数据 二.推荐城市item布局和适配器 ...

  9. android自定义Drawable实现炫酷UI-锦鲤游泳效果

    一.实现效果: 当点击屏幕的时候,屏幕中的锦鲤会身体摆动并且游到屏幕点击处,如下图: 效果分析: 1.小鱼的身体各个部件都是简单的半透明几何图形. 2.各个部件都可以活动. 3.从头到尾方向的部件摆动 ...

最新文章

  1. web实现QQ第三方登录 开放平台-web实现QQ第三方登录
  2. Eclipse RCP 中将窗口始终保持在最前
  3. 跨域(CORS)请求问题[No 'Access-Control-Allow-Origin' header is present on the requested resource]常见解决方案
  4. LiveVideoStackCon 2020 漫游指南
  5. iOS面试总结(待完善)
  6. 软考初级——操作系统
  7. 数仓安全:用Alter default privilege解决共享schema权限
  8. 3位黑洞发现者获2020年诺贝尔物理学奖
  9. Python开发过程中错误解决记录【持续更新记录,欢迎交流】
  10. 计算机电子书 2016 BiliDrive 备份
  11. 黑盒测试VS白盒测试
  12. 计算机弹奏简谱成都,赵雷《成都》简谱,分享给大家
  13. 解析DNA甲基化临床科研 | 无论什么科室,一定要有project的经典视角|易基因
  14. html左右滑轮标签,css样式支持左右滑动要点
  15. Lory的编程之旅就此启动
  16. 同时查询京东多个快递物流,并分析中转延误
  17. 你画我猜 计算机题目,你画我猜:你知道这些题目的答案是什么吗?
  18. PMP官方教材(PMBOK第五版中文电子版)
  19. 电子计算机主机房国标,根据国标GB50174-93《电子计算机机房设计规范》.ppt
  20. Windows下编译FFmpeg 32位和64位DLL

热门文章

  1. word和excel测试软件,windows_OfficeHelper
  2. 在线直播源码中直播间内大转盘功能的实现
  3. 凯撒密码-CTF(Crypto)
  4. 创建一个MySQL数据库中的datetime类型
  5. 计算机技术论文搜索引擎,搜索引擎-毕设论文.doc
  6. 如何查看C++ 编译的DLL函数
  7. LeetCode-Python-883. 三维形体投影面积
  8. 数据结构|魔王语言解释
  9. 基于matlab的蓝色车牌识别(车牌倾斜矫正)
  10. 思维导图软件哪个好?MindNow思维导图