【難以想象的Canvas】天氣不可能那麼可愛

本篇文章參考自【難以想象的CSS】天氣不可能那麼可愛,使用Android中的自定義View實現相似的效果。java

前言

這段時間一直在研究自定義View,剛好看到使用CSS實現的天氣效果很不錯,遂嘗試使用自定義View實現一發。
不要臉的套用原做者的一句話,但願原做者不要揍我~git

只有你想不到,沒有自定義View實現不了的。今日分享由自定義View實現的效果 - Weathergithub

效果

今我來思,雨雪霏霏


晴空一鶴排雲上,便引詩情到碧霄。canvas


因爲不可抗力緣由(懶),這裏只實現了晴、雪兩種天氣效果。原做者文章裏實現了晴、雪、雲、雨、超級月亮?(原文Supermoon,本人水平有限,實在不知道怎麼翻譯),有興趣的讀者能夠自行實現。數組

源碼

兩種天氣效果的實現源碼均已上傳至Github - Weather,客官請自取,若是剛好遇上鐵汁您心情好,不妨點個starbash

分析

接下來按照慣例分析一波項目裏使用的重要API:markdown

BlurMaskFilter

首先咱們來看看源碼中的註釋是怎麼描述的:app

/**
 * This takes a mask, and blurs its edge by the specified radius. Whether or
 * or not to include the original mask, and whether the blur goes outside,
 * inside, or straddles, the original mask's border, is controlled by the * Blur enum. */ /* 翻譯成大白話的意思就是BlurMaskFilter能夠在本來的View上添加一層指定模糊半徑的蒙層,具體模糊的方式,由Blur枚舉類型控制 */ 複製代碼

這裏咱們用BlurMaskFilter實現陰影效果~dom

LinearGradient 線性漸變

Android系統裏的LinearGradient是paint的一種shader(着色器)方案。LinearGradient指的就是線性漸變:設置兩個點和兩種顏色,以這兩個點做爲端點,使用兩種顏色的漸變來繪製顏色,大概像下面這樣:
ide

輻射漸變(圖片源自扔物線Hencoder)


咱們這裏星球的顏色所有都是經過指定paint的shader爲LinearGradient實現的。

一塊兒畫

上面介紹了部分重要的API,接下來咱們來一步一步實現 Snowy 的效果

step1:繪製光暈

作一個黑色背景,由於黑色視覺反差大視覺效果槓槓的,這裏先畫一個圓使其位於Canvas畫布中心位置,再使用BlurMaskFilter做出陰影,達成光暈的效果:

// 光暈的paint
private val outPaint = Paint(Paint.ANTI_ALIAS_FLAG).apply {
        // 光暈的顏色
        color = Color.parseColor("#e6e8db")
        // 使用BlurMaskFilter製做陰影效果
        maskFilter = BlurMaskFilter(shadowRadius.toFloat(), BlurMaskFilter.Blur.SOLID)
    }
/** 如下代碼是在onDraw()方法中 */
canvas.drawColor(Color.BLACK)
canvas.drawCircle(centerX, centerY, outRadius.toFloat(), outPaint)
複製代碼

step2:畫一個圓

這裏使用LinearGradient作一個漸變的圓,並位於Canvas畫布中心位置,與step1中的光暈造成同心圓,這樣馬上就有一個不靈不靈的效果了~

private val innerCirclePaint = Paint(Paint.ANTI_ALIAS_FLAG).apply {
        shader = LinearGradient(centerX - innerRadius, centerY + innerRadius, centerX, centerY - innerRadius,
                Color.parseColor("#e0e2e5"), Color.parseColor("#758595"), Shader.TileMode.CLAMP)
}
/** 如下代碼是在onDraw()方法中 */
if (canvas != null){
// 繪製黑色背景     
canvas.drawColor(Color.BLACK)
// 繪製漸變圓
canvas.drawCircle(centerX, centerY, innerRadius.toFloat(), innerCirclePaint)
}
複製代碼

