• android自定义View 中秋节放个烟花吧~


    本系列自定义View全部采用kt

    系统: mac

    android studio: 4.1.3

    kotlin version:1.5.0

    gradle: gradle-6.5-bin.zip

    废话不多说,先来看今天要完成的效果:

    D22DF4E9A12638281720875AEFB8A045

    效果分析:

    首先我们需要将这个功能分为两部分

    • 画渐变过渡文字
    • 画“爆炸烟花”

    其实烟花就是由一条条贝塞尔曲线构成,那么只要会画一条曲线,再循环一下就可以画出多条曲线

    首先来画一条曲线!

    画曲线

    image-20220906140929913

    Path方法介绍:

    • moveTo(x,y): 将画笔移动到x,y位置
    • quadTo(cX,cY,x2,y2): cX和cY表示控制点, x2,y2表示结束点

    这段代码很简单,就是贝塞尔最基本的使用

    让贝塞尔动起来,

    很显然,如果想让贝塞尔动起来,就不能使用这种方式, 最起码保证不能写死数据

    先来看一眼要完成的效果,在来看代码:

    4B7E021EA79013A53C92AE525C572908

    再来看一眼代码:

    class FireworksBlogView @JvmOverloads constructor(
        context: Context, attrs: AttributeSet? = null, defStyleAttr: Int = 0
    ) : View(context, attrs, defStyleAttr) {
        val paint = Paint(Paint.ANTI_ALIAS_FLAG).also {
            it.strokeWidth = 2.dp
            it.color = Color.BLACK
            it.style = Paint.Style.STROKE
        }
    
        var pointF = PointF()
            set(value) {
                field = value
                // 画线
                path.lineTo(value.x, value.y)
                invalidate()
            }
        val path = Path()
    
        override fun onSizeChanged(w: Int, h: Int, oldw: Int, oldh: Int) {
            super.onSizeChanged(w, h, oldw, oldh)
            animator()
        }
    
        private fun animator() {
            val p0 = PointF(50.dp, 100.dp) // 开始点
            val p1 = PointF(100.dp, 50.dp) // 控制点
            val p2 = PointF(150.dp, 100.dp) // 结束点
            val animator = ObjectAnimator.ofObject(
                this,
                "pointF",
                SecondBezierTypeEvaluator(p1),
                p0,
                p2
            )
            // 将画笔移动到开始位置
            path.moveTo(p0.x, p0.y)
            animator.duration = 2000L // 设置时间
            animator.start()
        }
    
        override fun onDraw(canvas: Canvas) {
            canvas.drawPath(path, paint)
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43
    • 44

    如果想要自己画贝塞尔曲线,那么就不能通过paint自带画贝塞尔曲线的方式,

    而是自己通过贝塞尔公式来计算!

    这段代码中,最重要的就是自定义TypeEvaluator()方法

    来看看SecondBezierTypeEvaluator类

    image-20220906142512776

    贝塞尔公式现在都是透明的,只要往里面带入一下值就可以

    只需要注意的是:

    • p0:开始点
    • p1:控制点
    • p2:结束点
    • t: 进度(0…1)

    调用的时候,只需要

    val animator = ObjectAnimator.ofObject(
        this,
        "pointF",
        SecondBezierTypeEvaluator(p1), // 传入控制点
        p0, // 开始点
        p2 // 结束点
    )	
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7

    最终贝塞尔曲线的路线,就会赋值到pointF上, 然后一直绘制pointF即可!

    那么二阶贝塞尔这么操作的话,三阶贝塞尔也是同样的道理:

    image-20220906143141126

    ObjectAnimator.ofObject(this,
        "pointF",
        ThirdBezierTypeEvaluator(p1, p2), // p1控制点1; p2控制点2;
        p0,// 开始点
        p3) // 结束点
    
    • 1
    • 2
    • 3
    • 4
    • 5

    这里用不到三阶贝塞尔,只是举例子.

    画多条贝塞尔线

    假设需要画100条贝塞尔曲线,并且平均分开

    首先先别着急画贝塞尔曲线,先来简单的,**先画100条直线,**看看思路是否正确,然后在往下走

    我们现在要画的效果长这样:

    image-20220906145333815

    这里假装有100条QaQ, 其实只有8条…

    这里我们要想画成这种效果,其实就是在一个圆内,求对应角的位置

    这个圆的半径是自己定义的

    每个角度 = 360.0 / 总个数

    画辅助线来看看

    image-20220906150153379

    这里就可以通过三角函数算出角A的位置

    角A.x = 半径 * cos(45) + 中心点.x

    角A.y = 半径 * sin(45) + 中心点.y

    然后改变角度就可计算出其他的位置,来看看代码

    image-20220906150606441

    可以看出,思路是没问题的, 那么结合画曲线和画直线,来完成今天的效果

    • 绘制曲线时候,是通过自定义TypeEvaluator来绘制

    那么要绘制多条曲线,肯定是将所有的点放到list中然后交给TypeEvaluator来处理

    来看看关键代码

    
    // 控制点
    private val controlPointF = PointF(100.dp, 100.dp)
    
    // 开始点
    private val startPointF by lazy { PointF(width / 2f, height / 2f) }
    
    
    // 用来存储路径 first画笔颜色, second:路径
    private val paths = arrayListOf<Pair<Int, Path>>()
    
    // 通过属性动画改变了值会跑到这里
    var points = arrayListOf<PointF>()
      set(value) {
        field = value
        repeat(COUNT) {
          // 绘制每一条曲线
          paths[it].second.lineTo(value[it].x, value[it].y)
        }
        invalidate()
      }
    
    private fun secondListBezierAnimator() {
        val p0 = arrayListOf<PointF>() // 开始点
        val p1 = arrayListOf<PointF>() // 控制点
        val p2 = arrayListOf<PointF>() // 结束点
        var angle = 0.0
        // 循环所有的点
        repeat(COUNT) {
            p0.add(startPointF) // 添加开始点
            p1.add(controlPointF) // 添加控制点
            val x = FireworksView.RADIUS * sin(Math.toRadians(angle)) + width / 2f
            val y = FireworksView.RADIUS * cos(Math.toRadians(angle)) + height / 2f
            p2.add(PointF(x.toFloat(), y.toFloat()))
    
            // 一个的角度
            angle += 360.0 / COUNT
    
            val path = Path()
            // 将画笔移动到开始点
            path.moveTo(p0[it].x, p0[it].y)
            // 保存起来
            paths.add(colorRandom to path)
        }
      
        val animator = ObjectAnimator.ofObject(
            this,
            "points",
            SecondListBezierTypeEvaluator(p1),
            p0,
            p2
        )
        animator.duration = FireworksView.TIME
        animator.start()
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43
    • 44
    • 45
    • 46
    • 47
    • 48
    • 49
    • 50
    • 51
    • 52
    • 53
    • 54
    • 55

    这段代码应该也比较简单,就是画一条会动的曲线和 画多条直线的结合!

    // 随机颜色
    val colorRandom: Int 
       get() {
           return Color.argb(
               255,
               (0 until 255).random(),
               (0 until 255).random(),
               (0 until 255).random()
           )
       }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10

    来看看SecondListBezierTypeEvaluator代码

    class SecondListBezierTypeEvaluator(private val p1: List<PointF>) :
        TypeEvaluator<List<PointF>> {
        // p0开始点; p1控制点; p2结束点
        override fun evaluate(t: Float, p0: List<PointF>, p2: List<PointF>): List<PointF> {
            // 二阶贝塞尔公式地址: https://baike.baidu.com/item/贝塞尔曲线/1091769
            if (!(p0.size == p1.size && p0.size == p2.size)) {
                throw RuntimeException("长度不匹配")
            }
    
            val points = arrayListOf<PointF>()
            repeat(p0.size) {
                points.add(
                    PointF(
                        (1 - t).pow(2) * p0[it].x + 2 * t * (1 - t) * p1[it].x + t.pow(2) * p2[it].x,
                        (1 - t).pow(2) * p0[it].y + 2 * t * (1 - t) * p1[it].y + t.pow(2) * p2[it].y
                    )
                )
            }
            return points
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21

    这里也比较简单,同样都是套公式, 不一样的只是多个一个循环而已

    绘制:

    override fun onDraw(canvas: Canvas) {
        paint.style = Paint.Style.STROKE
        // 绘制每一条线
        repeat(COUNT) {
            // 设置颜色
            paint.color = paths[it].first
            // 画曲线
            canvas.drawPath(
                paths[it].second, paint
            )
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12

    来看看当前效果:

    859D0AB22FC0F0B6C04A9636EC100A78

    渐变文字绘制

    还是同样的套路,从最简单开始

    绘制一段文字,并居中

    这段代码比较简单,来看代码

    image-20220906153517329

    Paint#measureText:

    @param 0: 需要测量的文字

    返回文字的宽度

    Canvas#drawText:

    @param 0: 需要绘制的文字

    @param start/ end: 绘制文字开始 / 结束 位置

    @param x,y: 绘制文字位置

    @param paint: 画笔

    文字坐标系可以参考这篇

    绘制完文字后,首先让文字全部渐变!

    image-20220906154556156

    使用渐变有2个需要注意的点:

    • 渐变的时候,Paint.color 会失效
    • 渐变完成后,一定要将shader设置为null

    否则就会出现这种情况

    image-20220906154730231

    渐变都是调用api,就不多介绍了,如果有疑问底部会给出完整demo

    让渐变颜色动起来,

    首先来看看移动位置的起点以及终点

    image-20220906164358254
    • 蓝色的为渐变的开始位置 (x)

    • 绿色为渐变的结束位置 (x + textWidth)

    动起来还是用属性动画

    image-20220906164620900

    有了这是一只在变得值,那么只需要变换渐变的位置即可!

    来看看绘制文字完整代码:

    /*
     * TODO 绘制文字
     */
    private fun drawText(canvas: Canvas) {
        paint.textSize = FireworksView.TEXT_SIZE
        paint.style = Paint.Style.FILL
        paint.color = Color.BLACK
    
        // 文字宽度
        val textWidth = paint.measureText(FireworksView.TEXT)
    
        val x = width / 2f - textWidth / 2f
        val y = -paint.fontMetrics.top + 50.dp
    
        // 渐变颜色
        val colors = intArrayOf(Color.BLACK,Color.RED, Color.YELLOW,Color.BLACK)
        // 线性渐变
        val linearGradient = LinearGradient(
            x, // 开始位置
            0f,
            x + 50.dp, // 渐变的位置 (这个位置是固定的,然后移动位置即可)
            0f,
            colors,
            null,
            Shader.TileMode.CLAMP
        )
    
        // 使用ktx扩展平移渐变位置
        linearGradient.transform {
            setTranslate(textWidthShader, 0f)
        }
    
    	  //  设置渐变色
        paint.shader = linearGradient
        canvas.drawText(
            FireworksView.TEXT, 0, FireworksView.TEXT.length,
            x, y, paint
        )
        paint.shader = null
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40

    最终效果:

    DFA551D2910B778E120229AF9F9169A6

    思路参考自

    完整代码

    原创不易,您的点赞与关注就是我最大的动力!

    其他自定义文章:

  • 相关阅读:
    源码编译安装PHP及搭建论坛
    Win11快捷复制粘贴不能用怎么办?Win11快捷复制粘贴不能用
    rust特性
    UVA - 10765 Doves and bombs
    正则表达式
    C/C++面试题
    Allegro在走线时如何隐藏其它网络的飞线
    html(抽奖设计)
    如何对Map集合的key进行大小写转换?
    概率论的学习和整理--番外11:10球里8红球2白球,抽俩次抽中白球的概率是多少呢? 一个例题的不同方法
  • 原文地址:https://blog.csdn.net/weixin_44819566/article/details/126728890