高仿微信底部导航栏动画

转载请注明出处: juejin.im/post/5d5365…php

微信自发布以来,底部导航栏的动画一直让开发者津津乐道,并且伴随着版本更新,底部导航栏的动画也一直在改进。我最近在闲暇之余,看了下微信的底部导航栏动画,因而思考了下这个动画的原理,感受很是有意思,因而写下这篇文章。java

下图就是我实现的效果,你们能够对比下微信的效果,几乎能够以假乱真。android

微信底部动画

动画过程

关于这个动画的过程,我刚开始了是瞅了老半天了,由于若是咱们不了解动画的过程也是无从去实现了,因此动画过程很重要,这个动画其实有两个过程git

  1. 首先是默认图片的轮廓变色。
  2. 轮廓变色到必定程度后,整个图片出现了绿色的填充效果,也就是整个图片开始变绿,直到整个图片彻底变为了绿色。其实这是两个图片的透明度变换的达成的效果。

动画实现原理

首先咱们从总体上看,滑动的页面能够用ViewPager实现,在滑动的过程当中,经过监听ViewPager的滑动事件,能够获取一个滑动的比例值。github

底部的导航栏的4个Tab能够用自定义一个View来实现,我把这个自定义的View叫作TabView。那么,在滑动的过程当中,当前页面的TabView执行褪色动画,后一个页面执行变色动画。动画到底执行到哪一步,确定就是由ViewPager的滑动比例值决定的。所以TabView须要一个接收动画进度比例值的方法来控制动画的程度。微信

代码实现

俗话说得好,Talk is cheap, show me the code!。那咱们就经过代码来实现咱们以前的猜测吧,这确定是一段很是激情的旅程!ide

因为不想篇幅过大,所以我省略了ViewPager的一些样板代码,由于这些属于基本功。若是不会用ViewPager,在网上随便搜索就是一大堆的文章,很轻松就掌握了。那么本文主要就是解决如何自定义这个TabView函数

自定义View有不少方式,我相信不少人比我还懂。而我选择的是组合系统控件的方式来实现这个自定义View。那么可能有人问我,若是为了更好的绘制性能,能不能彻底的自定义一个View来实现呢?这固然是能够的,学完本文你就能够作这个牛逼的操做。然而,这点绘制性能的提高,其实在如今的高配置的手机上是能够忽略的。那么为了开发效率,组合系统控件应该是首选。布局

实现组合控件的布局

TabView须要的组合控件的布局以下post

// tab_layout.xml

<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android" android:layout_width="match_parent" android:layout_height="40dp" android:gravity="center_horizontal" android:orientation="vertical">

    <FrameLayout android:layout_width="wrap_content" android:layout_height="0dp" android:layout_weight="1">

        <ImageView android:id="@+id/tab_image" android:layout_width="wrap_content" android:layout_height="match_parent" />

        <ImageView android:id="@+id/tab_image_top" android:layout_width="wrap_content" android:layout_height="match_parent" />
    </FrameLayout>

    <TextView android:id="@+id/tab_title" android:layout_width="wrap_content" android:layout_height="wrap_content" android:textSize="12sp" />
</LinearLayout>
复制代码

布局的TextView确定是用来显示标题的,然而还有两个ImageView,为什么这样设计呢?这与咱们动画的实现有关。

@+id/tab_imageImageView在底部,它是用来显示一个默认的图片的,我称它为轮廓图,例如第一个页面的TabView的轮廓图以下

轮廓图片

咱们须要对这个轮廓进行变色处理,你们能够观察一下动画的过程,第一个过程很显然是轮廓的变色。

@+id/tab_image_topImageView在上面,它是用来显示一个页面被选中后的图片,也是动画最终要显示的图片,例如第一个页面的TabView的选中图片以下

选中图片

