轉載請註明出處: juejin.im/post/5d5365…php
微信自發布以來,底部導航欄的動畫一直讓開發者津津樂道,並且伴隨着版本更新,底部導航欄的動畫也一直在改進。我最近在閒暇之餘,看了下微信的底部導航欄動畫,因而思考了下這個動畫的原理,感受很是有意思,因而寫下這篇文章。java
下圖就是我實現的效果,你們能夠對比下微信的效果,幾乎能夠以假亂真。android
關於這個動畫的過程,我剛開始了是瞅了老半天了,由於若是咱們不瞭解動畫的過程也是無從去實現了,因此動畫過程很重要,這個動畫其實有兩個過程git
首先咱們從總體上看,滑動的頁面能夠用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_image
的ImageView
在底部,它是用來顯示一個默認的圖片的,我稱它爲輪廓圖,例如第一個頁面的TabView
的輪廓圖以下
咱們須要對這個輪廓進行變色處理,你們能夠觀察一下動畫的過程,第一個過程很顯然是輪廓的變色。
@+id/tab_image_top
的ImageView
在上面,它是用來顯示一個頁面被選中後的圖片,也是動畫最終要顯示的圖片,例如第一個頁面的TabView
的選中圖片以下
如今來講明下如何用這個佈局來實現動畫。
TabView
都顯示輪廓圖,選中圖都進行隱藏。如何隱藏呢,我選擇使用透明度來隱藏選中圖,由於整個動畫過程有透明度的變換。ViewPager
的時候,TabView
獲取滑動的進度值,咱們就讓輪廓圖的輪廓開始變色。那麼怎麼變色呢,有一個很方便的方法,就是Drawable.setTint()
方法。這個方法的原理就是PorterDuff.Mode.DST_IN
混合模式。若是你們有興趣,能夠去研究下原理。ViewPager
滑動到必定距離的時候,若是鬆開手指,頁面會自動滑動到下一個頁面,這個比例值究竟是多少呢?我暫時尚未考究,我假定是0.5吧。當滑動的比較超過0.5的時候,就要讓輪廓圖的透明度逐漸變是0,也就是慢慢地的看不見了,同時,選中圖的透明度逐漸變爲255,也是慢慢的清晰了。如此一來,就會出現輪廓圖的總體顏色填充效果。怎麼樣,實現思路是否是有點意思,那麼咱們來根據這個思路來實現這個自定義ViewTabView
吧。
既然有了佈局,那麼首先用在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();
}
複製代碼
自定義屬性解析完畢後,就須要給用這些屬性值給控件進行初始化。View
的onFinishInflate()
方法表明佈局加載完成,所以在這裏獲取控件,並進行初始化。
@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,讓系統進行重繪。
最重要的自定義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
,咱們頁面中有四個TabView
。mTabViews.get(posistion)
獲取的是滑動時左邊的頁面,mTabViews.get(position + 1)
獲取的就是右邊的頁面。
當從左邊向右邊滑動的時候,左邊頁面的positionOffset
的值是從0到1的,此時咱們須要左邊的頁面的TabView
執行褪色動畫。然而在咱們設計的TabView
中,進度值達到1的時候,執行的是變色動畫,而不是褪色動畫,所以左邊頁面的TabView
的進度取值要改變下,取1 - positionOffset
。那麼右邊的頁面的進度取值天然就是positionOffset
了。
從右到左的滑動的原理其實與從左到右的滑動的原理是同樣的,你們能夠從Log中看出端倪。
然而,在爲左邊的TabView
作動畫的時候,咱們必定要確保有右邊的頁面存在。咱們前面講解的時候說過,若是positionOffset
爲0的時候,右邊的頁面是不可見的,所以咱們要作一些排除的動做,這在代碼中有體現的。
ViewPager
能夠自動滑動到下一個頁面的進度值臨界點是多少?TabView
須要這個臨界點來控制透明度的變換。TabView
只能經過XML的屬性來控制圖片的顯示,控制最終顯色的顏色等等功能,其實這些能夠經過代碼動態控制,咱們能夠實現一個對外的接口。若是你們是個精益求精的人,能夠對這兩點進行考究和實現。
本文把動畫的原理,以及如何用代碼實現這些原理講解清楚了,這些都是關鍵部分。然而其它部分的代碼我並無給出。爲了方便想查看demo的人,我把代碼上傳到 github。所謂贈人玫瑰,手留餘香,若是您以爲代碼寫的還行,客官給個star或者fork吧~