Material Design系列,自定義Behavior實現Android知乎首頁
版權聲明:轉載必須註明本文轉自嚴振杰的博客: http://blog.csdn.net/yanzhenjie1003
友情連接:
Material Design博客專欄
系列博客:
1. Material Design系列,Behavior之BottomSheetBehavior與BottomSheetDialog
2. Material Design系列,Behavior之SwipeDismissBehavior
3. Material Design系列,自定義Behavior之上滑顯示返回頂部按鈕
4. Material Design系列,自定義Behavior實現Android知乎首頁
5. Material Design系列,自定義Behavior支持所有 View
本博客目的:仿知乎首頁向上滑動時動畫隱藏Toolbar
、FlocationActionButton
、Tab導航
,下滑時顯示,如果和你的期望不同,那麼你可以不需要看了,免的浪費你的寶貴時間噢。
效果預覽
知乎效果:
本博客實現效果:
今天效果的源代碼下載鏈接在文章末尾。
實現分析
這個效果其實並不難實現,但是它的用處很大,當用戶手指上滑,屏幕上顯示下方內容的時候,隱藏Toolbar
、Tab導航
、FAB
來騰出更大的空間顯示內容,讓用戶爽。簡單粗暴,但這就是我們的目的。
首先就是頭部的Toolbar
,這個就不用說了吧,基本會,不會的人隨便看我一篇博客的demo都有這個效果,簡直小學級別。
其次來看看FAB(FlocationActionButton)的顯示和隱藏,知乎是用的平移,我們這裏做個優化改動,當然平移也是可以的,如果你看過我的Material Design系列,自定義Behavior之上滑顯示返回頂部按鈕這篇博客的話。那麼我們的FAB的動畫隱藏和顯示也是用上一篇博客的原理,沒有看上一篇博客的同學需要回過頭看看噢,這裏不在贅述。
最後來看下面的Tab導航的隱藏和顯示,這個確確實實用平移更好是吧,然而相信你如果看過我Material Design系列,Behavior之BottomSheetBehavior與BottomSheetDialog這篇博客的話,這個效果實現起來也不難。強烈建議看下文之前讀這篇文章,不然真的沒法繼續看下去了。
其實代碼量還是很少的,主要是Behavior
原理、Behavior
和CoordinatorLayout
如何結合使用。so,強烈建議去上讀下上面兩篇博客噢。
……
好的,五分鐘過去了,我相信你大概已經速讀了上面提到的兩篇博客了。那麼在第一篇FAB的那篇博客中實現的效果是手指向上滑時(屏幕顯示下方的內容時)顯示FAB用來回到頂部,但是這裏剛好是相反的:向上滑時隱藏FAB。如果你認真讀了原理解釋的那一段或者運行過demo,這個效果so easy吧。第二篇博客中也講到了如何隱藏和顯示這個Tab導航
,那麼有的同學就覺得今天的博客就結束了吧?答案當然是No了,不然我也不會再開一篇博客來講這個了。
爲什麼呢?還是有難點的,難點在哪裏?就是上面講到的兩個Behavior
如何和CoordinatorLayout
結合使用,同時實現兩個效果。而且BottomSheetBehavior
隱藏和顯示Tab導航
這個裏面之前我們使用Button
來控制的,如何做到`CoordinatorLayout
中的ContentView
滑動時動態的顯示和隱藏Tab導航
呢?
擼碼
閒扯裝逼都弄完了,下來來點真材實料,帶領大家一起代碼擼起來。
頁面佈局
上面的引文和介紹,我們已經知道了FAB的顯示和隱藏用自定義Behavior
實現,Tab導航
用BottomSheetBehavior
來實現,那麼我們佈局文件也該問世了:
<android.support.design.widget.CoordinatorLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:layout_width="match_parent"
android:layout_height="match_parent">
<android.support.design.widget.AppBarLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:theme="@style/AppTheme.AppBarOverlay">
<android.support.v7.widget.Toolbar
android:id="@+id/toolbar"
android:layout_width="match_parent"
android:layout_height="?attr/actionBarSize"
app:layout_scrollFlags="scroll|enterAlways|snap"
app:popupTheme="@style/AppTheme.PopupOverlay" />
</android.support.design.widget.AppBarLayout>
<android.support.v7.widget.RecyclerView
android:id="@+id/recyclerView"
android:layout_width="match_parent"
android:layout_height="match_parent"
app:layout_behavior="@string/appbar_scrolling_view_behavior" />
<LinearLayout
android:id="@+id/tab_layout"
android:layout_width="match_parent"
android:layout_height="?actionBarSize"
android:layout_alignParentBottom="true"
android:background="@android:color/white"
app:layout_behavior="@string/bottom_sheet_behavior">
<Button
android:layout_width="0dp"
android:layout_height="match_parent"
android:layout_weight="1"
android:text="第一" />
<Button
android:layout_width="0dp"
android:layout_height="match_parent"
android:layout_weight="1"
android:text="第二" />
<Button
android:layout_width="0dp"
android:layout_height="match_parent"
android:layout_weight="1"
android:text="第三" />
<Button
android:layout_width="0dp"
android:layout_height="match_parent"
android:layout_weight="1"
android:text="第四" />
</LinearLayout>
<android.support.design.widget.FloatingActionButton
android:id="@+id/fab"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="bottom|end"
android:layout_marginBottom="70dp"
android:layout_marginEnd="16dp"
android:layout_marginRight="16dp"
android:src="@mipmap/ic_action_new"
app:layout_behavior="@string/scale_down_show_behavior"
app:layout_scrollFlags="scroll|enterAlways|snap" />
</android.support.design.widget.CoordinatorLayout>
還是稍微解釋下,內容區域是一個RecyclerView
,使用的Behavior
是design的ScrollingViewBehavior
:
app:layout_behavior="@string/appbar_scrolling_view_behavior"
然後一個LinearLayout
,使用的Behavior
是design的BottomSheetBehavior
:
app:layout_behavior="@string/bottom_sheet_behavior"
最後一個FloatingActionButton
,使用我們的自定義ScaleDownShowBehavior
:
app:layout_behavior="@string/scale_down_show_behavior"
其他兩個都是design自帶的,唯有FloatingActionButton
的ScaleDownShowBehavior
需要我們自定義,那麼下面我們就來實現下ScaleDownShowBehavior
。
自定義Behavior實現FAB的動畫控制
這裏又談到了自定義Behavior
了,首先就來實現:用戶手指在屏幕上滑,隱藏FAB,留出更多位置給用戶。
這裏還是繼承FloatingActionButton.Behavior
:
public class ScaleDownShowBehavior extends FloatingActionButton.Behavior {
public ScaleDownShowBehavior(Context context, AttributeSet attrs) {
super();
}
}
這裏我們的滑動方向還是不變,監聽豎着方向的滑動:
@Override
public boolean onStartNestedScroll(CoordinatorLayout coordinatorLayout, ...) {
return nestedScrollAxes == ViewCompat.SCROLL_AXIS_VERTICAL;
}
那麼我們就要稍微改一下我們的開始滑動時回調這個方法了:onNestedScroll()
:
@Override
// 隱藏動畫是否正在執行
private boolean isAnimatingOut = false;
public void onNestedScroll(CoordinatorLayout coordinatorLayout, FloatingActionButton child,
View target, int dxConsumed, int dyConsumed, int dxUnconsumed, int dyUnconsumed) {
if ((dyConsumed > 0 || dyUnconsumed > 0) && !isAnimatingOut
&& child.getVisibility() == View.VISIBLE) {// 手指上滑,隱藏FAB
AnimatorUtil.scaleHide(child, listener);
} else if ((dyConsumed < 0 || dyUnconsumed < 0) && child.getVisibility() != View.VISIBLE) {
AnimatorUtil.scaleShow(child, null);// 手指下滑,顯示FAB
}
}
private ViewPropertyAnimatorListener listener = new ViewPropertyAnimatorListener() {
@Override
public void onAnimationStart(View view) {
isAnimatingOut = true;
}
@Override
public void onAnimationEnd(View view) {
isAnimatingOut = false;
view.setVisibility(View.GONE);
}
@Override
public void onAnimationCancel(View arg0) {
isAnimatingOut = false;
}
};
好吧,代碼非常少,完成了。那麼我們就在string.xml中定義好,剛纔我們引用的變量@string/scale_down_show_behavior
:
<string name="scale_down_show_behavior">com.yanzhenjie.definebehavior.behavior.ScaleDownShowBehavior</string>
啊呀,好激動呀,我趕緊運行一下。但是但是。。。運行後發現見鬼啊,只有FAB會跟着顯示和隱藏,完全看不到Tab導航呀
,嚴振杰你是在忽悠人麼?哈哈哈哈,且聽我細細道來。
通過監聽ScaleDownShowBehavior中的view顯示/隱藏來控制Tab導航欄
其實只要看過Material Design系列,Behavior之BottomSheetBehavior與BottomSheetDialog這篇文章的同學會發現,用BottomSheetBehavior
的控件默認都是隱藏起來的,需要我們去調用它的方法來控制它的View
的顯示。所以我們這裏需要在CoordinatorLayout
中的ContentView
滾動的時候來調用BottomSheetBehavior
的方法使它依附的View
顯示與隱藏。
那麼我們發現ScaleDownShowBehavior
被系統自動調用了,也觸發了View
的隱藏和顯示,CoordinatorLayout
這貨沒有給我們自動調用BottomSheetBehavior
,我們怎麼辦?如果你沒有忘記的話,我們自定義ScaleDownShowBehavior
的時候,在onNestedScroll()
方法中有個地方是去調用了FAB的顯示和隱藏,所以我們在這裏加一個回調監聽,讓外部可以監聽到它的動作,是不是同時可以控制BottomSheetBehavior
了?如果還沒有嚮明的話看代碼。
先在ScaleDownShowBehavior
中定一個Listener
:
// 外部監聽顯示和隱藏。
public interface OnStateChangedListener {
void onChanged(boolean isShow);
}
然後在ScaleDownShowBehavior
的onNestedScroll()
方法中回調:
private OnStateChangedListener mOnStateChangedListener;
public void setOnStateChangedListener(OnStateChangedListener mOnStateChangedListener) {
this.mOnStateChangedListener = mOnStateChangedListener;
}
@Override
public void onNestedScroll(CoordinatorLayout coordinatorLayout, FloatingActionButton child,
View target, int dxConsumed, int dyConsumed, int dxUnconsumed, int dyUnconsumed) {
if ((dyConsumed > 0 || dyUnconsumed > 0) && !isAnimatingOut
&& child.getVisibility() == View.VISIBLE) {//往下滑
AnimatorUtil.scaleHide(child, viewPropertyAnimatorListener);
if (mOnStateChangedListener != null) {
mOnStateChangedListener.onChanged(false);
}
} else if ((dyConsumed < 0 || dyUnconsumed < 0) && child.getVisibility() != View.VISIBLE) {
AnimatorUtil.scaleShow(child, null);
if (mOnStateChangedListener != null) {
mOnStateChangedListener.onChanged(true);
}
}
}
好完美啊。來來來,設置一個監聽。。。我勒個去,突然發現怎麼從FAB拿到這個Behavior
啊?且看我下面的分析,保證讓你柳暗花明又一村啊。
拿到FAB的Behavior對象,通過監聽控制BottomSheetBehavior的View的顯示/隱藏
我們這知道,給一個View
設置Behavior
對象的時候是在xml中設置,所以Behavior
是一個View
的LayoutParams
屬性吧?哈哈哈明白了吧,然後Behavior
又必須和CoordinatorLayout
結合使用,不然也是扯淡,so,這個View
也必須是CoordinatorLayout
的子View,所以在ScaleDownShowBehavior
中產生了如下的一個靜態方法(爲了方便閱讀,提示寫了中文):
public static <V extends View> ScaleDownShowBehavior from(V view) {
ViewGroup.LayoutParams params = view.getLayoutParams();
if (!(params instanceof CoordinatorLayout.LayoutParams)) {
throw new IllegalArgumentException("這個View不是CoordinatorLayout的子View");
}
CoordinatorLayout.Behavior behavior = ((CoordinatorLayout.LayoutParams) params).getBehavior();
if (!(behavior instanceof ScaleDownShowBehavior)) {
throw new IllegalArgumentException("這個View的Behaviro不是ScaleDownShowBehavior");
}
return (ScaleDownShowBehavior) behavior;
}
所以我們在Activity
中:
private BottomSheetBehavior mBottomSheetBehavior;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.zhihu_main);
ScaleDownShowBehavior scaleDownShowFab = ScaleDownShowBehavior.from(FAB);
scaleDownShowFab.setOnStateChangedListener(onStateChangedListener);
mBottomSheetBehavior = BottomSheetBehavior.from(findViewById(R.id.tab_layout));
}
private OnStateChangedListener onStateChangedListener = new OnStateChangedListener() {
@Override
public void onChanged(boolean isShow) {
mBottomSheetBehavior.setState(
isShow ? BottomSheetBehavior.STATE_EXPANDED
: BottomSheetBehavior.STATE_COLLAPSED);
}
};
哎喲喂,不知不覺中已經把我們的效果實現了,這裏最重要的就是onStateChangedListener
了,這裏實現了Tab導航
的隱藏和顯示,它的狀態是從ScaleDownShowBehavior
中回調出來的。
頁面初始化好後顯示Tab導航
我們上文中說道,添加了BottomSheetBehavior
屬性的View,默認是隱藏的,所以我們在頁面初始化時要把我們的Tab導航
顯示出來:
private boolean initialize = false;
@Override
public void onWindowFocusChanged(boolean hasFocus) {
super.onWindowFocusChanged(hasFocus);
if (!initialize) {
initialize = true;
mBottomSheetBehavior.setState(BottomSheetBehavior.STATE_EXPANDED);
}
}
Ok,今天的博客就擼完了,本例源碼傳送門。
版權聲明:轉載必須註明本文轉自嚴振杰的博客: http://blog.csdn.net/yanzhenjie1003