查看原文
其他

这交互炸了 第十五式 之 啡常OK

陈小缘 鸿洋 2020-10-29

本文作者


作者:陈小缘

链接:

https://blog.csdn.net/u011387817/article/details/100530256

本文由作者授权发布。


小缘出品,必是精品。收藏吧,短时间你肯定看不完咯,当然看完一定有收获。


1前言


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


这交互炸了系列  第十四式 之 百步穿扬


先看看效果图:




哈哈,小手是不是很可爱?O不OK?。


这个动画看上去挺难,但实际上还没有上一篇的射箭动画复杂。我们等下还会用上一些技巧,来简化画各个元素的步骤。


2初步分析


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



和上一篇的方式一样:先把各个组成部分拆开。


那这个杯子就可以拆分成:


杯身、手柄、杯底、手柄底、还有咖啡


  • 咖啡的话,我们能很直观的看出来,就是一个咖啡色的实心圆形,再加上边缘的【透明~白色】放射渐变;

  • 杯身其实也是一个圆形,只是它的直径比咖啡要大一点;

  • 手柄看上去是一个旋转了45°的圆角矩形;

  • 杯底手柄底,其实也就是偏移一下位置,改一下颜色,重新画杯身和手柄罢了;


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


现在来看看手要怎么画:



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


  • 两只竖起来像K型的手指,看着是两个圆角矩形;

  • 拇指和食指组成的O型手势,可以用一个圆弧来做;

  • 保持垂直的手臂,其实就是一个矩形;


3画手指技巧


如果手指和刚刚的手柄用圆角矩形来画的话,就会很麻烦,因为除了要计算[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(100F100F800F800F, mPaint)


emmm,没错了,等下画手柄和手指,都可以用这个方法来做,这样就方便了很多。



4创建Drawable


像上次那样,先创建一个类继承自Drawable,然后把最基本的几个方法重写(因为我们这次要做的是搅拌咖啡的效果,名字就叫CoffeeDrawable了):



class CoffeeDrawable(private var width: Intprivate var height: Int) : Drawable() {

    private var paint = Paint()

    init {
        initPaint()
        updateSize(width, height)
    }

    private fun initPaint() = paint.run {
        isAntiAlias = true
        strokeCap = Paint.Cap.ROUND
        strokeJoin = Paint.Join.ROUND
    }

    fun updateSize(width: Int, height: Int) {
        this.width = width
        this.height = height
    }

    override fun draw(canvas: Canvas) {

    }

    override fun getIntrinsicWidth() = width

    override fun getIntrinsicHeight() = height

    override fun getOpacity() = PixelFormat.TRANSLUCENT

    override fun setAlpha(alpha: Int) {
        paint.alpha = alpha
    }

    override fun setColorFilter(colorFilter: ColorFilter?) {
        paint.colorFilter = colorFilter
    }
}


5画杯


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


  • 中心点坐标: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.WHITE
    paint.style = Paint.Style.FILL

    //画杯身
    canvas.drawCircle(centerX, centerY, cupRadius, paint)

    //画手柄
    paint.style = Paint.Style.STROKE
    canvas.drawLine(centerX, centerY, centerX + cupRadius, centerY + cupRadius, paint)

    //咖啡颜色
    paint.color = -0x81A4C2
    paint.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(.7F1F)
        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 = coffeeShader
    canvas.drawCircle(centerX, centerY, coffeeRadius, paint)
    paint.shader = null
}


好,来看看现在的效果:




OK啦。


6画手


跟着前面的思路: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 / 2F
    val oCenterY = height / 2F
    //根据o手指的半径来计算出矩形的边界
    val left = oCenterX - fingerORadius
    val top = oCenterY - fingerORadius
    val right = left + fingerORadius * 2
    val 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 * 2

    oFingersPath.reset()
    oFingersPath.addArc(oFingerRect, startAngle, sweepAngle)
}


预留的开口角度现在写死为30度,等下我们会根据搅拌棒的宽度来动态计算这个值。


接下来到K手势了:


private fun updateKFingersPath() {
    //o手指的中心点坐标
    val oCenterY = height / 2F

    kFingersPath.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 / 2F
    val halfFingerWidth = fingerWidth / 2

    val left = oFingerRect.right - armWidth + halfFingerWidth
    val top = oCenterY
    val 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.FILL
    paint.color = -0x16386c

    canvas.drawPath(armPath, paint)
}

private fun drawOKFingers(canvas: Canvas) {
    paint.style = Paint.Style.STROKE
    paint.strokeWidth = fingerWidth

    canvas.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)
}


很简单,就在方法的最后加上这句就行了。


看看现在的效果:



棒~

7画搅拌棒


现在手和杯都已经画出来了,接下来我们要借助一样东西把它们连接在一起,这个东西就是搅拌棒。


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



跟前面的思路一样,搅拌棒同样也可以用一条线来实现。


先把搅拌棒和手连在一起:


可以看到,这条线右边的端点,是在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 + fingerORadius
      val oCenterY = stickEndPoint.y

      ......
      ......

      //向左偏移半个手指宽度的距离 
      val halfFingerWidth = fingerWidth / 2
      left -= halfFingerWidth
      right -= halfFingerWidth
      oFingerRect.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.WHITE
    paint.style = Paint.Style.STROKE
    paint.strokeWidth = coffeeStickWidth

    canvas.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)为基准了,所以现在更新搅拌棒的坐标之后,手的坐标也会跟着变。