注意,當設置了paint的**shader**屬性後,paint的**color**屬性就會失效,也就是說,當設置了 Shader 以後,Paint 在繪製圖形和文字時就不使用 setColor/ARGB() 設置的顏色了,而是使用 Shader 的方案中的顏色。

step3:畫雪人的手臂

咱們這裏使用drawArc繪製一段圓弧:

// 確認雪人手臂位置的Rect
private val snowyManHandRect = RectF(centerX - dp2px(40f), centerY, centerX + dp2px(40f), snowyManBodyY - dp2px(8f))
// 雪人手臂的paint
private val snowyManHandPaint = Paint(Paint.ANTI_ALIAS_FLAG).apply {
        color = Color.BLACK
        style = Paint.Style.STROKE
        strokeWidth = dp2px(5f).toFloat()
        alpha = 120
    }
 /** 如下代碼是在onDraw()方法中 */
 if (canvas != null){
        canvas.drawColor(Color.BLACK)
        canvas.drawCircle(centerX, centerY, outRadius.toFloat(), outPaint)
        canvas.drawCircle(centerX, centerY, innerRadius.toFloat(), innerCirclePaint)
        canvas.drawArc(snowyManHandRect, 155f,-120f,false, snowyManHandPaint)
}
複製代碼

看到這裏有人會說,這是什麼鬼啊,哪裏像雪人的手臂啦,別急,咱們「走着瞧」

step4:畫雪人身體

雪人的身體是由兩個相切(感謝個人數學老師,我居然還記得這麼專業的數學名詞)的大小不一樣的圓組成:

private val snowyManHeaderRadius = dp2px(12f)
private val snowyManBodyRadius = dp2px(25f)
private val snowyManHeaderX = centerX
private val snowyManHeaderY = centerY + outRadius - snowyManHeaderRadius - snowyManBodyRadius * 2
    private val snowyManBodyX = centerX - dp2px(3f)
    private val snowyManBodyY = centerY + outRadius - snowyManBodyRadius - dp2px(5f)
    private val snowyManPaint = Paint(Paint.ANTI_ALIAS_FLAG).apply {
        color = Color.parseColor("#e6e8db")
    }
     /** 如下代碼是在onDraw()方法中 */
    canvas.drawCircle(snowyManHeaderX, snowyManHeaderY, snowyManHeaderRadius.toFloat(), snowyManPaint)
            canvas.drawCircle(snowyManBodyX, snowyManBodyY, snowyManBodyRadius.toFloat(), snowyManPaint)
複製代碼

這亞子第三步中畫的雪人手臂像手臂了吧,哼~

step5:畫一朵飄落的雪花

畫雪花以前咱們須要想象一下雪花在現實生活中的表現是什麼樣的:

  1. 大小不一
  2. 下落的速度不一
  3. 受風力等的影響水平速度不一
  4. 下落的初始位置不一樣

再結合咱們設備的信息,咱們知道,雪花在設備上飄落時會有一個運動的範圍,這個範圍取決於它的父佈局的寬和高。 綜合以上信息,咱們能夠畫出雪花的類圖:

代碼以下:

/**
 *  snowyView 中飄落的雪花實體
 *  使用Builder模式構造
 */
