高仿微信底部導航欄動畫 [復制鏈接]

2019-8-26 10:38
kengsirLi 閱讀:614 評論:0 贊:1
Tag:  導航欄 動畫

微信自發布以來,底部導航欄的動畫一直讓開發者津津樂道,而且伴隨著版本更新,底部導航欄的動畫也一直在改進。 最近有人問我,微信的最新版本的底部導航欄的動畫的原理是什么。閑暇之余,我仔細瞅了瞅最新版本的微信,底部的動畫非常可謂非常之有意思,這也是這篇文章的由來。

我想大家都安裝有微信,大家可以自己看看自己手機上微信的底部導航欄的動畫效果,然后再對比看看我實現的效果(如下圖),幾乎是一毛一樣。

高仿微信底部導航欄動畫

原理

首先,項目的架構是一個ViewPager加上底部導航欄,ViewPager的滑動可以產生一個滑動比例,底部導航欄根據這個比例值做相應的動畫。

那么,現在問題來了,底部導航欄如何實現。其實我們可以對底部導航欄的tab寫一個自定義View,這個自定義View可以接收一個進度值(ViewPager產生的滑動比例值)來做一些動畫。

實現

ViewPager的初始化代碼我就不展示了,這個是基本功了,本文主要展示底部的Tab如何自定義View。

布局

這個自定義View的名字叫做TabView, 我選擇讓它繼承自FrameLayout(繼承其他的ViewGroup控件也可以的),并且加載一個如下的組合控件布局

// 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>

android:id="@+id/tab_title"的TextView顯示標題。

android:id="@+id/tab_image"的ImageView顯示的是一個輪廓圖片,也就是未選中時候的圖片。

android:id="@+id/tab_image_top"的ImageView在android:id="@+id/tab_image"的ImageView之上,顯示的是選中時候的圖片。

輪廓圖片和選中的圖片,可以看下如下的圖片例子

高仿微信底部導航欄動畫

高仿微信底部導航欄動畫

第一圖片就是輪廓圖片,第二個圖片就是選中后的圖片。

為什么布局要這么設計呢?這當然是根據微信的動畫而設計的布局(廢話!),首先默認顯示的是輪廓圖片,當接收到一個進度值后,會讓輪廓圖片的輪廓變色,當進度值超過某個閾值的時候,讓輪廓圖片的透明度漸漸變為0(也就是完全透明,看不見),而讓選中的圖片的透明度漸漸變為255(也就是慢慢變清晰)。

TabView

既然知道了變色的原理,現在就來寫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();
}

當加載完布局后,需要為各個控件設置相應的屬性

 @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);
}

第二步中,為輪廓圖調用了Drawable.setTint()方法為輪廓著色,默認著色為黑色。

Drawable.setTint() 其實就是利用 PorterDuff.Mode.DST_IN 來進行顏色混合。

然后我還需要一個接收進度值(范圍為0.f 到 1.f)的方法,從而利用這個進度值做動畫

 /**
* 根據進度值進行變色和透明度處理。
*
* @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到1,所以可以把這個方法拷貝過來使用。

計算出顏色值后,就可以對標題和輪廓圖著色了。

第二步,按照之前說的動畫原理,要利用進度值計算透明度,然后分別對輪廓圖和選中圖設置透明度。透明度的計算原理已經在注釋中寫清楚了,這里不再贅述。

最后就是更新UI了。

與ViewPager聯動

一切準備就緒,就等一個ViewPager的進度值

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

既然想知道ViewPager如何提供進度值,那就必須了解onPageScrolled方法的幾個參數的意思,首先從源碼的注釋中進行了解

 /**
* 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,mTabViews.get(posistion)獲取的是左邊的頁面,mTabViews.get(position)獲取的是右邊的頁面。

當從左邊向右邊滑動的時候,左邊頁面的positionOffset的值是從0到1的。當從右邊到左邊滑動的時候,左邊頁面的positionOffset的值是從1到0的。 而我在設計TabView的時候,如果進度值是1就表示選中,這與positionOffset的值的變動范圍恰恰相反(我這個設計是否需要改進下?)。所以對左邊的頁面取的進度值就是1 - positionOffset,而對右邊頁面的進度值取的就是positionOffset。然而,右邊的頁面也有不可見的時候,那就是positionOffset為0的時候,這個時候就不需要對右邊的頁面執行動畫,這個處理很關鍵。


我來說兩句
您需要登錄后才可以評論 登錄 | 立即注冊
facelist
所有評論(0)
領先的中文移動開發者社區
18620764416
7*24全天服務
意見反饋:[email protected]

掃一掃關注我們

Powered by Discuz! X3.2© 2001-2019 Comsenz Inc.( 粵ICP備15117877號 )

阿拉斯加垂钓APP下载
股票涨跌由什么决定视频 黑龙江p62 7星彩 创利配资 最新股票指数 财神到配资 即时指数190bp 辽宁11选5 湖北十一选五 全球股票指数基金 短线股票推荐免费 股票推荐每日一股 微博 黑龙江11选5 双色球 股票融资融券买入是什么意思 股E融配资