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

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="..." 一样

  • 首先,我们在 res/values 下新建一个 attrs.xml 文件,这个文件将用来储存我们的自定义View属性。

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

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 203,271评论 5 476
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 85,275评论 2 380
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 150,151评论 0 336
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,550评论 1 273
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,553评论 5 365
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,559评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,924评论 3 395
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,580评论 0 257
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,826评论 1 297
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,578评论 2 320
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,661评论 1 329
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,363评论 4 318
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,940评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,926评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,156评论 1 259
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 42,872评论 2 349
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,391评论 2 342

推荐阅读更多精彩内容