class SnowFlake(
        var radius: Float?,
        val speed: Float?,
        val angle: Float?,
        val moveScopeX: Float?,
        val moveScopeY: Float?
        ) {
    private val TAG = "SnowFlake"
    private val random = java.util.Random()
    private var presentX = random.nextInt(moveScopeX?.toInt() ?: 0).toFloat()
    private var presentY = random.nextInt(moveScopeY?.toInt() ?: 0).toFloat()
    private var presentSpeed = getSpeed()
    private var presentAngle = getAngle()
    private var presentRadius = getRadius()
    private val paint = Paint(Paint.ANTI_ALIAS_FLAG).apply {
        color = Color.parseColor("#e6e8db")
        alpha = 100
    }

    // 繪製雪花
    fun draw(canvas: Canvas){
        moveX()
        moveY()
        if (moveScopeX != null && moveScopeY != null){
            if (presentX > moveScopeX || presentY > moveScopeY || presentX < 0 || presentY < 0){
                reset()
            }
        }
        canvas.drawCircle(presentX, presentY, presentRadius, paint)
    }
    // 移動雪花(x軸方向)
    fun moveX(){
        presentX += getSpeedX()
    }
    // 移動雪花(Y軸方向)
    fun moveY(){
        presentY += getSpeedY()
    }

    fun getSpeed(): Float{
        var result: Float
        speed.let {
            result = it ?: (random.nextFloat() + 1)
        }
        return result
    }
    // 獲取雪花大小
    fun getRadius(): Float{
        var size: Float
        radius.let {
            size = it ?: random.nextInt(15).toFloat()
        }
        return size
    }
    // 獲取雪花下落角度
    fun getAngle(): Float{
        angle.let {
            if (it != null){
                if (it > 30){
                    return 30f
                }
                if (it < 0){
                    return 0f
                }
                return it
            }else{
                return random.nextInt(30).toFloat()
            }
        }
    }
    // 獲取雪花x軸的速度
    fun getSpeedX(): Float{
        return (presentSpeed * Math.sin(presentAngle.toDouble())).toFloat()
    }
    // 獲取雪花Y軸的速度
    fun getSpeedY(): Float{
        return (presentSpeed * Math.cos(presentAngle.toDouble())).toFloat()
    }
    // 重置雪花位置
    fun reset(){
        presentSpeed = getSpeed()
        presentAngle = getAngle()
        presentRadius = getRadius()
        presentX = random.nextInt(moveScopeX?.toInt()?:0).toFloat()
        presentY = 0f
    }

    data class Builder(
            var mRadius: Float? = null,
            var mSpeed: Float? = null,
            var mAngle: Float? = null,
            var moveScopeX: Float? = null,
            var moveScopeY: Float? = null
            ){
        fun radius(radius: Float) = apply { this.mRadius = radius }
        fun speed(speed: Float) = apply { this.mSpeed = speed }
        fun angle(angle: Float) = apply { this.mAngle = angle }
        fun scopeX(scope: Float) = apply { this.moveScopeX = scope }
        fun scopeY(scope: Float) = apply { this.moveScopeY = scope }
        fun build() = SnowFlake(mRadius, mSpeed, mAngle, moveScopeX, moveScopeY)
    }
}
複製代碼

step6:讓一羣雪花動起來!

這裏咱們隨機構造出30個雪花,他們的速度、大小、下落角度、初始位置都是隨機生成的,而後在SnowyView的onDraw()方法中繪製出來,並每隔5ms就刷新一次View,因爲雪花的位置是不停變換的,視覺上就造成了雪花紛紛揚揚的效果:

雪花紛紛何所似?——未若柳絮因風起


// snowFlakes 爲包含30個雪花的數組
for (snow in snowFlakes){
                snow.draw(canvas)
            }
            handler.postDelayed({
                invalidate()
            },5)
複製代碼

待優化

  • 因不可抗力緣由(仍是懶),代碼中許多變量命名略顯隨意
  • 雪花紛紛揚揚實現遵循簡單的原則,沒有考慮重力等因素的影響,若是把這些都考慮進去,實現出來的效果應該會更優秀
  • 還有4個天氣效果沒有實現
  • 雪花實體類SnowyFlake使用kotlin實現Builder模式總感受怪怪的,望能有大佬指點一二,不勝感激~

總結

感謝原做者D文斌的文章:【難以想象的CSS】天氣不可能那麼可愛 感謝扔物線大神的HenCoder系列文章(剛看到扔物線大佬的blog居然更新了)

相關文章
相關標籤/搜索