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)
    }
}

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

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

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

来看看SecondBezierTypeEvaluator类

image-20220906142512776

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

只需要注意的是:

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

调用的时候,只需要

val animator = ObjectAnimator.ofObject(
    this,
    "pointF",
    SecondBezierTypeEvaluator(p1), // 传入控制点
    p0, // 开始点
    p2 // 结束点
)	

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

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

image-20220906143141126

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

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

画多条贝塞尔线

假设需要画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()
}

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

// 随机颜色
val colorRandom: Int 
   get() {
       return Color.argb(
           255,
           (0 until 255).random(),
           (0 until 255).random(),
           (0 until 255).random()
       )
   }

来看看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
    }
}

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

绘制:

override fun onDraw(canvas: Canvas) {
    paint.style = Paint.Style.STROKE
    // 绘制每一条线
    repeat(COUNT) {
        // 设置颜色
        paint.color = paths[it].first
        // 画曲线
        canvas.drawPath(
            paths[it].second, paint
        )
    }
}

来看看当前效果:

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
}

最终效果:

DFA551D2910B778E120229AF9F9169A6

思路参考自

完整代码

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

其他自定义文章:

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

s10g

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值