【Android】从零实现一个美团同款底部导航栏波纹动画

2022-12-29  本文已影响0人  只有青山如洛

1. 实现一个自定义View

因为我们的动画需要自己来进行绘制,所以我们需要自定义 View

简单来说,自定义 View 是我们自己实现的一个继承于 View 的类。在实现后,我们就可以在 xml 文件中像调用正常的系统控件一样,来调用我们自己写的 View 啦。

1.1 实现自定义View的步骤:

1. 定义一个类,继承于 View

class FloatingTabView @JvmOverloads constructor(
    context: Context, attrs: AttributeSet? = null, defStyleAttr: Int = 0
) : View(context, attrs, defStyleAttr) {
}

附:@JvmOverloads传送门

2. 可以选择性的定义你的 View 中的自定义属性

在我们的类中自定义属性之后,这些属性可以在xml中直接使用,就像我们平时用 TextViewandroid:text="..." 一样

attrs.xml
接着,我们在 attrs.xml 中添加如下代码:
<?xml version="1.0" encoding="utf-8"?>
<resources>
    <declare-styleable name="FloatingTabViewStyle">
        <attr name="text_selected_color" format="color"/>
        <attr name="text_normal_color" format="color"/>
        <attr name="text_size" format="dimension"/>
        <attr name="lottie_path" format="string"/>
        <attr name="icon_normal" format="reference"/>
        <attr name="tab_selected" format="boolean"/>
        <attr name="tab_name" format="string"/>
    </declare-styleable>
</resources>

在上面这段代码中,我们为自定义View添加了很多的自定义属性,如 text_selected_color 等等,而这些属性整体的 style 名字叫做 FloatingTabViewStyle ,一会我们将用这个名字来在我们的代码中声明这些属性。
在代码中获取这些属性,我们需要用到 TypedArray
在我们的 类中添加如下代码:

init {
        val typedArray = context.obtainStyledAttributes(attrs, R.styleable.FloatingTabViewStyle)
        mIconNormal = typedArray.getDrawable(R.styleable.FloatingTabViewStyle_icon_normal)
        mAnimationPath = typedArray.getString(R.styleable.FloatingTabViewStyle_lottie_path)
        mTabName = typedArray.getString(R.styleable.FloatingTabViewStyle_tab_name)
        isSelected = typedArray.getBoolean(R.styleable.FloatingTabViewStyle_tab_selected, false)
        initAnimator()
    }

3. 可以选择性的获取自定义View的宽与高
获取自定义View的宽与高有很多方式,这里主要介绍一种最常用的,在 onMeasure 方法中获取。

override fun onMeasure(widthMeasureSpec: Int, heightMeasureSpec: Int) {
        super.onMeasure(widthMeasureSpec, heightMeasureSpec)
        screenWidth = measuredWidth.toFloat()   //获取自定义View的宽
        height = measuredHeight.toFloat()   //获取自定义View的高
}

1.2 绘制自定义View:

自定义 View 的绘制在 onDraw 方法中完成。在绘制过程中,我们主要用到三样工具:PaintCanvasPath

  1. 其中 Paint 代表画笔,需要我们自己进行初始化,比如我们可以设置画笔的颜色和线条宽度。
  2. CanvasonDraw 方法传入的参数,代表画板,是一种绘制时的规则,比如我们可以调用 canvas.drawRect() 来画一个矩形。 Canvas 的详细理解
  3. Path 代表画画的路径,定义了绘制的顺序 & 区域,一般用于绘制复杂图形(比如我们的波纹动画)。 Path的详细理解
    这里写一个简单的小例子,绘制一个紫色的矩形:
override fun onDraw(canvas: Canvas?) {
        super.onDraw(canvas)
        val paint = Paint()
        paint.color = R.color.purple_200
        paint.strokeWidth = 10f;
        canvas?.drawRect(0f, rectHeightTop, screenWidth, rectHeightBottom, paint);
}

2. 为自定义View加入动画效果