如今来讲明下如何用这个布局来实现动画。

  1. 首先全部的TabView都显示轮廓图,选中图都进行隐藏。如何隐藏呢,我选择使用透明度来隐藏选中图,由于整个动画过程有透明度的变换。
  2. 当滑动ViewPager的时候,TabView获取滑动的进度值,咱们就让轮廓图的轮廓开始变色。那么怎么变色呢,有一个很方便的方法,就是Drawable.setTint()方法。这个方法的原理就是PorterDuff.Mode.DST_IN混合模式。若是你们有兴趣,能够去研究下原理。
  3. ViewPager滑动到必定距离的时候,若是松开手指,页面会自动滑动到下一个页面,这个比例值究竟是多少呢?我暂时尚未考究,我假定是0.5吧。当滑动的比较超过0.5的时候,就要让轮廓图的透明度逐渐变是0,也就是慢慢地的看不见了,同时,选中图的透明度逐渐变为255,也是慢慢的清晰了。如此一来,就会出现轮廓图的总体颜色填充效果。

怎么样,实现思路是否是有点意思,那么咱们来根据这个思路来实现这个自定义ViewTabView吧。

实现TabView

加载布局

既然有了布局,那么首先用在TabView的构造函数中来加载这个布局

public TabView(Context context, @Nullable AttributeSet attrs) {
        super(context, attrs);
        // 加载布局
        inflate(context, R.layout.tab_layout, this);
}
复制代码

自定义属性与解析

为了更好的在XML布局中使用TabView,我为TabView抽取的自定义属性

// res/values/tabview_attrs.xml

<?xml version="1.0" encoding="utf-8"?>
<resources>
    <declare-styleable name="TabView">
        <attr name="tabColor" format="color|integer" />
        <attr name="tabImage" format="reference" />
        <attr name="tabSelectedImage" format="reference" />
        <attr name="tabTitle" format="string|reference" />
    </declare-styleable>
</resources>
复制代码

tabColor表明变色最终显示的颜色,这个颜色能够从选中图中用取色器获取。

tabImage表明默认显示的轮廓图。

tabSelectedImage表明选中后的图。

tabTitle表明要显示的标题。

有了这些自定义属性,那么在TabView中必需要解析这些自定义属性

public TabView(Context context, @Nullable AttributeSet attrs) {
        super(context, attrs);
        // 加载布局
        inflate(context, R.layout.tab_layout, this);

        TypedArray a = context.obtainStyledAttributes(attrs, R.styleable.TabView);
        for (int i = 0; i < a.getIndexCount(); i++) {
            int attr = a.getIndex(i);
            switch (attr) {

                case R.styleable.TabView_tabColor:
                    // 获取标题和轮廓最终的着色
                    mTargetColor = a.getColor(attr, DEFAULT_TAB_TARGET_COLOR);
                    break;

                case R.styleable.TabView_tabImage:
                    // 获取轮廓图
                    mNormalDrawable = a.getDrawable(attr);
                    break;

                case R.styleable.TabView_tabSelectedImage:
                    // 获取选中图
                    mSelectedDrawable = a.getDrawable(attr);
                    break;

                case R.styleable.TabView_tabTitle:
                    // 获取标题
                    mTitle = a.getString(attr);
                    break;
            }

        }
        a.recycle();
    }
复制代码

自定义属性解析完毕后,就须要给用这些属性值给控件进行初始化。ViewonFinishInflate()方法表明布局加载完成,所以在这里获取控件,并进行初始化。

@Override
    protected void onFinishInflate() {
        super.onFinishInflate();

        // 1.设置标题,默认着色为黑色
        mTitleView = findViewById(R.id.tab_title);
        mTitleView.setTextColor(DEFAULT_TAB_COLOR);
        mTitleView.setText(mTitle);

        // 2.设置轮廓图片,不透明,默认着色为黑色
        mNormalImageView = findViewById(R.id.tab_image);
        mNormalDrawable.setTint(DEFAULT_TAB_COLOR);
        mNormalDrawable.setAlpha(255);
        mNormalImageView.setImageDrawable(mNormalDrawable);

        // 3.设置选中图片,透明,默认着色为黑色
        mSelectedImageView = findViewById(R.id.tab_selected_image);
        mSelectedDrawable.setAlpha(0);
        mSelectedImageView.setImageDrawable(mSelectedDrawable);
    }
复制代码

