@[CoordinatorLayout, Bahavior]
CoordinatorLayout是android support design包中可以算是最重要的一個東西,運用它可以做出一些不錯的特效,而其中的難點就是Bahavior的使用
- 拖動效果實例
- CoordinatorLayout和Bahavior解析
- 關于NestScrolling機制
拖動效果實例
上述效果圖中上部是一個LinearLayout中含有的一個EditText從而做成的一個簡單的搜索框,而下部是一個ListView。我們想當ListView往上拉動(查看更多)的時候隱藏搜索框,而向下拉動(往回看)的時候展現搜索框。
首先看下布局
<android.support.design.widget.CoordinatorLayout
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical">
<FrameLayout
android:layout_width="match_parent"
android:layout_height="match_parent">
<com.qianmi.adminapp.lib.utils.widget.BaseSwipeRefreshLayout
android:id="@+id/swipeRefreshLayout"
android:layout_width="match_parent"
android:layout_height="match_parent">
<ListView
android:id="@+id/listview"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:dividerHeight="1px"
android:scrollbars="vertical"
tools:listitem="@layout/item_member_list"
/>
<TextView
android:id="@+id/empty"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center"
android:textColor="#444"
android:textSize="12sp"
android:visibility="gone"
/>
</com.qianmi.adminapp.lib.utils.widget.BaseSwipeRefreshLayout>
</FrameLayout>
<LinearLayout
android:id="@+id/search_pane"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical"
app:layout_behavior="com.xxx.common.behavior.SearchScrollBehavior"
>
<View
android:id="@+id/tab_margin"
android:layout_width="match_parent"
android:layout_height="@dimen/fit_status_bar_height"
android:background="@color/toolbar_background_color"
android:orientation="vertical"
/>
<LinearLayout
android:id="@+id/search_bg"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:background="@color/toolbar_background_color"
android:gravity="center"
android:paddingBottom="10dp"
android:paddingLeft="@dimen/content_margin"
android:paddingRight="@dimen/content_margin"
android:paddingTop="10dp">
<EditText
android:id="@+id/et_search"
style="@style/search_edit_view_style"
android:layout_width="0dp"
android:layout_weight="1"
android:clickable="true"
android:gravity="left|center_vertical"
android:hint="@string/member_list_search_hint"
android:imeOptions="actionSearch"
android:inputType="textPersonName"/>
</LinearLayout>
</LinearLayout>
</android.support.design.widget.CoordinatorLayout>
涉及到本例子的圖片、文字等資源的地方不需要關注,我們主要關注這幾個地方:
- 首先根布局是一個 CoordinatorLayout
- 其次id為search_pane的這個LinearLayout整體就是我們的搜索框,也就是最終需要根據滑動而顯示/隱藏的部分
- 這個搜索框(R.id.search_pane)他必須是直接為CoordinatorLayout的一級child view,從而才能實現上述效果
- 在這個搜索框中添加了一個屬性app:layout_behavior="com.xxx.common.behavior.SearchScrollBehavior",SearchScrollBehavior實際是一個我們自定義的Behavior,而這里需要指定它的完全限定名(后面將會詳細說明該如何寫這個自定義Behavior)
java代碼部分基本上和傳統的listview的實現沒有任何區別,這里就不再貼出代碼。而唯獨需要注意的一點是必須要子啊設置完畢ListView后添加這句代碼:
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
listview.setNestedScrollingEnabled(true);
}
后面將會說明加上這句話的原因。當然如果你用的是RecyclerView而不是ListView,那么就不用添加這句代碼。
CoordinatorLayout和Bahavior解析
剛解除CoordinatorLayout是從Google的官方的demo里,demo使用了CoordinatorLayout作為布局根節點,同時使用了appbar、tablayout、fab等控件,做出了一個聯動效果的demo,很是漂亮。但是剛開始對于其深層次的用法,從來沒有深究。
翻開CoordinatorLayout的源碼,發現它其實就是一個ViewGroup,確切來說可以看做為一個FrameLayout。
使用CoordinatorLayout主要出于兩種目的:
- 用于應用界面的根布局
- 作為一個容器用來協調子View之間的交互
說到CoordinatorLayout就不得不說Behavior這個類,他其實是CoordinatorLayout的一個內部抽象類
public static abstract class Behavior<V extends View> {
public boolean onStartNestedScroll(CoordinatorLayout coordinatorLayout,
V child, View directTargetChild, View target, int nestedScrollAxes) {
return false;
}
public void onNestedPreScroll(CoordinatorLayout coordinatorLayout, V child, View target,
int dx, int dy, int[] consumed) {
// Do nothing
}
public boolean onDependentViewChanged(CoordinatorLayout parent, V child, View dependency) {
return false;
}
public void onDependentViewRemoved(CoordinatorLayout parent, V child, View dependency) {
}
}
上面代碼中列出了我們在自定義Behavior時需要復寫的幾個方法(大部分情況),根據需要,一般分為兩類:
- 某個view監聽另一個view的狀態變化,例如大小、位置、顯示狀態等
- 某個view監聽CoordinatorLayout里的滑動狀態
對于第一種情況,一個例子就是當現實SnackBar的時候,Fab同樣的隨著Snackbar向上彈出而向上平移。這里不再舉例。
對于第二種i情況,我們需要復寫前兩個方法,泛型V實際制定的是使用該Behavior的View,本例我們在search_pane這個LinearLayout中通過app:layout_behavior將該自定義Behavior加了進來,實際上這個V就是該搜索框。而onStartNestedScroll和onNestedPreScroll中的child即該搜索框。
onStartNestedScroll一般用于滑動開始之前調用,一般用來計算一些child的尺寸,同時指定我們的Behavior感興趣的滑動方向;onNestedPreScroll用來實時的監聽滑動狀態,根據狀態從而對child做出特定的響應。對于我們的例子來說,如果ListView往上拉動(查看更多)的時候隱藏搜索框,而向下拉動(往回看)的時候展現搜索框。
本例的SearchScrollBehavior代碼為:
public class SearchScrollBehavior extends CoordinatorLayout.Behavior<View> {
private int searchPaneHeight = 0;//搜索框的高度
private boolean isAnimate;//動畫是否在進行
private Context ctx;
public SearchScrollBehavior(Context context, AttributeSet attrs) {
super(context, attrs);
ctx = context;
}
@Override
public boolean onStartNestedScroll(CoordinatorLayout coordinatorLayout, View child, View directTargetChild, View target, int nestedScrollAxes) {
if (child.getVisibility() == View.VISIBLE && searchPaneHeight == 0) {
//獲取控件高度
searchPaneHeight = DensityUtils.dp2px(ctx, child.getHeight());
}
return (nestedScrollAxes & ViewCompat.SCROLL_AXIS_VERTICAL) != 0;//判斷是否豎直滾動
}
@Override
public void onNestedPreScroll(CoordinatorLayout coordinatorLayout, View child, View target, int dx, int dy, int[] consumed) {
//dy大于0是向上滾動 小于0是向下滾動
if (dy >= 0 && !isAnimate && child.getVisibility() == View.VISIBLE) {
showSearch(child, false);
} else if (dy < 0 && !isAnimate && child.getVisibility() == View.GONE) {
showSearch(child, true);
}
}
private void showSearch(final View searchPane, boolean visible) {
if (searchPane == null) {
return;
}
float translationY = searchPane.getTranslationY();
if (!visible) {
//show -> hide
if (translationY < 0 || isAnimate) {
return;
}
L.d("hide, searchPaneHeight" + searchPaneHeight);
ViewPropertyAnimator animator = searchPane.animate()
.translationY(-searchPaneHeight)
.setDuration(800)
.setInterpolator(new DecelerateInterpolator())
.setListener(new Animator.AnimatorListener() {
@Override
public void onAnimationStart(Animator animation) {
isAnimate = true;
}
@Override
public void onAnimationEnd(Animator animation) {
searchPane.setVisibility(View.GONE);
isAnimate = false;
}
@Override
public void onAnimationCancel(Animator animation) {
showSearch(searchPane, true);
}
@Override
public void onAnimationRepeat(Animator animation) {
}
});
animator.start();
} else {
//hide -> show
if (translationY >= 0 || isAnimate) {
return;
}
L.d("show, searchPaneHeight" + searchPaneHeight);
ViewPropertyAnimator animator = searchPane.animate()
.translationY(0)
.setDuration(500)
.setInterpolator(new DecelerateInterpolator())
.setListener(new Animator.AnimatorListener() {
@Override
public void onAnimationStart(Animator animation) {
searchPane.setVisibility(View.VISIBLE);
isAnimate = true;
}
@Override
public void onAnimationEnd(Animator animation) {
isAnimate = false;
}
@Override
public void onAnimationCancel(Animator animation) {
showSearch(searchPane, false);
}
@Override
public void onAnimationRepeat(Animator animation) {
}
});
animator.start();
}
}
}
這樣子,我們把自定義的Behavior寫到搜素框的xml布局中進行指定一下,不需額外的代碼,就實現了本文開頭的效果。
關于NestScrolling機制
那么CoordinatorLayout是如何實現通過Behavior來控制子View的狀態的呢?這里必須要說到NestedScrolling機制。
在support-v4包中有這兩個接口:
- NestedScrollingParent
- NestedScrollingChild
CoordinatorLayout實現了NestedScrollingParent這個接口,而RecylerView實現了NestedScrollingChild這個接口
public class CoordinatorLayout extends ViewGroup implements NestedScrollingParent {
}
public class RecyclerView extends ViewGroup implements ScrollingView, NestedScrollingChild {
}
我們知道View的事件傳遞是從子到父的,我們看下實現了NestedScrollingChild接口的RecyclerView 的onTouchEvent是怎么寫的,在onTouchEvent的ACTION_MOVE分支,調用了他自己的dispatchNestedPreScroll方法(這個是NestedScrollingChild接口的一個方法,被RecyclerView 實現了)
case MotionEvent.ACTION_MOVE: {
...
if (dispatchNestedPreScroll(dx, dy, mScrollConsumed, mScrollOffset)) {
...
}
...
@Override
public boolean dispatchNestedPreScroll(int dx, int dy, int[] consumed, int[] offsetInWindow) {
return getScrollingChildHelper().dispatchNestedPreScroll(dx, dy, consumed, offsetInWindow);
}
dispatchNestedPreScroll的實現中實際是調用NestedScrollingChildHelper的dispatchNestedPreScroll方法,看下NestedScrollingChildHelper的該方法的實現:
public boolean dispatchNestedPreScroll(int dx, int dy, int[] consumed, int[] offsetInWindow) {
if (isNestedScrollingEnabled() && mNestedScrollingParent != null) {
if (dx != 0 || dy != 0) {
int startX = 0;
int startY = 0;
if (offsetInWindow != null) {
mView.getLocationInWindow(offsetInWindow);
startX = offsetInWindow[0];
startY = offsetInWindow[1];
}
if (consumed == null) {
if (mTempNestedScrollConsumed == null) {
mTempNestedScrollConsumed = new int[2];
}
consumed = mTempNestedScrollConsumed;
}
consumed[0] = 0;
consumed[1] = 0;
ViewParentCompat.onNestedPreScroll(mNestedScrollingParent, mView, dx, dy, consumed);
if (offsetInWindow != null) {
mView.getLocationInWindow(offsetInWindow);
offsetInWindow[0] -= startX;
offsetInWindow[1] -= startY;
}
return consumed[0] != 0 || consumed[1] != 0;
} else if (offsetInWindow != null) {
offsetInWindow[0] = 0;
offsetInWindow[1] = 0;
}
}
return false;
}
當發現該子view的isNestedScrollingEnabled被設置的話(RecylerView是開啟的,而繼承自AbsListview的ListView是關閉的,所以如果使用Behavior的控件是ListView的話,必須要setNestedScrollingEnabled(true)),就調用ViewParentCompat.onNestedPreScroll方法,將滑動的位置傳遞過去。
而ViewParentCompat就是包含了這個子View(RecyclerView)的父親布局,所以很顯然就是CoordinatorLayout這個ViewGroup了(這里也就說明了第一節所說的使用Behavior的控件必須是直接從屬于CoordinatorLayout的原因),而onNestedPreScroll這個方法就是NestedScrollingParent接口的,CoordinatorLayout恰恰實現了這個方法。
那么現在有點眉目了,事件是這樣子傳遞的,當我們滑動RecyclerView的時候,事件會通過NestScrolling機制傳給CoordinatorLayout,這是第一步。那么CoordinatorLayout怎么通過Behavior將事件繼續傳遞給它的子View呢。肯定就要看下CoordinatorLayout的onNestedPreScroll中寫了什么:
public void onNestedPreScroll(View target, int dx, int dy, int[] consumed) {
int xConsumed = 0;
int yConsumed = 0;
boolean accepted = false;
final int childCount = getChildCount();
for (int i = 0; i < childCount; i++) {
final View view = getChildAt(i);
final LayoutParams lp = (LayoutParams) view.getLayoutParams();
if (!lp.isNestedScrollAccepted()) {
continue;
}
final Behavior viewBehavior = lp.getBehavior();
if (viewBehavior != null) {
mTempIntPair[0] = mTempIntPair[1] = 0;
viewBehavior.onNestedPreScroll(this, view, target, dx, dy, mTempIntPair);
xConsumed = dx > 0 ? Math.max(xConsumed, mTempIntPair[0])
: Math.min(xConsumed, mTempIntPair[0]);
yConsumed = dy > 0 ? Math.max(yConsumed, mTempIntPair[1])
: Math.min(yConsumed, mTempIntPair[1]);
accepted = true;
}
}
consumed[0] = xConsumed;
consumed[1] = yConsumed;
if (accepted) {
dispatchOnDependentViewChanged(true);
}
}
CoordinatorLayout是找出了所有的child view,通過getBehavior一個個的獲得child view的Behavior然后直接執行Behavior的onNestedPreScroll方法,將dx/dy等數據以及這個child view本身傳遞進去。到了這里,我們也就執行到了我們自定義的SearchScrollBehavior
的onNestedPreScroll方法里面去了。從而拿到了dx和dy。dy大于0是向上滾動 小于0是向下滾動,隨后對自己(child view)進行響應。
這里要提一下int[] consumed
這個數據,它是一個int型的數組,長度為2,第一個元素是父view消費的x方向的滾動距離;第二個元素是父view消費的y方向的滾動距離,如果這兩個值不為0,則子view需要對滾動的量進行一些修正。正因為有了這個參數,使得我們處理滾動事件的時候,思路更加清晰,不會像以前一樣被一堆的滾動參數搞混。
說到這里,好像一直沒講NestScrolling機制,可以參考下這篇博客,有一個很詳細的講解
https://segmentfault.com/a/1190000002873657
可以說CoordinatorLayout的Behavior機制完全離不開NestScrolling!