大体思路,借鉴了中的缓冲圆圈动画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
    }
}