博主声明:
转载请在开头附加本文链接及作者信息,并标记为转载。本文由博主 威威喵 原创,请多支持与指教。
本次自定义 View 写的是一个仿联想手机 ZUI 系统加载动画的效果,前几天博主收到了 ZUI 11 的更新,发现更新了之后并没有大的改善,反而一直用的 touch 手势被改了,一时间不太习惯,老是滑错了,不得不说这个联想手机系统不怎么样。博主也是第一次买的联想手机,吐槽一下,哈哈。
不过呢,对 ZUI 系统的加载动画产生了兴趣,大家有用过联想手机的可能会注意到,它是三个小球在不停的旋转,并且移动到中心,合为一个。接着又分裂为三个,换了颜色,然后一直循环这样做。表达不清楚,直接来看效果图吧,以下是我的手机中的一段录制视频,随便找了一个蓝牙的搜索功能,在搜索时就会有这三个小球的加载动画,如下:
image看到这个效果呢,首先当然是分析一波了。它是三个球旋转一圈,加上往中心聚合的动画,这两种动画一起执行,然后又开始发散,并且颜色也改变了。
首先呢,我们需要绘制三个小圆,这三个小圆每隔的角度都是一样的,也就是 120°,我们要得到小圆的坐标 x,y 值,就需要知道 c1 圆心与 r 半径,这两个都是我们自己设定的。来看下面这张图:
imageP 点就代表小圆的坐标,从上面看,利用三角函数公式,我们很容易就得出 P 点的坐标值。C 点是圆心的坐标,设置为屏幕的中心位置,半径 r 的值,我们给予一个默认的就好了。代码如下:
<?xml version="1.0" encoding="utf-8"?>
<resources>
<declare-styleable name="ZUILoadingView">
<attr name="circle_radius" format="float" />
<attr name="circle_distance" format="float" />
</declare-styleable>
</resources>
三个小圆,我们利用循环来绘制即可。看了一点 kotlin 的语法,感觉学起来挺轻松的,也很容易,这是我第一篇使用 kotlin 来编写的。博主将要在以后的文章中使用 kotlin 了,毕竟要随波逐流,才能飘起来,哈哈。
private fun drawCircles(canvas: Canvas) {
for (i in 0..2) {
val diff = mRad * mAngle + mRad * 120f * i
val circleX: Float = mDefDistance * Math.cos(diff).toFloat();
val circleY: Float = mDefDistance * Math.sin(diff).toFloat();
mPaint.color = getColors(mCurColor)[i]
canvas.drawCircle(circleX, circleY, mDefRadius, mPaint)
}
startAnimator()
}
以上代码没什么难度,我们可以很轻松的绘制三个分布一圈的小圆,它们的间隔是 120 °,这个效果的难点,也就是对三个小圆动画的处理了。
这个效果的动画分为两个部分,第一是:圆旋转并且往中心汇聚,第二是:汇聚后更换颜色,然后向四周旋转并且发散。要做到这个效果,我废了少的功夫。我明白了一个问题,那就是 AnimatorSet 没有重复的方法,而且 ValueAnimator 的同一个实例,我们在添加到 AnimatorSet 里的时候,不能实现动画循环效果。至于什么意思,自己可以去试试,我就不多说明了。直接来看动画的代码吧
private fun startAnimator() {
// 旋转动画
if (mRotateAnimator == null) {
mRotateAnimator = ObjectAnimator.ofFloat(0f, 360f)
mRotateAnimator?.addUpdateListener { animation: ValueAnimator ->
mAngle = animation.getAnimatedValue() as Float
postInvalidate()
}
mRotateAnimator?.duration = 1000L
}
if (mRotateAnimator2 == null) {
mRotateAnimator2 = ObjectAnimator.ofFloat(360f, 720f)
mRotateAnimator2?.addUpdateListener { animation: ValueAnimator ->
mAngle = animation.getAnimatedValue() as Float
postInvalidate()
}
mRotateAnimator2?.duration = 1000L
}
// 平移动画
if (mTranslateAnimator == null) {
mTranslateAnimator = ObjectAnimator.ofFloat(mDefDistance, 0f)
mTranslateAnimator?.addUpdateListener { animation: ValueAnimator ->
mDefDistance = animation.getAnimatedValue() as Float
}
mTranslateAnimator?.duration = 1000L
}
if (mTranslateAnimator2 == null) {
mTranslateAnimator2 = ObjectAnimator.ofFloat(0f, mDefDistance)
mTranslateAnimator2?.addUpdateListener { animation: ValueAnimator ->
mDefDistance = animation.getAnimatedValue() as Float
}
mTranslateAnimator2?.duration = 1000L
}
if (mAnimTogether == null) {
mAnimTogether = AnimatorSet()
mAnimTogether?.playTogether(mRotateAnimator, mTranslateAnimator)
}
if (mAnimTogether2 == null) {
mAnimTogether2 = AnimatorSet()
mAnimTogether2?.playTogether(mRotateAnimator2, mTranslateAnimator2)
mAnimTogether2?.addListener(object : AnimatorListenerAdapter() {
override fun onAnimationStart(animation: Animator?) {
changeColor()
}
})
}
if (mAnimSequentially == null) {
mAnimSequentially = AnimatorSet()
mAnimSequentially?.playSequentially(mAnimTogether, mAnimTogether2)
mAnimSequentially?.start()
//动画循环
mAnimSequentially?.addListener(object : AnimatorListenerAdapter() {
override fun onAnimationEnd(animation: Animator?) {
mAnimSequentially?.start()
}
})
}
}
简单的介绍一下上面的代码,小圆的旋转效果,一圈就是 360 ° 的变化,这个好理解。那么汇聚和发散的动画,它其实变化的就是中心点与小圆圆心的距离,这其实就是我们赋予的半径 R ,也就是下面的一个 circle_distance 属性。改变这个值,就能实现汇聚和发散的动画效果。
<nd.no.xww.learnkotlin.ZUILoadingView
android:layout_width="200dp"
app:circle_radius="10"
app:circle_distance="40"
android:layout_height="200dp"
android:layout_centerInParent="true" />
那么以上就是我们在 activity_main 中的简单使用,你可以控制小圆的半径,以及和圆心的距离。最后,是我们的完整代码,如下:
package nd.no.xww.learnkotlin
import android.animation.*
import android.annotation.TargetApi
import android.content.Context
import android.content.res.TypedArray
import android.graphics.Canvas
import android.graphics.Color
import android.graphics.Paint
import android.os.Build
import android.util.AttributeSet
import android.view.View
/**
*@desciption : 联想手机 zui 系统的加载动画
*@author xww
*@date 2019/8/16
*@time 16:10
* 博主:威威喵
* 博客:https://blog.csdn.net/smile_Running
*/
class ZUILoadingView @JvmOverloads constructor(context: Context, attrs: AttributeSet? = null, defStyleAttr: Int = 0)
: View(context, attrs, defStyleAttr) {
private var mPaint: Paint = Paint()
private var mDefRadius: Float = 20f
private var mDefDistance: Float = 100f
private val mRad = 2 * Math.PI / 360f;
init {
val array: TypedArray = context.obtainStyledAttributes(attrs, R.styleable.ZUILoadingView)
mDefRadius = array.getFloat(R.styleable.ZUILoadingView_circle_radius, mDefRadius)
mDefDistance = array.getFloat(R.styleable.ZUILoadingView_circle_distance, mDefDistance)
array.recycle()
mPaint.isDither = true
mPaint.isAntiAlias = true
}
override fun onMeasure(widthMeasureSpec: Int, heightMeasureSpec: Int) {
val widthMode = MeasureSpec.getMode(widthMeasureSpec)
var widthSize = MeasureSpec.getSize(widthMeasureSpec)
if (widthMode == MeasureSpec.AT_MOST)
widthSize = 200
val heigthMode = MeasureSpec.getMode(widthMeasureSpec)
var heigthSize = MeasureSpec.getSize(widthMeasureSpec)
if (heigthMode == MeasureSpec.AT_MOST)
widthSize = 200
if (widthSize != heigthSize) {
widthSize = Math.min(widthSize, heigthSize)
heigthSize = widthSize
}
setMeasuredDimension(widthSize, heigthSize)
}
override fun onDraw(canvas: Canvas?) {
super.onDraw(canvas)
canvas ?: return
canvas.translate(width / 2f, height / 2f)
drawCircles(canvas)
}
private val COLORS_ONE: IntArray = intArrayOf(Color.BLUE, Color.YELLOW, Color.RED)
private val COLORS_TWO: IntArray = intArrayOf(Color.YELLOW, Color.RED, Color.BLUE)
private val COLORS_THREE: IntArray = intArrayOf(Color.RED, Color.BLUE, Color.YELLOW)
private var mCurColor: ColorSelected = ColorSelected.BLUE
private enum class ColorSelected {
BLUE, YELLOW, RED
}
private fun getColors(colorSelected: ColorSelected): IntArray {
return when (colorSelected) {
ColorSelected.BLUE -> COLORS_TWO
ColorSelected.YELLOW -> COLORS_THREE
ColorSelected.RED -> COLORS_ONE
else -> COLORS_ONE
}
}
private fun drawCircles(canvas: Canvas) {
for (i in 0..2) {
val diff = mRad * mAngle + mRad * 120f * i
val circleX: Float = mDefDistance * Math.cos(diff).toFloat();
val circleY: Float = mDefDistance * Math.sin(diff).toFloat();
mPaint.color = getColors(mCurColor)[i]
canvas.drawCircle(circleX, circleY, mDefRadius, mPaint)
}
startAnimator()
}
var mAngle: Float = 0f
var mRotateAnimator: ValueAnimator? = null
var mRotateAnimator2: ValueAnimator? = null
var mTranslateAnimator: ValueAnimator? = null
var mTranslateAnimator2: ValueAnimator? = null
var mAnimTogether: AnimatorSet? = null
var mAnimTogether2: AnimatorSet? = null
var mAnimSequentially: AnimatorSet? = null
@TargetApi(Build.VERSION_CODES.O)
private fun startAnimator() {
// 旋转动画
if (mRotateAnimator == null) {
mRotateAnimator = ObjectAnimator.ofFloat(0f, 360f)
mRotateAnimator?.addUpdateListener { animation: ValueAnimator ->
mAngle = animation.getAnimatedValue() as Float
postInvalidate()
}
mRotateAnimator?.duration = 1000L
}
if (mRotateAnimator2 == null) {
mRotateAnimator2 = ObjectAnimator.ofFloat(360f, 720f)
mRotateAnimator2?.addUpdateListener { animation: ValueAnimator ->
mAngle = animation.getAnimatedValue() as Float
postInvalidate()
}
mRotateAnimator2?.duration = 1000L
}
// 平移动画
if (mTranslateAnimator == null) {
mTranslateAnimator = ObjectAnimator.ofFloat(mDefDistance, 0f)
mTranslateAnimator?.addUpdateListener { animation: ValueAnimator ->
mDefDistance = animation.getAnimatedValue() as Float
}
mTranslateAnimator?.duration = 1000L
}
if (mTranslateAnimator2 == null) {
mTranslateAnimator2 = ObjectAnimator.ofFloat(0f, mDefDistance)
mTranslateAnimator2?.addUpdateListener { animation: ValueAnimator ->
mDefDistance = animation.getAnimatedValue() as Float
}
mTranslateAnimator2?.duration = 1000L
}
if (mAnimTogether == null) {
mAnimTogether = AnimatorSet()
mAnimTogether?.playTogether(mRotateAnimator, mTranslateAnimator)
}
if (mAnimTogether2 == null) {
mAnimTogether2 = AnimatorSet()
mAnimTogether2?.playTogether(mRotateAnimator2, mTranslateAnimator2)
mAnimTogether2?.addListener(object : AnimatorListenerAdapter() {
override fun onAnimationStart(animation: Animator?) {
changeColor()
}
})
}
if (mAnimSequentially == null) {
mAnimSequentially = AnimatorSet()
mAnimSequentially?.playSequentially(mAnimTogether, mAnimTogether2)
mAnimSequentially?.start()
//动画循环
mAnimSequentially?.addListener(object : AnimatorListenerAdapter() {
override fun onAnimationEnd(animation: Animator?) {
mAnimSequentially?.start()
}
})
}
}
private fun changeColor() {
if (mCurColor == ColorSelected.BLUE)
mCurColor = ColorSelected.YELLOW
else if (mCurColor == ColorSelected.YELLOW)
mCurColor = ColorSelected.RED
else if (mCurColor == ColorSelected.RED)
mCurColor = ColorSelected.BLUE
}
fun cancle() {
mAnimSequentially?.cancel()
}
}
最后,来看看实现的效果吧
image当然了,你可以去改变大小和距离试试,不过我还是觉得这个效果有一点瑕疵,写的不够好,就这样吧。
网友评论