大体思路,借鉴了中的缓冲圆圈动画Android 绘制动画(波浪动画/轨迹动画/PathMeasure)
绘制一个矩形边框,通过截取轨迹片段,通过修改轨迹片段的 start 和 stop 不断刷新,就会形成线条在移动的动画效果。
准备工作
1.控制线条的属性
// 设置旋转方向 CW 顺时针 CCW 逆时针
var direction: Path.Direction = Path.Direction.CCW
set(value) {
field = value
}
// 初始化画笔
private val paint: Paint = Paint(Paint.ANTI_ALIAS_FLAG)
// 设置线条的宽度
var lineWidth: Int = 10
set(value) {
field = value + 10
paint.strokeWidth = KBubbleUtils.dp2px(field).toFloat()
}
// 设置线条的长度
var lineWidth: Int = 10
set(value) {
field = value + 10
paint.strokeWidth = KBubbleUtils.dp2px(field).toFloat()
}
// 设置线条的旋转速度
var lineWidth: Int = 10
set(value) {
field = value + 10
paint.strokeWidth = KBubbleUtils.dp2px(field).toFloat()
}
// type == 0 单条线 type == 1 双线
var type = 0
set(value) {
field = value
// 初始化线条位置
rightAnimValue = (screenWidth + lineLength - (radius / 2)) / length
leftAnimValue = (screenWidth * 2 + screenHeight + lineLength - (radius / 2)) / length
}
2.设置一下画笔的基本属性和初始化动画
init {
paint.style = Paint.Style.STROKE
paint.strokeWidth = KBubbleUtils.dp2px(lineWidth).toFloat()
// 绘制的轨迹
dst = Path()
// 画一个圆, 关联pathMeasure
// 创建属性动画
animator = ValueAnimator.ofFloat(0f, 1f)
animator?.duration = 5000
animator?.interpolator = LinearInterpolator()
animator?.repeatCount = ValueAnimator.INFINITE
animator?.addUpdateListener { invalidate() }
animator?.start()
}
3.创建一个带圆角的矩形Path
这里用到了 moveTo(移动到某一位置)、 lineTo(画直线)、 cubicTo(二阶贝塞尔曲线,画圆角)
小知识点
// 将起始点移动到 x, y
public void moveTo(float x, float y)
// 画一条直线到 x,y 坐标
public void lineTo(float x, float y)
// 二阶贝塞尔曲线(两个控制点)
// x1 y1 第一个控制点
// x2 y2 第二个控制点
// x3 y3 曲线的终点
public void cubicTo(float x1, float y1, float x2, float y2,float x3, float y3)
开始绘制矩形并测量矩形的长度
val path = Path()
path.moveTo(radius, 0f)
path.lineTo(screenWidth - radius, 0f)
path.cubicTo(
screenWidth - radius / 2,
0f,
screenWidth.toFloat(),
radius / 2,
screenWidth.toFloat(),
radius
)
path.lineTo(screenWidth.toFloat(), screenHeight - radius)
//
path.cubicTo(
screenWidth.toFloat(),
screenHeight - radius / 2,
screenWidth - radius / 2,
screenHeight.toFloat(),
screenWidth - radius,
screenHeight.toFloat()
)
path.lineTo(radius, screenHeight.toFloat())
path.cubicTo(
radius / 2,
screenHeight.toFloat(),
0f,
screenHeight - radius / 2,
0f,
screenHeight - radius
)
path.lineTo(0f, radius)
path.cubicTo(0f, radius / 2, radius / 2, 0f, radius, 0f)
pathMeasure = PathMeasure() // 测量Path的类
pathMeasure.setPath(path, true)
length = pathMeasure.length // 路径长度
想让线条变得炫酷一些可以给线条增加渐变色
小知识点
我们这里用到线性渐变
// x0 y0 渐变的起始点
// x1 横向渐变的长度
// y1 纵向渐变的长度
// colors 渐变颜色 colors 必须 >= 2 否则会报错
// pos 必须和colors数量一致 或者 写 null
// tile 渐变的模式 CLAMP 会将边缘的一个像素进行拉伸、扩展
// REPEAT 平移复制
// MIRROR 镜面翻转
public LinearGradient(float x0, float y0, float x1, float y1, @NonNull @ColorInt int[] colors,@Nullable float[] positions, @NonNull TileMode tile)
绘制花里胡哨的线
val colors = intArrayOf(
Color.parseColor("#4FBCB5"),
Color.parseColor("#9D27DC"),
Color.parseColor("#4FBCB5")
)
val pos = floatArrayOf(0f, 0.5f, 1f)
val linearGradient =
LinearGradient(0f, 0f, lineLength, lineLength, colors, pos, Shader.TileMode.REPEAT)
val matrix = Matrix()
linearGradient.setLocalMatrix(matrix)
paint.shader = linearGradient
设置左右两条线的起点
rightAnimValue = (w + lineLength - (radius / 2)) / length
leftAnimValue = (w * 2 + h + lineLength - (radius / 2)) / length
4.开始绘制矩形和线条片段
// 关键方法
private fun drawLine(dst: Path, animValue: Float, canvas: Canvas): Float {
var tempAnimValue = animValue
dst.reset()
// 绘制方向
if (direction == Path.Direction.CW) {
tempAnimValue += speed
} else {
tempAnimValue -= speed
}
dst.lineTo(0f, 0f) // 解决硬件加速的bug
val stop = length * tempAnimValue
val start = stop - lineLength
//这里就是当动画走完一遍让他看起来不是立马重置,而是要继续绘制完线条的长度,才能连贯的转
if (tempAnimValue >= 1) { // 顺时针旋转
pathMeasure.getSegment(
length * tempAnimValue - lineLength,
length,
dst,
true
) // 截取整个path的任何片段(开始长度 / 结束长度 / 保存截取的路径 / 是否从起点开始截取)
canvas.drawPath(dst, paint)
pathMeasure.getSegment(
0f,
length * (tempAnimValue - 1),
dst,
true
) // 截取整个path的任何片段(开始长度 / 结束长度 / 保存截取的路径 / 是否从起点开始截取)
canvas.drawPath(dst, paint)
if (tempAnimValue > 1 + lineLength / length) {
tempAnimValue = lineLength / length
}
} else if (tempAnimValue <= lineLength / length) { // 逆时针旋转
pathMeasure.getSegment(
0f,
length * tempAnimValue,
dst,
true
)
canvas.drawPath(dst, paint)
pathMeasure.getSegment(
(1 + tempAnimValue) * length - lineLength,
length,
dst,
true
)
canvas.drawPath(dst, paint)
if (tempAnimValue < 0) {
tempAnimValue = 1f
}
} else {
pathMeasure.getSegment(
start,
stop,
dst,
true
) // 截取整个path的任何片段(开始长度 / 结束长度 / 保存截取的路径 / 是否从起点开始截取)
canvas.drawPath(dst, paint)
}
return tempAnimValue // 返回当前截取的位置
}
override fun onDraw(canvas: Canvas) {
super.onDraw(canvas)
rightAnimValue = drawLine(dst, rightAnimValue, canvas)
if (type == 1) { // type == 1 双线
leftAnimValue = drawLine(dst, leftAnimValue, canvas)
}
}
完整代码
package com.example.phonelightning.widget.screen
import android.animation.ValueAnimator
import android.content.Context
import android.graphics.*
import android.util.AttributeSet
import android.view.View
import android.view.animation.LinearInterpolator
import com.example.phonelightning.widget.KBubbleUtils
class LinePathView(context: Context?, attrs: AttributeSet?) : View(context, attrs) {
// 方向
var direction: Path.Direction = Path.Direction.CCW
set(value) {
field = value
}
private val paint: Paint = Paint(Paint.ANTI_ALIAS_FLAG)
private val dst: Path
private var length: Float = 0f
private var rightAnimValue: Float = 0f
private var leftAnimValue: Float = 0f
private val radius = 50f
private var screenWidth: Int = 0
private var screenHeight: Int = 0
var lineWidth: Int = 10
set(value) {
field = value + 10
paint.strokeWidth = KBubbleUtils.dp2px(field).toFloat()
}
var lineLength: Float = 400f
set(value) {
field = 400f + 50 * value
}
var speed: Float = 0.002f
set(value) {
field = 0.002f + (value / 2000)
}
lateinit var pathMeasure: PathMeasure
private var animator: ValueAnimator? = null
var type = 0
set(value) {
field = value
rightAnimValue = (screenWidth + lineLength - (radius / 2)) / length
leftAnimValue = (screenWidth * 2 + screenHeight + lineLength - (radius / 2)) / length
}
init {
paint.style = Paint.Style.STROKE
paint.strokeWidth = KBubbleUtils.dp2px(lineWidth).toFloat()
dst = Path()
// 画一个圆, 关联pathMeasure
// 创建属性动画
animator = ValueAnimator.ofFloat(0f, 1f)
animator?.duration = 5000
animator?.interpolator = LinearInterpolator()
animator?.repeatCount = ValueAnimator.INFINITE
animator?.addUpdateListener { invalidate() }
animator?.start()
}
override fun onSizeChanged(w: Int, h: Int, oldw: Int, oldh: Int) {
super.onSizeChanged(w, h, oldw, oldh)
screenHeight = h
screenWidth = w
val path = Path()
path.moveTo(radius, 0f)
path.lineTo(screenWidth - radius, 0f)
path.cubicTo(
screenWidth - radius / 2,
0f,
screenWidth.toFloat(),
radius / 2,
screenWidth.toFloat(),
radius
)
path.lineTo(screenWidth.toFloat(), screenHeight - radius)
//
path.cubicTo(
screenWidth.toFloat(),
screenHeight - radius / 2,
screenWidth - radius / 2,
screenHeight.toFloat(),
screenWidth - radius,
screenHeight.toFloat()
)
path.lineTo(radius, screenHeight.toFloat())
path.cubicTo(
radius / 2,
screenHeight.toFloat(),
0f,
screenHeight - radius / 2,
0f,
screenHeight - radius
)
path.lineTo(0f, radius)
path.cubicTo(0f, radius / 2, radius / 2, 0f, radius, 0f)
pathMeasure = PathMeasure() // 测量Path的类
pathMeasure.setPath(path, true)
length = pathMeasure.length // 路径长度
//渐变颜色.colors和pos的个数一定要相等
val colors = intArrayOf(
Color.parseColor("#4FBCB5"),
Color.parseColor("#9D27DC"),
Color.parseColor("#4FBCB5")
)
val pos = floatArrayOf(0f, 0.5f, 1f)
val linearGradient =
LinearGradient(0f, 0f, lineLength, lineLength, colors, pos, Shader.TileMode.REPEAT)
val matrix = Matrix()
linearGradient.setLocalMatrix(matrix)
paint.shader = linearGradient
rightAnimValue = (w + lineLength - (radius / 2)) / length
leftAnimValue = (w * 2 + h + lineLength - (radius / 2)) / length
}
override fun onDraw(canvas: Canvas) {
super.onDraw(canvas)
rightAnimValue = drawLine(dst, rightAnimValue, canvas)
if (type == 1) {
leftAnimValue = drawLine(dst, leftAnimValue, canvas)
}
}
private fun drawLine(dst: Path, animValue: Float, canvas: Canvas): Float {
var tempAnimValue = animValue
dst.reset()
if (direction == Path.Direction.CW) {
tempAnimValue += speed
} else {
tempAnimValue -= speed
}
dst.lineTo(0f, 0f) // 解决硬件加速的bug
val stop = length * tempAnimValue
val start = stop - lineLength
if (tempAnimValue >= 1) {
pathMeasure.getSegment(
length * tempAnimValue - lineLength,
length,
dst,
true
) // 截取整个path的任何片段(开始长度 / 结束长度 / 保存截取的路径 / 是否从起点开始截取)
canvas.drawPath(dst, paint)
pathMeasure.getSegment(
0f,
length * (tempAnimValue - 1),
dst,
true
) // 截取整个path的任何片段(开始长度 / 结束长度 / 保存截取的路径 / 是否从起点开始截取)
canvas.drawPath(dst, paint)
if (tempAnimValue > 1 + lineLength / length) {
tempAnimValue = lineLength / length
}
} else if (tempAnimValue <= lineLength / length) {
pathMeasure.getSegment(
0f,
length * tempAnimValue,
dst,
true
)
canvas.drawPath(dst, paint)
pathMeasure.getSegment(
(1 + tempAnimValue) * length - lineLength,
length,
dst,
true
)
canvas.drawPath(dst, paint)
if (tempAnimValue < 0) {
tempAnimValue = 1f
}
} else {
pathMeasure.getSegment(
start,
stop,
dst,
true
) // 截取整个path的任何片段(开始长度 / 结束长度 / 保存截取的路径 / 是否从起点开始截取)
canvas.drawPath(dst, paint)
}
return tempAnimValue
}
}