8搅拌咖啡


现在想要让它动起来太简单了,只需要不断更新搅拌棒坐标所依赖的stickAngle就行:


//旋转一圈的时长
private var stirringDuration = 1000L
//开始时间
private var stirringStartTime = 0F

private fun updateStickAngle() {
    if (stirringStartTime > 0) {
        val playTime =  SystemClock.uptimeMillis() - stirringStartTime
        //得到当前进度
        var percent = playTime / stirringDuration
        if (percent >= 1F) 
{
            percent = 1F
            //转完一圈,重新开始
            stirringStartTime = SystemClock.uptimeMillis().toFloat()
        }
        //逆时针旋转所以是负数
        stickAngle = percent * -360F
    }
}


还是跟上一篇一样的思路:记录起始时间和时长,然后计算出当前进度,再用当前进度 * 总距离,现在的距离就是-360,也就是每一次播放动画都逆时针旋转一圈。


可以看到,里面还判断了当前进度是否>=1,如果是的话,证明本次动画已经播放完成,准备下一次动画的播放。


在开始动画前,我们还应该先定义两个状态,好让Drawable能根据不同的状态做出不同的行为:


private var state = 0

companion 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换成rightOffset
    right -= rightOffset

    ......
}


接着是updateKFingersPath方法:


private fun updateKFingersPath() {

    ......

    val finger1Offset = stickStartPoint.y - centerY
    val finger2Offset = finger1Offset / 2

    kFingersPath.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轴都分别减去了对应的偏移量,这样就能随着搅拌棒端点的旋转而摆动起来了。


运行一下看看效果:



不错不错。


9水涟漪


在文章开头的预览图中可以看到,在搅拌的时候会有一个涟漪效果,这个效果是怎么做的呢?

其实也就是一个圆弧,我们可以用Path来实现。不过这个圆弧在搅拌动画刚开始时是慢慢延长而不是突然出现的,所以要动态去更新Path。


细心的同学还会发现,这条涟漪是头大尾细的,还有不透明度也是从头到尾逐渐变小(越来越透明)。


但因为现在没有API可以直接画这样的线条,所以我们还需要先把画好圆弧的Path分解成坐标数组,来给圆弧上的每一个点设置不同的透明度,还有借助上一篇的那个缩放辅助类ScaleHelper来实现头大尾细的效果。


好,先来把Path搞定:


//涟漪是否完全展开
private var rippleFulled = false

private fun updateRipplePath() 
{
    val halfSize = cupRadius / 2
    val left = centerX - halfSize
    val right = centerX + halfSize
    val top = centerY - halfSize
    val bottom = centerY + halfSize

    var sweepAngle: Float
    if (rippleFulled) 
{
        sweepAngle = 180F
    } else {
        //因为现在的stickAngle为负数(逆时针),所以要取负数
        //涟漪拉伸的速度是搅拌速度的一半,所以要/2
        sweepAngle = -stickAngle / 2
        if (sweepAngle >= 180) {
            sweepAngle = 180F
            //标记已满
            rippleFulled = true
        }
    }
    ripplePath.reset()
    ripplePath.addArc(left, top, right, bottom, stickAngle, sweepAngle)
}


圆弧扫过的最大角度,我们指定为180度,也就是半圆了。


接着还要用搅拌棒的旋转角度stickAngle来作为圆弧的起点,结束点取旋转角度的一半,也就是当搅拌棒刚好旋转了一圈时,这条圆弧也刚好完全伸展开,完全伸展开之后,就保持这个长度继续跟着搅拌棒转圈了。


Path准备好之后,看看要怎么把它画出来:


private val scaleHelper = ScaleHelper(1F0F, .2F1F)

private fun drawRipple(canvas: Canvas) {
    paint.style = Paint.Style.FILL
    paint.color = stickColor

    //以最小缩放时的直径为精确度(确保在最小圆点之间也不会有空隙)
    val precision = (coffeeStickWidth * .2F).toInt()
    val points = decomposePath(ripplePath, precision)
    //一半的透明度=128,但因为精度是coffeeStickWidth的1/5(0.2),
    //也就是Path上一段长度为coffeeStickWidth的路径范围内最多会有5个点
    //也就是会有5个半透明的点在叠加,为了保持这个透明度不变,还要用128 * 2 或 / 5
    val baseAlpha = 128F * .2F
    val length = points.size
    var i = 0
    while (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,....】,所以每次+2
        i += 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.length

    val numPoints = (pathLength / precision).toInt() + 1
    val points = FloatArray(numPoints * 2)
    val position = FloatArray(2)
    var index = 0
    var distance: Float
    for (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()
    ......
}


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




太棒了!


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


说一下思路:


  1. 渐变透明:在画完杯之后,setShader之前不断更新paint的alpha就行了;

  2. 进出场:利用刚刚的decomposePath方法把一条路径事先分解成坐标点数组,然后把这些坐标点应用到搅拌棒的两端点上就行了(手也会跟随搅拌棒的坐标变更而变更的);


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


Github地址:

https://github.com/wuyr/CoffeeDrawable 


欢迎Star


推荐阅读

走心推荐几个必备的插件
我爱 Android
不破不立!


扫一扫 关注我的公众号

如果你想要跟大家分享你的文章,欢迎投稿~


┏(^0^)┛明天见!

    您可能也对以下帖子感兴趣

    文章有问题?点此查看未经处理的缓存