美文网首页Android开发Android高级开发架构Android开发经验谈
【不可思议的Canvas】天气不可能那么可爱

【不可思议的Canvas】天气不可能那么可爱

作者: 像程序那样思考 | 来源:发表于2019-08-23 15:32 被阅读13次

    前言

    这段时间一直在研究自定义View,恰好看到使用CSS实现的天气效果很不错,遂尝试使用自定义View实现一发。
    不要脸的套用原作者的一句话,希望原作者不要揍我~

    只有你想不到,没有自定义View实现不了的。今日分享由自定义View实现的效果 - Weather

    效果

    今我来思,雨雪霏霏


    晴空一鹤排云上,便引诗情到碧霄。


    由于不可抗力原因(懒),这里只实现了晴、雪两种天气效果。原作者文章里实现了晴、雪、云、雨、超级月亮?(原文Supermoon,本人水平有限,实在不知道怎么翻译),有兴趣的读者可以自行实现。

    源码

    两种天气效果的实现源码均已上传至Github - Weather,客官请自取,如果恰好赶上铁汁您心情好,不妨点个star

    分析

    接下来按照惯例分析一波项目里使用的重要API:

    BlurMaskFilter

    首先我们来看看源码中的注释是怎么描述的:

    /**
     * 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实现阴影效果~

    LinearGradient 线性渐变

    Android系统里的LinearGradient是paint的一种shader(着色器)方案。LinearGradient指的就是线性渐变:设置两个点和两种颜色,以这两个点作为端点,使用两种颜色的渐变来绘制颜色,大概像下面这样:

    辐射渐变(图片源自扔物线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模式总感觉怪怪的,望能有大佬指点一二,不胜感激~

    最后

    最后感谢大家能看到这,希望能带给大家帮助!
    同时也希望各位可以帮忙点个赞,关注一下,谢谢大家的支持!

    漫漫开发之路,我们只是其中的一小部分……只有不断的学习、进阶,才是我们的出路!才跟得上时代的进步!

    我从事Android开发快十年了,今年年初我花近两个月的时间收录整理了一套Android进阶知识体系,如果有想法深入的系统化的去学习,可以Android进阶技术交流(895077617 )免费获取整理版的资料
    **,我会把我收录整理的资料都送给大家,帮助大家更快的进阶。

    重要的事说三遍,关注+喜欢,让更多需要的朋友们都可以看到并且领到!

    image image image image

    相关文章

      网友评论

        本文标题:【不可思议的Canvas】天气不可能那么可爱

        本文链接:https://www.haomeiwen.com/subject/nnazsctx.html