高仿微信底部導航欄動畫

轉載請註明出處: 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吧~

相關文章
相關標籤/搜索