Android自定義View之星球運動

gengzhibo發表於2018-02-24

Android自定義View之星球運動

歡迎大家訪問我的個人部落格

在dribbble閒逛的時候發現的一個有意思的星球運動的動畫,剛好最近時間尚可,就簡單實現了一下中間運動的部分,又是因為時間的原因,開頭位移的部分沒有完成.

dribbble中發現的動畫
這是在dribbble中發現的動畫

自己實現的動畫
這是我自己實現的效果... 總覺得我這個星球有點胖... 因為胖所以轉的慢麼這是.速度等細節還有優化的餘地

設計過程

老辦法,先分解動畫的構成.整個動畫可以看做是一個自旋的星球從右上角由小變大的移動到螢幕的中央的.

星球的位移及縮放不說(其實是最近有需求,暫時沒時間完善),主要完善了星球的旋轉及尾部的處理.

最底層是背景的星星閃爍,每次在星球一定範圍內隨機出現,並縮放就好

最開始設計尾部效果的時候,是在沒列中設計了兩端線.再不斷的執行及移動.但是實現起來很亂.最後採用了先繪製所有尾部展示的內容,然後在用和背景一樣的顏色部分遮蓋並移動此部分形成視覺上的效果的方法.(也可以設定PorterDuff模式來展示).設計過程中的效果如下

尾部效果1

尾部效果2

星球的設計,星球的本身使用簡單的遮蓋和貝塞爾曲線就能完成一個較為滿意的星球背景.

星球背景

重點是星球地表的設計以及星球自轉下的地表樣式的移動.解決的方法是是先繪製三個重複並連續的地表樣式,通過移動整個地表樣式模擬星球的轉動.最後通過PorterDuff來控制展示的部分和星球的位置重合.

未開啟PorterDuff模式時繪製的樣式如下:

地貌的設定

開啟PorterDuff模式後再指定位置展示指定形狀的圖形如下:

開啟PorterDuff模式

最後再移動設定好的星球地貌就可以模擬出星球轉動的效果了

程式碼實現

背景的星星

private fun drawStarts(canvas: Canvas, perIndexInAll: Float) {
    //背景的星星在星球附近的一定範圍內隨機出現
    val maxRand = 800

    canvas.translate(-maxRand / 2F , -maxRand / 2F)
    val Random = Random(perIndexInAll.toInt().toLong())

    //繪製背景的星星
    for (index in 0..4){
        drawStart(canvas ,  Random.nextFloat() * maxRand , Random.nextFloat() * maxRand , perIndex)
    }

    canvas.translate(maxRand / 2F , maxRand / 2F)
}

//繪製背景的星星內容
//繪製背景的星星內容
private fun drawStart(canvas: Canvas, x: Float, y: Float, per: Float) {
    var per = per
    //這個部分是為了讓星星實現從小到大後再從大到小的變動
    if (per >= 1.0F){
        per -= 1F
    }
    if (per <= 0.5F){
        per *= 2
    }else{
        per = (1 - per) * 2
    }

    canvas.save()
    canvas.translate(x , y)

    canvas.scale(per , per)

    val paint = Paint()
    paint.color = 0xff78D8DF.toInt()

    val startLength = 30F
    val startOffset = startLength / 3F

    //通過路徑描繪星星的形狀
    val path = Path()
    path.moveTo(0F , startLength)
    path.lineTo(startOffset , startOffset )
    path.lineTo(startLength , 0F)
    path.lineTo(startOffset  , -startOffset )
    path.lineTo(0F , -startLength)
    path.lineTo(-startOffset  , -startOffset )
    path.lineTo(-startLength , 0F)
    path.lineTo(-startOffset  , startOffset )
    path.lineTo(0F , startLength)

    canvas.drawPath(path , paint)

    paint.color = viewBackgroundColor
    //通過縮小繪製星星內部形狀
    canvas.scale(0.3F , 0.3F)
    canvas.drawPath(path , paint)

    canvas.restore()
}
複製程式碼

星球外部

