前言
本章主要用原生的方式實作一個菜單頁面,主要用到的知識點為位移動畫,我們可以先看看效果。

高斯模糊的菜單效果圖.gif
分析
高斯模糊背景
我們的菜單背景是一個高斯模糊的背景,雖然看上去高大上,但是不要被吓到了,實作原理非常的簡單:截取目前螢幕轉換為bitmap,将bitmap進行高斯模糊,然後設定為菜單的背景。
當然,還有另外一種實作方式就是讓UI設計師切一張高斯模糊模糊的透明背景圖,看看UI設計師會不會打死你。
哒哒
菜單跳動
- 這個效果看上去雖然是複雜,但是不要被吓到了。其實也就是先用一個RelativeLayout作為根布局,将裡面的每個menu逐個的布局,排列好。
- 打開菜單的時候,使用translationY動畫逐個逐個的将menu從螢幕外移動到原來的位置,而從第0個menu開始,後面的每個menu根據下标延遲啟動動畫。
- 第1個比第0個延遲開始動畫160毫秒*
- 第2個比第1個延遲開始動畫260毫秒*
- 第3個比第2個延遲開始動畫360毫秒*
- 關閉菜單,從第0個進行translationY動畫将menu逐個移出到螢幕外。從下标遞增,不斷的延遲動畫的開始。
- 第0個延遲開始動畫430毫秒,并且在動畫結束關閉彈出*
- 第1個比第0個延遲開始動畫330毫秒*
- 第2個比第1個延遲開始動畫230毫秒*
- 第3個比第2個延遲開始動畫130毫秒*
底部圓形菜單
動畫
也就是這個東西
底部菜單
,菜單打開的時候使用rotation動畫從45度到180度旋轉,而關閉的時候則是使用rotation動畫從180度到45度旋轉。
如何實作突出
ViewGroup有個屬性是clipChildren,設定為false則代表目前ViewGroup對超出高度的子view不進行裁切
實作
底部Tab欄
首先是底部的tab欄實作,沒什麼好講的。根布局使用FrameLayout,然後又LineaLayout排列圖示。
<?xml version="1.0" encoding="utf-8"?>
<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_gravity="bottom|end"
android:background="#00000000"
android:clipChildren="false">
<LinearLayout
android:layout_width="match_parent"
android:layout_height="48dip"
android:layout_alignParentBottom="true"
android:background="@color/colorPrimary"
android:orientation="horizontal">
<RelativeLayout
android:id="@+id/rl_menu_home"
style="@style/MainMenuLinearStyle">
<ImageView
android:id="@+id/iv_menu_home"
style="@style/MainMenuImageStyle"
android:src="@drawable/menu_home_select" />
<TextView
android:id="@+id/tv_menu_home"
style="@style/MainMenuTextStyle"
android:layout_below="@id/iv_menu_home"
android:text="首頁" />
</RelativeLayout>
<RelativeLayout
android:id="@+id/rl_menu_project"
style="@style/MainMenuLinearStyle">
<ImageView
android:id="@+id/iv_menu_project"
style="@style/MainMenuImageStyle"
android:src="@drawable/menu_project_select" />
<TextView
android:id="@+id/tv_menu_project"
style="@style/MainMenuTextStyle"
android:layout_below="@id/iv_menu_project"
android:text="項目" />
</RelativeLayout>
<android.support.design.widget.FloatingActionButton
android:id="@+id/fab_all_menu"
android:layout_width="50dp"
android:layout_height="50dp"
android:layout_gravity="bottom"
android:layout_marginBottom="-15dp"
android:rotation="45"
android:src="@drawable/ic_clear" />
<RelativeLayout
android:id="@+id/rl_menu_client"
style="@style/MainMenuLinearStyle">
<ImageView
android:id="@+id/iv_menu_client"
style="@style/MainMenuImageStyle"
android:src="@drawable/menu_client_select" />
<TextView
android:id="@+id/tv_menu_client"
style="@style/MainMenuTextStyle"
android:layout_below="@id/iv_menu_client"
android:text="招商" />
</RelativeLayout>
<RelativeLayout
android:id="@+id/rl_menu_my"
style="@style/MainMenuLinearStyle">
<ImageView
android:id="@+id/iv_menu_my"
style="@style/MainMenuImageStyle"
android:src="@drawable/menu_my_select" />
<TextView
android:id="@+id/tv_main_home"
style="@style/MainMenuTextStyle"
android:layout_below="@id/iv_menu_my"
android:text="我的" />
</RelativeLayout>
</LinearLayout>
</FrameLayout>
看看效果吧:
底部tab欄
布局整個菜單
這一步也沒什麼大的難度,就是在布局每個menu的時候确定位置是比較繁瑣的,選擇RelativeLayout的原因是因為層級,所有的menu都處于同一個層級中,進行位移動畫的時候才不會被裁切。
另外需要的是在底部再增加一個和TAB欄一樣的圓形按鈕,用于占位和關閉菜單。
來看布局吧:
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:id="@+id/rl_more_menu_root"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:background="#90FFFFFF"
android:clickable="true"
android:focusable="true"
android:gravity="center_horizontal"
android:orientation="vertical">
<!--背景占位-->
<FrameLayout
android:id="@+id/rl_bg"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:alpha="0.3"
android:background="@color/white"
/>
<RelativeLayout
android:id="@+id/rl_menu_warp"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:layout_alignParentBottom="true"
android:layout_centerHorizontal="true"
android:gravity="bottom|center_horizontal">
<TextView
android:id="@+id/tv_new_action"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginRight="28dp"
android:clickable="true"
android:drawablePadding="@dimen/dp_8"
android:drawableTop="@mipmap/new_action_icon"
android:gravity="center_horizontal"
android:paddingBottom="@dimen/more_window_item_margin"
android:text="建立行動"
android:textColor="#666"/>
<TextView
android:id="@+id/tv_new_order"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginLeft="28dp"
android:layout_marginRight="28dp"
android:layout_toRightOf="@+id/tv_new_action"
android:clickable="true"
android:drawablePadding="@dimen/dp_8"
android:drawableTop="@mipmap/new_order_icon"
android:gravity="center_horizontal"
android:paddingBottom="@dimen/more_window_item_margin"
android:text="建立訂單"
android:textColor="#666"/>
<TextView
android:id="@+id/tv_client_input"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginLeft="28dp"
android:layout_toRightOf="@+id/tv_new_order"
android:clickable="true"
android:drawablePadding="@dimen/dp_8"
android:drawableTop="@mipmap/client_input_icon"
android:gravity="center_horizontal"
android:paddingBottom="@dimen/more_window_item_margin"
android:text="客戶錄入"
android:textColor="#666"/>
<TextView
android:id="@+id/tv_ranking_listcon"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_below="@+id/tv_new_action"
android:layout_marginRight="28dp"
android:layout_marginTop="20dp"
android:clickable="true"
android:drawablePadding="@dimen/dp_8"
android:drawableTop="@mipmap/ranking_listcon"
android:gravity="center_horizontal"
android:paddingBottom="@dimen/more_window_item_margin"
android:text="琅琊榜"
android:textColor="#666"/>
<TextView
android:id="@+id/tv_import_mail_icon"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_below="@+id/tv_new_order"
android:layout_marginLeft="28dp"
android:layout_marginRight="28dp"
android:layout_marginTop="20dp"
android:layout_toRightOf="@+id/tv_ranking_listcon"
android:clickable="true"
android:drawablePadding="@dimen/dp_8"
android:drawableTop="@mipmap/import_mail_icon"
android:gravity="center_horizontal"
android:paddingBottom="@dimen/more_window_item_margin"
android:text="通訊錄導入"
android:textColor="#666"/>
<TextView
android:id="@+id/tv_scan_card_icon"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_below="@+id/tv_client_input"
android:layout_marginBottom="140dp"
android:layout_marginLeft="28dp"
android:layout_marginTop="20dp"
android:layout_toRightOf="@+id/tv_import_mail_icon"
android:clickable="true"
android:drawablePadding="@dimen/dp_8"
android:drawableTop="@mipmap/scan_card_icon"
android:gravity="center_horizontal"
android:paddingBottom="@dimen/more_window_item_margin"
android:text="掃名片"
android:textColor="#666"/>
</RelativeLayout>
<!--打開與關閉的菜單-->
<android.support.design.widget.FloatingActionButton
android:id="@+id/fab_close_more_menu"
android:layout_width="50dp"
android:layout_height="50dp"
android:layout_alignParentBottom="true"
android:layout_centerHorizontal="true"
style="@style/DialogTextStyle"
android:layout_marginBottom="-15dp"
android:background="@color/white"
android:rotation="45"
android:src="@drawable/ic_clear"/>
</RelativeLayout>
運作起來看看效果吧:
布局效果
行了,初步完成,接下來就是為tab的快捷菜單增加點選時間,然後進行動畫部分的編寫了。
編寫打開動畫
将menu布局gone起來,再做下一步。雖然有點水字數的嫌疑,但是我還是要放出來:
使用translationY動畫逐個逐個的将menu從螢幕外移動到原來的位置,而從第0個menu開始,後面的每個menu根據下标延遲啟動動畫。
- 第1個比第0個延遲開始動畫160毫秒*
- 第2個比第1個延遲開始動畫260毫秒*
- 第3個比第2個延遲開始動畫360毫秒*
感動羞澀
來看代碼部分的編寫吧:
/**
* 打開動畫
*/
private void showAnimation() {
// 擷取子view的個數,進行周遊
for (int i = 0; i < rlMenuWrap.getChildCount(); i++) {
// 擷取到相應的子view
final View child = rlMenuWrap.getChildAt(i);
// 關閉按鈕和背景進行跳過
if (child.getId() == R.id.fab_close_more_menu || child.getId() == R.id.rl_bg) {
continue;
}
// 先暫時隐藏
child.setVisibility(View.INVISIBLE);
mHandler.postDelayed(new Runnable() {
@Override
public void run() {
//顯示菜單
child.setVisibility(View.VISIBLE);
// Y軸位移動畫 從800移動到目前位置
ValueAnimator fadeAnim = ObjectAnimator.ofFloat(child, "translationY", 600, 0);
//動畫時長
fadeAnim.setDuration(200);
// 自定義內插補點器
KickBackAnimator kickAnimator = new KickBackAnimator();
//設定速度
kickAnimator.setDuration(100);
// 設定內插補點器
fadeAnim.setEvaluator(kickAnimator);
//開始動畫
fadeAnim.start();
}
}, i * 60);// 根據下标延遲執行動畫
}
}
代碼裡面用到了一個自定義內插補點器,内容如下:
public class KickBackAnimator implements TypeEvaluator<Float> {
private final float s = 1.70158f;
float mDuration = 0f;
public void setDuration(float duration) {
mDuration = duration;
}
public Float evaluate(float fraction, Float startValue, Float endValue) {
float t = mDuration * fraction;
float b = startValue.floatValue();
float c = endValue.floatValue() - startValue.floatValue();
float d = mDuration;
float result = calculate(t, b, c, d);
return result;
}
public Float calculate(float t, float b, float c, float d) {
return c * ((t = t / d - 1) * t * ((s + 1) * t + s) + 1) + b;
}
}
接下來就是調用showAnimation方法了,在調用之前先setVisibility,來看看效果吧:
高斯模糊菜單01.gif
為了能夠更好的顯示效果,故意增加了動畫的時長,好了關于打開動畫就這樣完成,接下來就是關閉動畫的編寫了。
編寫關閉動畫
這裡不水了,關閉動畫與打開動畫基本上是一緻的,其他就兩點差別:
- 延遲動畫的時間是相反的
- 第0個動畫完成需要關閉菜單
/**
* 關閉動畫
*
*/
private void closeAnimation() {
// 擷取所有的子view
for (int i = 0; i < rlMenuWrap.getChildCount(); i++) {
final View child = rlMenuWrap.getChildAt(i);
// 判斷 關閉按鈕和背景不進入動畫
if (child.getId() == R.id.fab_close_more_menu || child.getId() == R.id.rl_bg) {
continue;
}
mHandler.postDelayed(new Runnable() {
@Override
public void run() {
// 顯示
child.setVisibility(View.VISIBLE);
// Y軸位移動畫 從目前位置到600
ValueAnimator fadeAnim = ObjectAnimator.ofFloat(child, "translationY", 0, 600);
fadeAnim.setDuration(200);
KickBackAnimator kickAnimator = new KickBackAnimator();
kickAnimator.setDuration(100);
fadeAnim.setEvaluator(kickAnimator);
fadeAnim.start();
fadeAnim.addListener(new BaseAnimatorListener() {
@Override
public void onAnimationEnd(Animator animation) {
// 動畫完成後隐藏menu
child.setVisibility(View.INVISIBLE);
}
});
}
}, (rlMenuWrap.getChildCount() - i - 1) * 30);// 設定和打開相反的延遲時長
//第0個
if (i == 0) {
mHandler.postDelayed(new Runnable() {
@Override
public void run() {
// 隐藏menu
rlMoreMenuRoot.setVisibility(View.GONE);
}
}, (rlMenuWrap.getChildCount() - i) * 30 + 80);
}
}
}
看看效果吧:
高斯模糊菜單02.gif
此緻,菜單相關的效果也就完成了,剩下的就是高斯模糊和底部圓形菜單的旋轉了。
高斯模糊和旋轉
高斯模糊
高斯模糊就是一個工具方法而已,沒什麼其它出彩的地方,拿到截圖後用高斯模糊算法工具方法進行模糊算法,這部分就不放出來了,以免有水字數的嫌疑。可以直接在底部點選源碼進行檢視。
旋轉
其實就是一個簡單的方法,先放出來吧:
/**
* 旋轉菜單按鈕
*/
private void rotationActionMenu(int from, int to) {
ValueAnimator fadeAnim = ObjectAnimator.ofFloat(fabCloseMoreMenu, "rotation", from, to);
fadeAnim.setDuration(300);
KickBackAnimator kickAnimator = new KickBackAnimator();
kickAnimator.setDuration(150);
fadeAnim.setEvaluator(kickAnimator);
fadeAnim.start();
}
在打開和關閉的時候傳入不同的值即可。
效果
最後,來看看相關的效果圖,再放一次。

才能夠本章可以看出,任何複雜的效果,都能通過拆分成小功能來實作。
未完待續、敬請期待!
我的部落格位址FullScreenDeveloper
源碼位址