标题设置了一个默认颜色DEFAULT_TAB_COLOR,是黑色。一样,也为轮廓图的轮廓设置黑色。轮廓图的透明度初始为255,也就是彻底可见,而选中图的透明度设置为0,也就是彻底不可见。全部这一切就是动画的初始状态。

控制动画进度

在前面的讲解动画的原理的时候说到一个事情,TabView须要使用ViewPager滑动进度值来控制动画的进度,所以还要为TabView定义一个接收进度值的方法。

/** * 根据进度值进行变色和透明度处理。 * * @param percentage 进度值,取值[0, 1]。 */
    public void setXPercentage(float percentage) {
        if (percentage < 0 || percentage > 1) {
            return;
        }

        // 1. 颜色变换
        int finalColor = evaluate(percentage, DEFAULT_TAB_COLOR, mTargetColor);
        mTitleView.setTextColor(finalColor);
        mNormalDrawable.setTint(finalColor);

        // 2. 透明度变换
        if (percentage >= 0.5 && percentage <= 1) {
            // 原理以下
            // 进度值: 0.5 ~ 1
            // 透明度: 0 ~ 1
            // 公式: percentage - 1 = (alpha - 1) * 0.5
            int alpha = (int) Math.ceil(255 * ((percentage - 1) * 2 + 1));
            mNormalDrawable.setAlpha(255 - alpha);
            mSelectedDrawable.setAlpha(alpha);
        } else {
            mNormalDrawable.setAlpha(255);
            mSelectedDrawable.setAlpha(0);
        }

        // 3. 更新UI
        invalidateUI();
    }
复制代码

在这个对外开放的接口中,首先咱们要根据进度值来计算轮廓要使用的颜色。起始颜色是黑色,最终颜色是一个绿色,而后咱们还有一个进度值,那么如何计算某个进度的对应的颜色值呢?其实在属性动画中有一个类,ArgbEvaluator,它提供了颜色的计算方法,代码以下

public Object evaluate(float fraction, Object startValue, Object endValue) {
        int startInt = (Integer) startValue;
        float startA = ((startInt >> 24) & 0xff) / 255.0f;
        float startR = ((startInt >> 16) & 0xff) / 255.0f;
        float startG = ((startInt >>  8) & 0xff) / 255.0f;
        float startB = ( startInt        & 0xff) / 255.0f;

        int endInt = (Integer) endValue;
        float endA = ((endInt >> 24) & 0xff) / 255.0f;
        float endR = ((endInt >> 16) & 0xff) / 255.0f;
        float endG = ((endInt >>  8) & 0xff) / 255.0f;
        float endB = ( endInt        & 0xff) / 255.0f;

        // convert from sRGB to linear
        startR = (float) Math.pow(startR, 2.2);
        startG = (float) Math.pow(startG, 2.2);
        startB = (float) Math.pow(startB, 2.2);

        endR = (float) Math.pow(endR, 2.2);
        endG = (float) Math.pow(endG, 2.2);
        endB = (float) Math.pow(endB, 2.2);

        // compute the interpolated color in linear space
        float a = startA + fraction * (endA - startA);
        float r = startR + fraction * (endR - startR);
        float g = startG + fraction * (endG - startG);
        float b = startB + fraction * (endB - startB);

        // convert back to sRGB in the [0..255] range
        a = a * 255.0f;
        r = (float) Math.pow(r, 1.0 / 2.2) * 255.0f;
        g = (float) Math.pow(g, 1.0 / 2.2) * 255.0f;
        b = (float) Math.pow(b, 1.0 / 2.2) * 255.0f;

        return Math.round(a) << 24 | Math.round(r) << 16 | Math.round(g) << 8 | Math.round(b);
    }
复制代码

熟悉属性动画的应该知道,参数float fraction的取值范围为0.f到1.f,因此能够把这个方法拷贝过来使用。

计算出颜色值后,就能够对标题和轮廓图着色了。

第二步,按照以前说的动画原理,当滑动的进度达到0.5后,要对轮廓图和选中图进行透明度的变换。

