最近在喜马拉雅听书发现它的播放和暂停状态切换的动画挺有意思的. 在脑子里想了下如何实现, 画了画图, 就开始肝了. 最开始是用贝塞尔曲线去做点的位置的移动效果, 虽然做出来了, 但发现两个path的变化不对称, 看着不美观.睡一觉起来顿悟其实只要旋转圆的角度就可以了, 用不上贝塞尔控制点这么复杂.
先来看下实现效果:
实现效果
喜马拉雅的效果:
我实现的效果:
动画分析
View有两种状态, 一种是播放,如下图左边的Start, 一种是暂停, 如下图右边的End.
Start和End都是两个Path, 每个Path由四个点位组成. 状态的转变过程, 就是点位从Start到End的移动过程.
有几个注意的点:
- 矩形和三角形是带圆角的
通过CornerPathEffect给每个拐角都设置成圆角:
private val mPathEffect: PathEffect = CornerPathEffect(6f)
mPaint.pathEffect = mPathEffect
但是这样有一个问题, 在动画结束时, End状态的LeftPath的第3点位和RightPath的第3点位会存在圆角缺口, 不会完全重合.
我的处理方式是在动画结束时, 单独绘制一个大三角形. 圆角的处理有点粗暴, 但暂时想不到更好的方法了, 如果有更好的思路, 欢迎探讨.
- 动画过程的两部分是对称的
其实Start到End状态可以看成如下图的点位变化, 方便计算, 只需要再同时加上圆的旋转就可以了.
- 三角形居中显示
三角形的重心和圆心重合, 三角形才居中. 三角形是等边三角形, 重心和中心重合.
中心的坐标o是(0,0) , 边长是mRectHeight.
可得出
ab = \frac{mRectHeight * \sqrt{3}}{2}
ao : ob = 2 : 1
各点位坐标计算得出:
a(0, - ab / 3 * 2)
b(0, ab / 3)
c(mRectHeight / 2 , ab / 3 )
d(- mRectHeight / 2 , ab / 3 )
- 点位动画变化
用PointFEvaluator计算动画过程中的点位:
PointFEvaluator.java
public PointF evaluate(float fraction, PointF startValue, PointF endValue)
注意evaluate默认每次都会生成一个新的PointF, 可以在构造方法里传入一个PointF对象, 这样就能重复使用啦, 不会创建大量对象.
public PointFEvaluator(PointF reuse) {
mPoint = reuse;
}
OK, 分析完了, 直接上完整源码:
实现源码
// 实现喜马拉雅播放状态按钮切换
// Created by skylar on 2022/4/27.
//
class PlayerActionView : View {
companion object {
private val BG_COLOR = Color.WHITE
private val RECT_COLOR = Color.parseColor("#FF65433A")
}
private val mPaint = Paint()
private val mLeftPath = Path()
private val mRightPath = Path()
private var mLeftStartOne = PointF(0f, 0f)
private var mLeftStartTwo = PointF(0f, 0f)
private var mLeftStartThree = PointF(0f, 0f)
private var mLeftStartFour = PointF(0f, 0f)
private var mLeftEndOne = PointF(0f, 0f)
private var mLeftEndTwo = PointF(0f, 0f)
private var mLeftEndThree = PointF(0f, 0f)
private var mLeftEndFour = PointF(0f, 0f)
private var mLeftCurrentOne = PointF(0f, 0f)
private var mLeftCurrentTwo = PointF(0f, 0f)
private var mLeftCurrentThree = PointF(0f, 0f)
private var mLeftCurrentFour = PointF(0f, 0f)
private var mRightStartOne = PointF(0f, 0f)
private var mRightStartTwo = PointF(0f, 0f)
private var mRightStartThree = PointF(0f, 0f)
private var mRightStartFour = PointF(0f, 0f)
private var mRightEndOne = PointF(0f, 0f)
private var mRightEndTwo = PointF(0f, 0f)
private var mRightEndThree = PointF(0f, 0f)
private var mRightEndFour = PointF(0f, 0f)
private var mRightCurrentOne = PointF(0f, 0f)
private var mRightCurrentTwo = PointF(0f, 0f)
private var mRightCurrentThree = PointF(0f, 0f)
private var mRightCurrentFour = PointF(0f, 0f)
private val mLeftPointFEvaluatorOne = PointFEvaluator(mLeftCurrentOne)
private val mLeftPointFEvaluatorTwo = PointFEvaluator(mLeftCurrentTwo)
private val mLeftPointFEvaluatorThree = PointFEvaluator(mLeftCurrentThree)
private val mLeftPointFEvaluatorFour = PointFEvaluator(mLeftCurrentFour)
private val mRightPointFEvaluatorOne = PointFEvaluator(mRightCurrentOne)
private val mRightPointFEvaluatorTwo = PointFEvaluator(mRightCurrentTwo)
private val mRightPointFEvaluatorThree = PointFEvaluator(mRightCurrentThree)
private val mRightPointFEvaluatorFour = PointFEvaluator(mRightCurrentFour)
private val mPathEffect: PathEffect = CornerPathEffect(6f)
private var mRectWidth = 0f
private var mRectHeight = 0f
private var mFraction = 0f
private var isPlay = true
private var valueAnimator : ValueAnimator? = null
constructor(context: Context) : super(context)
constructor(context: Context, attrs: AttributeSet?) : this(context, attrs, 0)
constructor(
context: Context, attrs: AttributeSet?,
defStyleAttr: Int
) : super(context, attrs, defStyleAttr)
init {
mPaint.style = Paint.Style.FILL
mPaint.isAntiAlias = true
setOnClickListener {
//播放和暂停切换
isPlay = !isPlay
startAnimation()
}
}
override fun onMeasure(widthMeasureSpec: Int, heightMeasureSpec: Int) {
super.onMeasure(widthMeasureSpec, heightMeasureSpec)
val width = measuredWidth
val height = measuredHeight
val size = Math.min(width, height)
setMeasuredDimension(size, size)
}
override fun onSizeChanged(w: Int, h: Int, oldw: Int, oldh: Int) {
super.onSizeChanged(w, h, oldw, oldh)
mRectHeight = Utils.divide(width.toFloat(), 3f)
mRectWidth = Utils.divide(mRectHeight, 4f)
val halfHeight = Utils.divide(mRectHeight, 2f)
mLeftStartOne.set(-mRectWidth * 1.5f, -halfHeight)
mLeftStartTwo.set(-mRectWidth * 0.5f, -halfHeight)
mLeftStartThree.set(-mRectWidth * 0.5f, halfHeight)
mLeftStartFour.set(-mRectWidth * 1.5f, halfHeight)
//三角形的重心和圆心重合, 三角形才居中
val halfTriangleHeight = Utils.divide(mRectHeight * sqrt(3f), 6f)
mLeftEndOne.set(0f, -halfTriangleHeight * 2)
mLeftEndTwo.set(0f, -halfTriangleHeight * 2)
mLeftEndThree.set(0f, halfTriangleHeight)
mLeftEndFour.set(-mRectHeight * 0.5f, halfTriangleHeight)
mLeftCurrentOne.set(mLeftStartOne.x, mLeftStartOne.y)
mLeftCurrentTwo.set(mLeftStartTwo.x, mLeftStartTwo.y)
mLeftCurrentThree.set(mLeftStartThree.x, mLeftStartThree.y)
mLeftCurrentFour.set(mLeftStartFour.x, mLeftStartFour.y)
mRightStartOne.set(mRectWidth * 0.5f, -halfHeight)
mRightStartTwo.set(mRectWidth * 1.5f, -halfHeight)
mRightStartThree.set(mRectWidth * 1.5f, halfHeight)
mRightStartFour.set(mRectWidth * 0.5f, halfHeight)
mRightEndOne.set(0f, -halfTriangleHeight * 2)
mRightEndTwo.set(0f, -halfTriangleHeight * 2)
mRightEndThree.set(mRectHeight * 0.5f, halfTriangleHeight)
mRightEndFour.set(0f, halfTriangleHeight)
mRightCurrentOne.set(mRightStartOne.x, mRightStartOne.y)
mRightCurrentTwo.set(mRightStartTwo.x, mRightStartTwo.y)
mRightCurrentThree.set(mRightStartThree.x, mRightStartThree.y)
mRightCurrentFour.set(mRightStartFour.x, mRightStartFour.y)
}
override fun onDraw(canvas: Canvas?) {
super.onDraw(canvas)
if (canvas == null) {
return
}
canvas.translate((width / 2).toFloat(), (height / 2).toFloat())
//画背景圆圈
mPaint.color = BG_COLOR
canvas.drawCircle(0f, 0f, (width / 2).toFloat(), mPaint)
mPaint.pathEffect = mPathEffect
//动画过程中,旋转圆,这样矩形的四个点位置比较好计算,而且两个矩形的变化可以对称
if(valueAnimator?.isRunning == true || mFraction > 0) {
if(!isPlay) {
canvas.rotate(90 * mFraction)
} else {
canvas.rotate(90 + 90 * mFraction)
}
}
//结束时,只绘制一个圆角的三角形
if(mFraction == 1f && !isPlay) {
mPaint.color = RECT_COLOR
mLeftPath.reset()
mLeftPath.moveTo(mLeftEndOne.x, mLeftEndOne.y)
mLeftPath.lineTo(mRightEndThree.x, mRightEndThree.y)
mLeftPath.lineTo(mLeftEndFour.x, mLeftEndFour.y)
mLeftPath.close()
canvas.drawPath(mLeftPath, mPaint)
return
}
//画两个path,分别由四个点构成,动画不断改变四个点的位置
mPaint.color = RECT_COLOR
mLeftPath.reset()
mLeftPath.moveTo(mLeftCurrentOne.x, mLeftCurrentOne.y)
mLeftPath.lineTo(mLeftCurrentTwo.x, mLeftCurrentTwo.y)
mLeftPath.lineTo(mLeftCurrentThree.x, mLeftCurrentThree.y)
mLeftPath.lineTo(mLeftCurrentFour.x, mLeftCurrentFour.y)
mLeftPath.close()
canvas.drawPath(mLeftPath, mPaint)
mRightPath.reset()
mRightPath.moveTo(mRightCurrentOne.x, mRightCurrentOne.y)
mRightPath.lineTo(mRightCurrentTwo.x, mRightCurrentTwo.y)
mRightPath.lineTo(mRightCurrentThree.x, mRightCurrentThree.y)
mRightPath.lineTo(mRightCurrentFour.x, mRightCurrentFour.y)
mRightPath.close()
canvas.drawPath(mRightPath, mPaint)
}
private fun startAnimation() {
valueAnimator?.end()
if(valueAnimator == null) {
valueAnimator = ValueAnimator.ofFloat(0f, 1f)
valueAnimator?.duration = 500
valueAnimator?.addUpdateListener {
mFraction = it.animatedFraction
if(!isPlay) {
computePausePoint()
} else {
computePlayPoint()
}
invalidate()
}
}
valueAnimator?.start()
}
private fun computePausePoint() {
mLeftPointFEvaluatorOne.evaluate(mFraction, mLeftStartOne, mLeftEndOne)
mLeftPointFEvaluatorTwo.evaluate(mFraction, mLeftStartTwo, mLeftEndTwo)
mLeftPointFEvaluatorThree.evaluate(mFraction, mLeftStartThree, mLeftEndThree)
mLeftPointFEvaluatorFour.evaluate(mFraction, mLeftStartFour, mLeftEndFour)
mRightPointFEvaluatorOne.evaluate(mFraction, mRightStartOne, mRightEndOne)
mRightPointFEvaluatorTwo.evaluate(mFraction, mRightStartTwo, mRightEndTwo)
mRightPointFEvaluatorThree.evaluate(mFraction, mRightStartThree, mRightEndThree)
mRightPointFEvaluatorFour.evaluate(mFraction, mRightStartFour, mRightEndFour)
}
private fun computePlayPoint() {
mLeftPointFEvaluatorOne.evaluate(mFraction, mLeftEndOne, mLeftStartOne)
mLeftPointFEvaluatorTwo.evaluate(mFraction, mLeftEndTwo, mLeftStartTwo)
mLeftPointFEvaluatorThree.evaluate(mFraction, mLeftEndThree, mLeftStartThree)
mLeftPointFEvaluatorFour.evaluate(mFraction, mLeftEndFour, mLeftStartFour)
mRightPointFEvaluatorOne.evaluate(mFraction, mRightEndOne, mRightStartOne)
mRightPointFEvaluatorTwo.evaluate(mFraction, mRightEndTwo, mRightStartTwo)
mRightPointFEvaluatorThree.evaluate(mFraction, mRightEndThree, mRightStartThree)
mRightPointFEvaluatorFour.evaluate(mFraction, mRightEndFour, mRightStartFour)
}
}
本文由博客一文多发平台 OpenWrite 发布!