前言
本文参考辉哥的视差动画 - 雅虎新闻摘要加载,继续加练学习自定义View动画效果。
最终效果
实现思路
整体动画可以拆分为三个阶段:
圆形旋转动画:
分别绘制六个不同颜色的圆形,其中每个圆的坐标如下图,角a变化范围从[0,2π],使用属性动画监听角度旋转,不断重绘各个圆即可;
每个圆向中心聚合动画
:这里不断的去改变旋转动画中每个圆点距离屏幕中心的距离
(也就是上图中的x),属性动画的取值范围为[x,0],同时需要注意聚合动画开始时会先外抖动扩散下,这里使用AnticipateInterpolator
差值器可以实现;从中心向外扩散动画:
这里绘制的是个空心圆,扩散范围从[0,x](图中centerX为屏幕宽度的一半,centerY为屏幕高度的一半),因为至少当半径到达x的距离,才可以显示整个屏幕下方的背景,同时需要注意画笔的宽度以及半径的计算方式
。
相关源码
- 自定义颜色数组
<color name="orange">#FF9600</color>
<color name="aqua">#02D1AC</color>
<color name="yellow">#FFD200</color>
<color name="blue">#00C6FF</color>
<color name="green">#00E099</color>
<color name="pink">#FF3891</color>
<array name="circle_colors">
<item>@color/blue</item>
<item>@color/green</item>
<item>@color/pink</item>
<item>@color/orange</item>
<item>@color/aqua</item>
<item>@color/yellow</item>
</array>
- 实现自定义加载动画
LoadingView
package com.crystal.view.parallax
import android.animation.Animator
import android.animation.AnimatorListenerAdapter
import android.animation.ObjectAnimator
import android.content.Context
import android.graphics.*
import android.graphics.Paint.Style
import android.util.AttributeSet
import android.view.View
import android.view.animation.AnticipateInterpolator
import android.view.animation.LinearInterpolator
import com.crystal.view.R
import kotlin.math.cos
import kotlin.math.sin
import kotlin.math.sqrt
/**
* 加载动画
* on 2022/11/15
*/
class LoadingView : View {
/**
* 画笔工具
*/
private var paint: Paint = Paint()
/**
* 圆形颜色集合
*/
private var paintColors: IntArray
/**
* 圆形半径 这里取 centerToCircleRadius的1/8
*/
private var circleRadius = 0f
/**
* 屏幕中心距离圆心距离 这里取屏幕宽度的1/4
*/
private var centerToCircleRadius = 0f
/**
* 屏幕宽度中心
*/
private var centerX = 0f
/**
* 屏幕高度中心
*/
private var centerY = 0f
/**
* 整体背景
*/
private var splashColor = Color.WHITE
/**
* 旋转不断变化的角度
*/
private var currentRotationDegree = 0f
/**
* 每一个圆初始值角度,不旋转默认状态下应为60
*/
private var perCircleDegree = 0f
/**
* 扩散最大半径 取 屏幕对角的1半
*/
private var spreadMaxRadius = 0f
constructor(context: Context) : this(context, null)
constructor(context: Context, attrs: AttributeSet?) : this(context, attrs, 0)
constructor(context: Context, attrs: AttributeSet?, defStyleAttr: Int) : super(
context,
attrs,
defStyleAttr
) {
paint.isDither = true
paint.isAntiAlias = true
paintColors = resources.getIntArray(R.array.circle_colors)
//每个圆平均角度
perCircleDegree = 2 * Math.PI.toFloat() / paintColors.size
}
override fun onMeasure(widthMeasureSpec: Int, heightMeasureSpec: Int) {
super.onMeasure(widthMeasureSpec, heightMeasureSpec)
centerToCircleRadius = measuredWidth / 4f
circleRadius = centerToCircleRadius / 8f
centerX = measuredWidth / 2f
centerY = measuredHeight / 2f
spreadMaxRadius = sqrt(centerX * centerX + centerY * centerY)
}
private var drawAnimator: DrawAnimator? = null
override fun onDraw(canvas: Canvas) {
if (drawAnimator == null) {
drawAnimator = DrawRotationAnimator()
}
drawAnimator?.onDraw(canvas)
}
/**
* 旋转、聚合、扩散动画抽象类
*/
private abstract inner class DrawAnimator {
abstract fun onDraw(canvas: Canvas)
open fun cancelAnimator() {}
}
/**
* 开启聚合动画
*/
fun startGatherAnimator() {
drawAnimator?.cancelAnimator()
drawAnimator = DrawGatherAnimator()
}
/**
* 开启扩散动画
*/
fun startSpreadAnimator() {
drawAnimator = DrawSpreadAnimator()
}
/**
* 绘制旋转动画
*/
private inner class DrawRotationAnimator : DrawAnimator() {
/**
* 旋转动画 角度变化从 0 - 2π
*/
private var animator = ObjectAnimator.ofFloat(0f, 2 * Math.PI.toFloat())
init {
animator.repeatCount = -1
animator.duration = 5000
animator.interpolator = LinearInterpolator()
animator.addUpdateListener {
currentRotationDegree = it.animatedValue as Float
invalidate()
}
animator.start()
}
override fun onDraw(canvas: Canvas) {
//白色背景
canvas.drawColor(splashColor)
for (i in paintColors.indices) {
//当前角度 第一个圆 [0+旋转角度] 第二个圆是[60 + 旋转角度] 依次类推
val currentDegree = i * perCircleDegree + currentRotationDegree
//绘制圆心坐标X点
val circleX = centerX + centerToCircleRadius * cos(currentDegree)
//绘制圆心坐标Y点
val circleY = centerY + centerToCircleRadius * sin(currentDegree)
paint.color = paintColors[i]
canvas.drawCircle(circleX, circleY, circleRadius, paint)
}
}
override fun cancelAnimator() {
animator.cancel()
}
}
/**
* 聚合动画 各个圆像中心点汇聚,实际上就是不断变化centerToCircleRadius的值
*/
private inner class DrawGatherAnimator : DrawAnimator() {
private var currentRadius = 0f
/**
* 从当前位置 不断缩小距离中心点距离
*/
private var animator = ObjectAnimator.ofFloat(centerToCircleRadius, 0f)
init {
animator.duration = 3000
//先回退一小步然后加速前进
animator.interpolator = AnticipateInterpolator()
animator.addUpdateListener {
currentRadius = it.animatedValue as Float
invalidate()
}
animator.addListener(object : AnimatorListenerAdapter() {
override fun onAnimationEnd(animation: Animator?) {
//结束后执行扩散动画
startSpreadAnimator()
}
})
animator.start()
}
override fun onDraw(canvas: Canvas) {
//白色背景
canvas.drawColor(splashColor)
for (i in paintColors.indices) {
//当前角度 第一个圆 [0+旋转角度] 第二个圆是[60 + 旋转角度] 依次类推
val currentDegree = i * perCircleDegree + currentRotationDegree
//绘制圆心坐标X点
val circleX = centerX + currentRadius * cos(currentDegree)
//绘制圆心坐标Y点
val circleY = centerY + currentRadius * sin(currentDegree)
paint.color = paintColors[i]
canvas.drawCircle(circleX, circleY, circleRadius, paint)
}
}
override fun cancelAnimator() {
animator.cancel()
}
}
/**
* 扩散动画 实际上就是画一个空心圆,不断向外扩散至
*/
private inner class DrawSpreadAnimator : DrawAnimator() {
private var currentSpreadRadius = 0f
/**
* 从中心点 向 spreadMaxRadius扩散
*/
private var animator = ObjectAnimator.ofFloat(0f, spreadMaxRadius)
init {
animator.duration = 3000
//先回退一小步然后加速前进
animator.interpolator = LinearInterpolator()
animator.addUpdateListener {
currentSpreadRadius = it.animatedValue as Float
invalidate()
}
animator.start()
}
override fun onDraw(canvas: Canvas) {
//白色背景
paint.style = Style.STROKE
//画笔宽度 = 变化最大值 - 当前值
paint.strokeWidth = spreadMaxRadius - currentSpreadRadius
paint.color = splashColor
//圆的半径 = 当前值 + 画笔宽度的1半
val radius = currentSpreadRadius + paint.strokeWidth / 2
canvas.drawCircle(centerX, centerY, radius, paint)
}
override fun cancelAnimator() {
animator.cancel()
}
}
}
验证代码
val loadingView = findViewById<com.crystal.view.parallax.LoadingView>(R.id.loadingView)
loadingView.postDelayed(
{ loadingView.startGatherAnimator() }, 3000
)
总结
看了辉哥一系列的自定义View课程,最最重要的是对效果的分析,将效果拆分成一步步对应去实现,就会容易很多。
结语
如果以上文章对您有一点点帮助,希望您不要吝啬的点个赞加个关注,您每一次小小的举动都是我坚持写作的不懈动力!ღ( ´・ᴗ・` )