private fun drawGas(canvas: Canvas, index: Float) {
    canvas.save()
    canvas.rotate(45F)

    val gasWidth = 18F
    val baseR = baseR * 0.7F
    val absBaseR = baseR / 5F

    val paint = Paint()
    paint.strokeWidth = gasWidth
    paint.style = Paint.Style.STROKE
    paint.color = 0xff2F3768.toInt()

    val paintArc = Paint()
    paintArc.color = 0xff2F3768.toInt()

    val gasLength = baseR * 2F
    canvas.save()

    val gsaL = gasWidth / 2F * 3
    var maxGasLength = (gasLength + gsaL ) / 2
    var index = index

    canvas.scale(1F , -1F)

    //繪製星球后面的氣流情況
    //捨不得那麼多定義好的變數
    //又不想寫個引數很多的函式,就這麼實現了
    canvas.save()
    canvas.translate(baseR , baseR * 1.2F)
    canvas.translate(0F , absBaseR)
    //drawLines函式一個繪製兩頭帶半圓的線段
    drawLines(0F, maxGasLength, canvas, paint)
    drawWhite( maxGasLength * index, gasWidth , gsaL * 2 , canvas)
    drawWhite( maxGasLength * (index - 1 ) * 1.1F, gasWidth , gsaL * 2 , canvas)
    drawWhite( maxGasLength * (index + 1 ) * 1.1F, gasWidth , gsaL * 2 , canvas)
    canvas.restore()

    index = index + 0.3F

    //.....沒有寫函式就不上重複的程式碼了

    val rectf = RectF(-baseR , -baseR , baseR ,baseR)
    canvas.drawArc(rectf , 0F , 180F , false , paint)

    canvas.drawLine(baseR ,0F ,  baseR ,  -baseR, paint)
    canvas.drawLine(-baseR ,0F ,  -baseR ,  -baseR, paint)

    canvas.restore()
}

//繪製尾部空白部分
private fun drawWhite(offset: Float, gasWidth: Float, gsaL : Float , canvas: Canvas) {
    val r = gasWidth / 2F

    canvas.save()
    canvas.translate( 0F , offset - 2 * gsaL )

    val pointPaint = Paint()
    pointPaint.strokeWidth = 20F
    pointPaint.color = Color.RED

    //通過貝塞爾曲線繪製半圓效果
    val path = Path()
    path.moveTo(-r , gsaL)
    path.cubicTo(
            - r * C ,  gsaL - r,
            r * C ,  gsaL - r,
            r , gsaL
    )

    path.lineTo(r , - gsaL)
    path.cubicTo(
            r * C ,  - gsaL + r,
            -r * C ,  - gsaL + r,
            -r , - gsaL
    )

    path.lineTo(-r , gsaL * 1.5F)

    val paint = Paint()
    paint.color = viewBackgroundColor
    canvas.drawPath(path , paint)

    canvas.restore()
}
複製程式碼

星球

private fun drawPlanet(canvas: Canvas , index : Float) {
    //設定原圖層
    val srcB = makeSrc(index)
    //設定遮罩層
    //遮罩層只有一和星球大小一樣的圓
    val dstB = makeDst(index)

    val paint = Paint()
    canvas.saveLayer(-baseR, -baseR, baseR , baseR, null, Canvas.ALL_SAVE_FLAG)
    //繪製遮罩層
    canvas.drawBitmap(dstB,  -baseR / 2F, -baseR / 2F , paint)
    //設定遮罩模式為SRC_IN顯示原圖層中原圖層與遮罩層相交部分
    paint.xfermode = PorterDuffXfermode(PorterDuff.Mode.SRC_IN)
    canvas.drawBitmap(srcB, width / -2F, height / -2F , paint)
    paint.xfermode = null
}


