自定义控件实现类似于抖音加载动画效果

最近做AI项目,设计师想实现类似于抖音那种加载动画效果,但是不是两个圆球交叉,而是两个三角形,其实可以用lottie动画的,但是我本人比较喜欢自定义控件,因此就自定义控件实现了。

代码如下:

kotlin 复制代码
import android.animation.ValueAnimator
import android.content.Context
import android.graphics.Canvas
import android.graphics.Color
import android.graphics.Matrix
import android.graphics.Paint
import android.graphics.Path
import android.graphics.Rect
import android.util.AttributeSet
import android.view.View
import android.view.animation.AccelerateDecelerateInterpolator
import androidx.core.graphics.PathParser

class IntersectLoadingView @JvmOverloads constructor(
    context: Context, attrs: AttributeSet? = null, defStyleAttr: Int = 0
) : View(context, attrs, defStyleAttr) {

    private val leftShapePaint: Paint
    private val rightShapePaint: Paint
    private val intersectPaint: Paint

    private val leftShapePath: Path = Path()
    private val rightShapePath: Path = Path()
    private val intersectPath: Path = Path()

    private val boundsRect: Rect = Rect()

    private val baseShapeWidth = 14f
    private val baseShapeHeight = 14f

    var shapeScale: Float = 1.0f
        set(value) {
            shapeScaleX = value
            shapeScaleY = value
        }

    var shapeScaleX: Float = 1.0f
        set(value) {
            val density = context.resources.displayMetrics.density
            field = value * density
            boundsRect.set(
                0, 0, (baseShapeWidth * field).toInt(), (baseShapeHeight * shapeScaleY).toInt(),
            )
            createLeftPath()
            createRightPath()
            invalidate()
        }

    var shapeScaleY: Float = 1.0f
        set(value) {
            val density = context.resources.displayMetrics.density
            field = value * density
            boundsRect.set(
                0, 0, (baseShapeWidth * shapeScaleX).toInt(), (baseShapeHeight * field).toInt()
            )
            createLeftPath()
            createRightPath()
            invalidate()
        }

    private var animator: ValueAnimator? = null
    private var lastAnimatedValue: Float = 0f

    var intersectRatio: Float = 1f / 2f
        set(value) {
            field = value
            invalidate()
        }

    init {
        shapeScale = 1.0f

        leftShapePaint = Paint(Paint.ANTI_ALIAS_FLAG)
        leftShapePaint.color = Color.parseColor("#FF6940")
        leftShapePaint.style = Paint.Style.FILL

        rightShapePaint = Paint(Paint.ANTI_ALIAS_FLAG)
        rightShapePaint.color = Color.parseColor("#7D70FF")
        rightShapePaint.style = Paint.Style.FILL

        intersectPaint = Paint(Paint.ANTI_ALIAS_FLAG)
        intersectPaint.color = Color.WHITE
        intersectPaint.style = Paint.Style.FILL

        createLeftPath()
        createRightPath()
    }

    override fun onSizeChanged(w: Int, h: Int, oldw: Int, oldh: Int) {
        super.onSizeChanged(w, h, oldw, oldh)

        rightShapePath.offset((w - boundsRect.width()).toFloat(), 0f)
        startAnimator()
    }

    override fun onDraw(canvas: Canvas) {
        super.onDraw(canvas)

        canvas.translate(0f, (height - boundsRect.height()) / 2f)

        canvas.drawPath(leftShapePath, leftShapePaint)
        canvas.drawPath(rightShapePath, rightShapePaint)

        intersectPath.reset()
        intersectPath.op(leftShapePath, rightShapePath, Path.Op.INTERSECT)
        canvas.drawPath(intersectPath, intersectPaint)
    }

    private fun createLeftPath() {
        leftShapePath.apply {
            reset()
            set(PathParser.createPathFromPathData("M8.089,2.67C10.719,4.189 12.035,4.948 12.408,5.974C12.649,6.637 12.649,7.363 12.408,8.026C12.035,9.052 10.719,9.811 8.089,11.33C5.458,12.849 4.143,13.608 3.068,13.418C2.373,13.296 1.744,12.933 1.291,12.392C0.589,11.556 0.589,10.037 0.589,7C0.589,3.963 0.589,2.444 1.291,1.608C1.744,1.067 2.373,0.704 3.068,0.581C4.143,0.392 5.458,1.151 8.089,2.67Z"))
            transform(Matrix().also {
                it.setScale(shapeScaleX, shapeScaleY)
            })
        }
    }

    private fun createRightPath() {
        rightShapePath.apply {
            reset()
            set(PathParser.createPathFromPathData("M4.911,2.67C2.281,4.189 0.966,4.948 0.592,5.974C0.351,6.637 0.351,7.363 0.592,8.026C0.966,9.052 2.281,9.811 4.911,11.33C7.542,12.849 8.857,13.608 9.932,13.418C10.627,13.296 11.256,12.933 11.709,12.392C12.411,11.556 12.411,10.037 12.411,7C12.411,3.963 12.411,2.444 11.709,1.608C11.256,1.067 10.627,0.704 9.932,0.581C8.857,0.392 7.542,1.151 4.911,2.67Z"))
            transform(Matrix().also {
                it.setScale(shapeScaleX, shapeScaleY)
            })
        }
    }

    private fun update(animatedValue: Float) {
        val offset = animatedValue - lastAnimatedValue
        lastAnimatedValue = animatedValue
        leftShapePath.offset(offset, 0f)
        rightShapePath.offset(-offset, 0f)
        invalidate()
    }

    private fun startAnimator() {
        (animator ?: run {
            ValueAnimator.ofFloat(0f, width / 2f - boundsRect.width() * (1 - intersectRatio))
        }).apply {
            removeAllUpdateListeners()
            duration = 300
            repeatCount = ValueAnimator.INFINITE
            repeatMode = ValueAnimator.REVERSE
            interpolator = AccelerateDecelerateInterpolator()
            addUpdateListener {
                update(it.animatedValue as Float)
            }
            if (!isStarted) {
                start()
            }
        }
    }

    private fun stopAnimator() {
        animator?.cancel()
    }

    override fun onDetachedFromWindow() {
        stopAnimator()
        super.onDetachedFromWindow()
    }

    override fun onVisibilityChanged(changedView: View, visibility: Int) {
        super.onVisibilityChanged(changedView, visibility)
        if (visibility != VISIBLE) {
            stopAnimator()
        } else {
            post {
                startAnimator()
            }
        }
    }
}

其中核心实现原理是:

复制代码
Path.op(Path, Path, android.graphics.Path.Op)

思考:采用Xfermode可以实现吗

感谢大家的支持,如有错误请指正,如需转载请标明原文出处!

相关推荐
似霰14 分钟前
Android 日志系统4——logd 写日志过程分析一
android
youyoulg1 小时前
利用Android Studio编译Android上可直接执行的二进制
android·ide·android studio
闽农1 小时前
Android ANR 调用栈溯源
android·anr
似霰1 小时前
Android 日志系统7——Android 平台日志丢失问题分析
android·log
·云扬·1 小时前
MySQL Undo Log 深度解析:事务回滚与 MVCC 的底层支柱
android·数据库·mysql
fareast_mzh2 小时前
如何检测、排除手机控制屏幕
android
左手厨刀右手茼蒿2 小时前
Flutter for OpenHarmony 实战:DartX — 极致简练的开发超能力集
android·flutter·ui·华为·harmonyos
codeGoogle2 小时前
2026 年 IM 怎么选?聊聊 4 家主流即时通讯方案的差异
android·前端·后端
hewence12 小时前
Kotlin初入协程
android·kotlin