Android 自定义 View

自定义 View 之联想手机 ZUI 系统加载动画

2019-08-29  本文已影响0人  威威喵丶

博主声明:

转载请在开头附加本文链接及作者信息,并标记为转载。本文由博主 威威喵 原创,请多支持与指教。

本文首发于此 博主威威喵 | 博客主页https://blog.csdn.net/smile_running

本次自定义 View 写的是一个仿联想手机 ZUI 系统加载动画的效果,前几天博主收到了 ZUI 11 的更新,发现更新了之后并没有大的改善,反而一直用的 touch 手势被改了,一时间不太习惯,老是滑错了,不得不说这个联想手机系统不怎么样。博主也是第一次买的联想手机,吐槽一下,哈哈。

不过呢,对 ZUI 系统的加载动画产生了兴趣,大家有用过联想手机的可能会注意到,它是三个小球在不停的旋转,并且移动到中心,合为一个。接着又分裂为三个,换了颜色,然后一直循环这样做。表达不清楚,直接来看效果图吧,以下是我的手机中的一段录制视频,随便找了一个蓝牙的搜索功能,在搜索时就会有这三个小球的加载动画,如下:

image

看到这个效果呢,首先当然是分析一波了。它是三个球旋转一圈,加上往中心聚合的动画,这两种动画一起执行,然后又开始发散,并且颜色也改变了。

首先呢,我们需要绘制三个小圆,这三个小圆每隔的角度都是一样的,也就是 120°,我们要得到小圆的坐标 x,y 值,就需要知道 c1 圆心与 r 半径,这两个都是我们自己设定的。来看下面这张图:

image

P 点就代表小圆的坐标,从上面看,利用三角函数公式,我们很容易就得出 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

当然了,你可以去改变大小和距离试试,不过我还是觉得这个效果有一点瑕疵,写的不够好,就这样吧。

上一篇 下一篇

猜你喜欢

热点阅读