本系列自定义View全部采用kt
系统: mac
android studio: 4.1.3
kotlin version:1.5.0
gradle: gradle-6.5-bin.zip
废话不多说,先来看今天要完成的效果:
效果分析:
首先我们需要将这个功能分为两部分
其实烟花就是由一条条贝塞尔曲线构成,那么只要会画一条曲线,再循环一下就可以画出多条曲线
首先来画一条曲线!
画曲线
Path方法介绍:
- moveTo(x,y): 将画笔移动到x,y位置
- quadTo(cX,cY,x2,y2): cX和cY表示控制点, x2,y2表示结束点
这段代码很简单,就是贝塞尔最基本的使用
让贝塞尔动起来,
很显然,如果想让贝塞尔动起来,就不能使用这种方式, 最起码保证不能写死数据
先来看一眼要完成的效果,在来看代码:
再来看一眼代码:
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类
贝塞尔公式现在都是透明的,只要往里面带入一下值就可以
只需要注意的是:
- p0:开始点
- p1:控制点
- p2:结束点
- t: 进度(0…1)
调用的时候,只需要
val animator = ObjectAnimator.ofObject(
this,
"pointF",
SecondBezierTypeEvaluator(p1),
p0,
p2
)
最终贝塞尔曲线的路线,就会赋值到pointF上, 然后一直绘制pointF即可!
那么二阶贝塞尔这么操作的话,三阶贝塞尔也是同样的道理:
ObjectAnimator.ofObject(this,
"pointF",
ThirdBezierTypeEvaluator(p1, p2),
p0,
p3)
这里用不到三阶贝塞尔,只是举例子.
画多条贝塞尔线
假设需要画100条贝塞尔曲线,并且平均分开
首先先别着急画贝塞尔曲线,先来简单的,**先画100条直线,**看看思路是否正确,然后在往下走
我们现在要画的效果长这样:
这里假装有100条QaQ, 其实只有8条…
这里我们要想画成这种效果,其实就是在一个圆内,求对应角的位置
这个圆的半径是自己定义的
每个角度 = 360.0 / 总个数
画辅助线来看看
这里就可以通过三角函数算出角A的位置
角A.x = 半径 * cos(45) + 中心点.x
角A.y = 半径 * sin(45) + 中心点.y
然后改变角度就可计算出其他的位置,来看看代码
可以看出,思路是没问题的, 那么结合画曲线和画直线,来完成今天的效果
- 绘制曲线时候,是通过自定义TypeEvaluator来绘制
那么要绘制多条曲线,肯定是将所有的点放到list中然后交给TypeEvaluator来处理
来看看关键代码
private val controlPointF = PointF(100.dp, 100.dp)
private val startPointF by lazy { PointF(width / 2f, height / 2f) }
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>> {
override fun evaluate(t: Float, p0: List<PointF>, p2: List<PointF>): List<PointF> {
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
)
}
}
来看看当前效果:
渐变文字绘制
还是同样的套路,从最简单开始
绘制一段文字,并居中
这段代码比较简单,来看代码
Paint#measureText:
@param 0: 需要测量的文字
返回文字的宽度
Canvas#drawText:
@param 0: 需要绘制的文字
@param start/ end: 绘制文字开始 / 结束 位置
@param x,y: 绘制文字位置
@param paint: 画笔
文字坐标系可以参考这篇
绘制完文字后,首先让文字全部渐变!
使用渐变有2个需要注意的点:
- 渐变的时候,Paint.color 会失效
- 渐变完成后,一定要将shader设置为null
否则就会出现这种情况
渐变都是调用api,就不多介绍了,如果有疑问底部会给出完整demo
让渐变颜色动起来,
首先来看看移动位置的起点以及终点
动起来还是用属性动画
有了这是一只在变得值,那么只需要变换渐变的位置即可!
来看看绘制文字完整代码:
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
)
linearGradient.transform {
setTranslate(textWidthShader, 0f)
}
paint.shader = linearGradient
canvas.drawText(
FireworksView.TEXT, 0, FireworksView.TEXT.length,
x, y, paint
)
paint.shader = null
}
最终效果:
思路参考自
完整代码
原创不易,您的点赞与关注就是我最大的动力!
其他自定义文章:
|