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

最近做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可以实现吗

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

相关推荐
拭心4 小时前
Google 提供的 Android 端上大模型组件:MediaPipe LLM 介绍
android
带电的小王6 小时前
WhisperKit: Android 端测试 Whisper -- Android手机(Qualcomm GPU)部署音频大模型
android·智能手机·whisper·qualcomm
梦想平凡7 小时前
PHP 微信棋牌开发全解析:高级教程
android·数据库·oracle
元争栈道7 小时前
webview和H5来实现的android短视频(短剧)音视频播放依赖控件
android·音视频
阿甘知识库8 小时前
宝塔面板跨服务器数据同步教程:双机备份零停机
android·运维·服务器·备份·同步·宝塔面板·建站
元争栈道8 小时前
webview+H5来实现的android短视频(短剧)音视频播放依赖控件资源
android·音视频
MuYe8 小时前
Android Hook - 动态加载so库
android
居居飒9 小时前
Android学习(四)-Kotlin编程语言-for循环
android·学习·kotlin
Henry_He12 小时前
桌面列表小部件不能点击的问题分析
android
工程师老罗12 小时前
Android笔试面试题AI答之Android基础(1)
android