Android仿雅虎新闻摘要加载视差动画效果

前言

本文参考辉哥的视差动画 - 雅虎新闻摘要加载,继续加练学习自定义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课程,最最重要的是对效果的分析,将效果拆分成一步步对应去实现,就会容易很多。

结语

如果以上文章对您有一点点帮助,希望您不要吝啬的点个赞加个关注,您每一次小小的举动都是我坚持写作的不懈动力!ღ( ´・ᴗ・` )

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值