那么首先咱们得计算出某个进度对应的透明度。很明显,这是一道数学题,进度的变化范围是从0.5到1.0,透明度的变换取0到1.0(以后于乘以255便可获得实际的透明度)。透明度和进度的比例值是2,那么就能够得出一个公式alpha - 1 = (percentage - 1.0) * 2。有了这个公式,就能够算出任意进度值对应的透明度了。

这一切就绪后,咱们就使出杀手锏了,更新UI,让系统进行重绘。

与ViewPager联动

最重要的自定义View已经准备完毕,是时候来测试效果了。那么咱们必需要知道如何获取ViewPager的滑动进度值了,咱们能够为ViewPager设置滑动监听器

mViewPager.addOnPageChangeListener(new ViewPager.SimpleOnPageChangeListener() {
            @Override
            public void onPageScrolled(int position, float positionOffset, int positionOffsetPixels) {

            }
        });
复制代码

参数float positionOffset就是一个进度值,可是这个进度值使用起来仍是须要点小技巧的,咱们先从源码中看下解释

/** * This method will be invoked when the current page is scrolled, either as part * of a programmatically initiated smooth scroll or a user initiated touch scroll. * * @param position Position index of the first page currently being displayed. * Page position+1 will be visible if positionOffset is nonzero. * @param positionOffset Value from [0, 1) indicating the offset from the page at position. * @param positionOffsetPixels Value in pixels indicating the offset from position. */
        void onPageScrolled(int position, float positionOffset, int positionOffsetPixels);
复制代码

从注释中能够看出,onPageScrolled方法是在滑动的时候调用,参数position表明当前显示的页面,这表解释很容易产生误解,其实不管是从左边往右边滑动,仍是从右边往左边滑动,position始终表明左边的页面,所以position + 1始终表明右边的页面。

参数positionOffset表明滑动的进度值,而且还有很重要一点,大部分人都会忽略,若是参数positionOffset为非零值,表示右边的页面可见,也就是说,若是positionOffset的值是零,那么表明右边的页面是不可见的,这一点会在代码中体现出来。

既然已经对参数有所了解,那么如今来看看实现

public void onPageScrolled(int position, float positionOffset, int positionOffsetPixels) {
                // 左边View进行动画
                mTabViews.get(position).setXPercentage(1 - positionOffset);
                // 若是positionOffset非0,那么就表明右边的View可见,也就说明须要对右边的View进行动画
                if (positionOffset > 0) {
                    mTabViews.get(position + 1).setXPercentage(positionOffset);
                }
            }
复制代码

mTabViews是一个ArrayList,它保存了全部的TabView,咱们页面中有四个TabViewmTabViews.get(posistion)获取的是滑动时左边的页面,mTabViews.get(position + 1)获取的就是右边的页面。

当从左边向右边滑动的时候,左边页面的positionOffset的值是从0到1的,此时咱们须要左边的页面的TabView执行褪色动画。然而在咱们设计的TabView中,进度值达到1的时候,执行的是变色动画,而不是褪色动画,所以左边页面的TabView的进度取值要改变下,取1 - positionOffset。那么右边的页面的进度取值天然就是positionOffset了。

从右到左的滑动的原理其实与从左到右的滑动的原理是同样的,你们能够从Log中看出端倪。

然而,在为左边的TabView作动画的时候,咱们必定要确保有右边的页面存在。咱们前面讲解的时候说过,若是positionOffset为0的时候,右边的页面是不可见的,所以咱们要作一些排除的动做,这在代码中有体现的。

代码优化

  1. ViewPager能够自动滑动到下一个页面的进度值临界点是多少?TabView须要这个临界点来控制透明度的变换。
  2. TabView只能经过XML的属性来控制图片的显示,控制最终显色的颜色等等功能,其实这些能够经过代码动态控制,咱们能够实现一个对外的接口。

若是你们是个精益求精的人,能够对这两点进行考究和实现。

结束

本文把动画的原理,以及如何用代码实现这些原理讲解清楚了,这些都是关键部分。然而其它部分的代码我并无给出。为了方便想查看demo的人,我把代码上传到 github。所谓赠人玫瑰,手留余香,若是您以为代码写的还行,客官给个star或者fork吧~

相关文章
相关标签/搜索