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

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

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

相关推荐
zhangphil32 分钟前
Android理解onTrimMemory中ComponentCallbacks2的内存警戒水位线值
android
你过来啊你43 分钟前
Android View的绘制原理详解
android
移动开发者1号4 小时前
使用 Android App Bundle 极致压缩应用体积
android·kotlin
移动开发者1号4 小时前
构建高可用线上性能监控体系:从原理到实战
android·kotlin
ii_best9 小时前
按键精灵支持安卓14、15系统,兼容64位环境开发辅助工具
android
美狐美颜sdk9 小时前
跨平台直播美颜SDK集成实录:Android/iOS如何适配贴纸功能
android·人工智能·ios·架构·音视频·美颜sdk·第三方美颜sdk
恋猫de小郭13 小时前
Meta 宣布加入 Kotlin 基金会,将为 Kotlin 和 Android 生态提供全新支持
android·开发语言·ios·kotlin
aqi0014 小时前
FFmpeg开发笔记(七十七)Android的开源音视频剪辑框架RxFFmpeg
android·ffmpeg·音视频·流媒体
androidwork16 小时前
深入解析内存抖动:定位与修复实战(Kotlin版)
android·kotlin
梦天201516 小时前
android核心技术摘要
android