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

本文作者

作者:陳小緣

鏈接:

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(100F, 100F, 800F, 800F, mPaint)

emmm,沒錯了,等下畫手柄和手指,都可以用這個方法來做,這樣就方便了很多。

4創建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 = 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(.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 = 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 = 0Fprivate 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 = 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換成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 = falseprivate 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(1F, 0F, .2F, 1F)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^)┛明天見!

免責聲明:本文僅代表文章作者的個人觀點,與本站無關。其原創性、真實性以及文中陳述文字和內容未經本站證實,對本文以及其中全部或者部分內容文字的真實性、完整性和原創性本站不作任何保證或承諾,請讀者僅作參考,并自行核實相關內容。

http://image99.pinlue.com/thumb/img_jpg/MOu2ZNAwZwMvAQrrwJxWicib03odnTVMSIgAevE72u99yCfLqicQibs7WibmmfxODBpRRp3FuVHHTa00LtqVXo71yDg/0.jpeg
我要收藏
贊一個
踩一下
分享到
相關推薦
精選文章
?
京东快彩是真的吗