//設定源圖層
fun makeSrc(index :Float): Bitmap {
    val bm = Bitmap.createBitmap(width, height, Bitmap.Config.ARGB_8888)
    val canvas = Canvas(bm)
    canvas.translate(width.toFloat() / 2F , height.toFloat() / 2F)

    val paint = Paint()
    paint.color = 0xff57BEC6.toInt()
    paint.style = Paint.Style.FILL

    val rectf = RectF(-baseR / 2F, -baseR / 2F, baseR / 2F, baseR / 2F)
    canvas.drawArc(rectf , 0F , 360F , true , paint)

    canvas.save()


    //繪製星球背景
    paint.color = 0xff78D7DE.toInt()
    var baseR = baseR * 0.9.toFloat()
    val rectf2 = RectF(-baseR / 2F, -baseR / 2F, baseR / 2F, baseR / 2F)
    canvas.translate(baseR / 6F , baseR / 6F)
    canvas.drawArc(rectf2 , 0F , 360F , true , paint)

    canvas.restore()
    canvas.rotate(-45F)
    canvas.save()

    val bottomBaseR = baseR / 0.9F / 2
    val path = Path()
    path.moveTo(-bottomBaseR , 0F)
    path.cubicTo(-bottomBaseR , bottomBaseR * 2, bottomBaseR  , bottomBaseR * 2, bottomBaseR , 0F)

    path.cubicTo(
            bottomBaseR * C,bottomBaseR ,
            -bottomBaseR * C,bottomBaseR ,
            -bottomBaseR , 0F
    )

    //繪製星球背景的陰影效果
    paint.color = 0xffAAEEF2.toInt()
    paint.style = Paint.Style.FILL
    canvas.drawPath(path , paint)

    //繪製星球的地貌
    drawPoints(index , canvas)

    canvas.restore()

    paint.strokeWidth = 30F
    paint.color = 0xff2F3768.toInt()
    paint.style = Paint.Style.STROKE
    canvas.drawArc(rectf , 0F , 360F , true , paint)

    return bm
}

private fun drawPoints(index: Float, canvas: Canvas) {
        val paintB = Paint()
        val paintS = Paint()
        paintS.style = Paint.Style.FILL
        paintS.color = 0xffE7F2FB.toInt()

        paintB.style = Paint.Style.FILL
        paintB.color = 0xff2F3768.toInt()

        val baseRB = baseR / 2F / 3
        val baseRS = baseR / 2F / 3 / 3

        val rectfB = RectF(-baseRB, -baseRB, baseRB, baseRB)
        val rectfS = RectF(-baseRS, -baseRS, baseRS, baseRS)

        val pointPaint = Paint()
        pointPaint.color = Color.BLACK
        pointPaint.strokeWidth = 50F

        val coverWidth = baseR

        //通過移動座標原點模擬星球的自轉效果
        canvas.translate(-coverWidth / 2F , coverWidth * 1.5F)

        val index = index
        canvas.translate(0F , coverWidth * index )

        //重複繪製三次星球的地貌使得星球的自轉無縫連線
        for (i in 0..2){
            canvas.save()
            canvas.translate(coverWidth / 3F / 2  , -coverWidth / 3F * 2)
            canvas.drawArc(rectfB , 0F , 360F , true , paintB)
            canvas.drawArc(rectfS , 0F , 360F , true , paintS)
            canvas.restore()

            canvas.save()
            canvas.translate(coverWidth / 3F *2 , -coverWidth / 3F)
            canvas.drawArc(rectfB , 0F , 360F , true , paintB)
            canvas.drawArc(rectfS , 0F , 360F , true , paintS)
            canvas.restore()

            canvas.save()
            canvas.translate(coverWidth / 3F *2 , -coverWidth / 8F * 7 + -coverWidth / 10F )
            canvas.drawArc(rectfS , 0F , 360F , true , paintB)
            canvas.restore()

            canvas.save()
            canvas.translate(coverWidth / 3F *2 , -coverWidth / 8F * 7  - -coverWidth / 10F )
            canvas.drawArc(rectfS , 0F , 360F , true , paintB)
            canvas.restore()

            canvas.translate(0F , -coverWidth)
        }
    }
複製程式碼

相關程式碼可以訪問我的GitHub來獲取,歡迎大家start或者提供建議.

相關文章