最近做AI项目,设计师想实现类似于抖音那种加载动画效果,但是不是两个圆球交叉,而是两个三角形,其实可以用lottie动画的,但是我本人比较喜欢自定义控件,因此就自定义控件实现了。
代码如下:
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可以实现吗
感谢大家的支持,如有错误请指正,如需转载请标明原文出处!