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

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

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

相关推荐
杉氧7 小时前
Framework 补完计划 (2):BufferQueue 与 SurfaceFlinger —— 像素的跨进程“接力赛”
android·架构·android jetpack
牢七7 小时前
RCE?复现成功
android
一化十7 小时前
Android16 自定义全局手势 任意界面依次点击屏幕四个角返回Home界面
android
三少爷的鞋9 小时前
Android 面试系列:Kotlin 协程的 delay 到底发生在哪个线程?
android
TechNomad20 小时前
Kotlin_Lambda编程详解
android·kotlin
熊猫_豆豆21 小时前
QT6 Android C++ 自制美观闹钟
android·c++·qt·闹钟
美狐美颜SDK开放平台21 小时前
Android/iOS直播APP平台开发中的视频美颜SDK优化技巧
android·ios·音视频·美颜sdk·第三方美颜sdk
聚美智数1 天前
黄历查询-运势查询-国际法定节假日查询-API接口介绍
android·java·数据库
私人珍藏库1 天前
[Android] 一木百宝箱-万能百宝箱+抖音去水印等几百种功能
android·人工智能·app·软件·多功能
wupa1 天前
在 enableEdgeToEdge 模式下处理软键盘与自定义面板协同
android