美文网首页Android 自定义 View
自定义 View 之联想手机 ZUI 系统加载动画

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

作者: 威威喵丶 | 来源:发表于2019-08-29 11:53 被阅读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

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

    相关文章

      网友评论

        本文标题:自定义 View 之联想手机 ZUI 系统加载动画

        本文链接:https://www.haomeiwen.com/subject/kaxiectx.html