我们现在可以绘制图形了,那怎么让我们的图形动起来呢?
在这里,我们使用 ValueAnimator 来帮助我们进行动画处理。 ValueAnimator 是三种主要动画中的一种,具体的动画知识请移步 Carson带你学Android:这是一份全面&详细的动画知识学习攻略
如下代码所示的例子,我们首先声明一个 animator,通过 ValueAnimator.ofFloat 来指定它的值变化的范围,并且开启一个监听,让我们的 yVariance 时刻等于变化的值,这样就实现了让数值动起来

      val animator = ValueAnimator.ofFloat(startY.toFloat(), endY.toFloat())
        animator?.addUpdateListener { valueAnimator ->
            yVariance = valueAnimator.animatedValue as Float
            invalidate()
        }
        animator?.duration = ANIM_TIME.toLong()    // 设置一次动画持续时长
        animator?.repeatCount = ValueAnimator.INFINITE;    // 设置动画重复次数
        animator?.start()

2.1 实现会动的贝塞尔曲线

从网上找到一个类似的 贝塞尔曲线实现,其中他的实现思路是把整个半圆分成3段来实现,每段用一个控制点,这样会造成曲线之间的衔接不平滑。于是在他的实现思路上做了如下优化:
如下图所示,我们将一个半圆分成两半,每一半除了起点、终点外,各自有两个控制点。

贝塞尔曲线原理图
具体的代码实现如下图所示:
override fun onDraw(canvas: Canvas?) {
        super.onDraw(canvas)
        canvas?.drawRect(0f, rectHeightTop, screenWidth, rectHeightBottom, paint);
        val path = Path()
        path.moveTo((arcStartX + xVariance), rectHeightTop);
        path.cubicTo(
            (arcStartX + arcWidth / 7 + xVariance / 2).toFloat(),
            startY.toFloat() - abs(yVariance - startY) / 12,
            (arcStartX + arcWidth / 7 + xVariance / 4).toFloat(),
            yVariance.toFloat(),
            arcStartX + arcWidth / 2,
            yVariance.toFloat()
        );
        path.cubicTo(
            (arcStartX + arcWidth * 6 / 7 - xVariance / 4).toFloat(),
            yVariance.toFloat(),
            (arcStartX + arcWidth * 6 / 7 - xVariance / 2).toFloat(),
            startY.toFloat() - abs(yVariance - startY) / 12,
            arcStartX + arcWidth - xVariance,
            rectHeightTop
        );
        canvas?.drawPath(path, paint);
    }

2.2 控制动画速度

控制动画速度方面我们用到了插值器。 详细了解插值器

因为想要实现一个回弹的效果,在研究了系统自带插值器之后发现,BounceInterpolator 比较接近想要的效果。在对 BounceInterpolator 的源码进行研究后发现,BounceInterpolator 的弹跳曲线如下图所示:

BounceInterpolator的弹跳曲线
而想要实现的效果是弹到中间点A后,再迅速弹到最高点B,最终下降到中间点A。所以,我们需要对插值器进行自定义构建。

2.3 实现自定义插值器

实现自定义插值器,我们只需要构建一个类,让其继承于 TimeInterpolator 类,并实现其中的 getInterpolation 方法。在 getInterpolation 方法中,传入的参数 input 范围在 0~1 之间,代表整个动画运动的过程。我们可以针对动画运动的不同阶段,来为其返回不同的运动速度。如下方代码所示,在运动的前半段和后半段,我们采用了不同的运动速度。具体的动画实现效果大家可以自由设定和发挥。

class ValueChangeInterpolator : TimeInterpolator{
        override fun getInterpolation(input: Float): Float {
            var result = 0f
            if(input <= 0.5){
                result = ((sin(Math.PI * input)) / 2).toFloat();
            }
             else {
                result = ((2 - sin(Math.PI * input)) / 2).toFloat()
            }
            return result
        }
    }

参考文档:
https://www.runoob.com/w3cnote/android-advance-custom-view.html
https://blog.csdn.net/mChenys/article/details/50408819
https://blog.csdn.net/carson_ho/article/details/60598775
https://segmentfault.com/a/1190000000721127
https://www.jianshu.com/p/2c19abde958c
https://www.jianshu.com/p/53759778284a
https://www.jianshu.com/p/2f19fe1e3ca1

上一篇 下一篇

猜你喜欢

热点阅读