前言部分
CoordinatorLayout這個view出現已經很久了,今天看到知乎上的一個神奇聯動的效果,類似的效果在很多應用上都有使用,以前雖然經常看到但是沒有實際去查看如何實現的。這幾天特意去查看資料學習其中的方式,但是由於能力有限和資料不是很全面只學到了一個皮毛。在此做一個記錄,也算是這幾天的努力的小總結吧。
說到CoordinatorLayout我們最先試用過的AppBarLayout和CollapsingToolbarLayout實現摺疊頭部。或者實現視差效果等等。。這裏我都不做介紹了文章很多,實現的效果也很棒,最主是不需要我們自己子們處理邏輯,系統都幫我們實現好了。
我們雖然使用過,但是其實並沒有去深入瞭解過實現的原理是什麼,今天就通過實現一個簡單的效果來學習一下自定義的Behavior。
實現的就是類似知乎等一些閱讀類app等效果,如下入:
事實上我們做這些工作是爲了更好的實現view之間的協調聯動,在沒有CoordinatorLayout輔助view協調滑動之前,我們都是通過parent來對觸摸事件進行分發,但這這樣做就需要我們對事件的傳遞有很深刻的理解,因爲事件的傳遞是從外由內的,但是事件消耗確實從內向外,parent響應事件後在傳遞給內部的子view,子view可以決定是否消耗事件,但是這也有一個問題,就是子view消耗事件後,其他的view都不會在收到事件的信息。所以協調多個子view聯動就變得很麻煩。
下面一些詳細的講解可以去連接看看大佬的介紹,我也是讀了文章開始自己寫一個小 demo練手的。
上面文章中有個總結很棒,這裏直接貼出來。
自定義 Behavior 的兩種目的
我們可以按照兩種目的來實現自己的 Behavior
,當然也可以兩種都實現啦
- 某個 view 監聽另一個 view 的狀態變化,例如大小、位置、顯示狀態等
- 某個 view 監聽
CoordinatorLayout
內的NestedScrollingChild
的接口實現類的滑動狀態
實例編寫
最終目標實現:
頭部view、尾部view和列表綁定,當列表處於初始位置的時候,下拉列表實現頭部view同時拉出,上拉列表的時候實現頭部view隱藏。尾部view的規則是列表上拉時候隱藏,列表下拉的時候出現。
第一步實現如下圖:
public class RecyclerTitleBehavior extends CoordinatorLayout.Behavior<RecyclerView> {
public RecyclerTitleBehavior() {
}
public RecyclerTitleBehavior(Context context, AttributeSet attrs) {
super(context, attrs);
}
@Override
public boolean layoutDependsOn(CoordinatorLayout parent, RecyclerView child, View dependency) {
return dependency instanceof TextView;
}
@Override
public boolean onDependentViewChanged(CoordinatorLayout parent, RecyclerView child, View dependency) {
//計算列表y座標,最小爲0
float y = dependency.getTranslationY();
if (y > dependency.getHeight()) {
y = dependency.getHeight();
} else if (y < 0) {
y = 0;
}
child.setY(y);
return true;
}
}
這個比較簡單,主要是通過依賴頭部view來改變自己的位置。如上面所說的兩種方式一種:RecyclerView是負責監控頭部view的別緻變化。頭部view發生位移變化時候,自身也作出相應的位移變化。
-
-
這一步實現頭部view監聽CoordinatorLayout的直接子 view的滑動,RecyclerView之所以作爲子view可以被監聽,因爲其內部是實現了NestedScrollingChild2接口,所以當發生滑動的時候會通過NestedScrollingChildHelper調用父view的onStartNestedScroll接口(前提是父 view實現了NestedScrollingParent2接口),CoordinatorLayout作爲父view可以接收到它滑動的信息,然後通過循環遍歷childCount獲取Behavior,通過Behavior來把滑動信息發送給所有的子view(這裏父 view也可以自己選擇接受滑動,這樣會優先子 view滑動)。其他子view如果依賴這個view則實現其他子 view的滑動;
-
同時其他的子 view(不可滑動的View默認是沒有實現NestedScrollingChild2接口)通過設置Behavior也實現了NestedScrollingChild2接口,通過Behavior來接收父view的滑動事件回調。如果子view發生滑動後,也會通過接口的形式回掉到NestedScrollingParent2接口中,然後父view決定自己是否配合子view的滑動,然後父view把自己滑動的情況反饋給子view,這時候子view再從新計算滑動的距離,在進行滑動。
-
如上所述,基本就是子view滑動,告訴一下父view我要滑動了,父view首先考慮一下自己要不要滑動,然後再告訴其它的子view有個子view滑動了,你們誰關注它了想一起動啊,然後把父 view自己滑動得結果告訴滑動的子view,讓他重新計算一下距離,然後滑動剩下的距離。
public class TextBehavior extends CoordinatorLayout.Behavior<TextView> { /** * 上邊界 */ private boolean isUpLimit = false; /** * 下邊界 */ private boolean isDownLimit = false; public TextBehavior() { } public TextBehavior(Context context, AttributeSet attrs) { super(context, attrs); } @Override public boolean onInterceptTouchEvent(@NonNull CoordinatorLayout parent, @NonNull TextView child, @NonNull MotionEvent ev) { switch (ev.getAction()) { case MotionEvent.ACTION_DOWN: isUpLimit = false; isDownLimit = false; default: break; } return super.onInterceptTouchEvent(parent, child, ev); } @Override public boolean onStartNestedScroll(@NonNull CoordinatorLayout coordinatorLayout, @NonNull TextView child, @NonNull View directTargetChild, @NonNull View target, int axes, int type) { return (axes & ViewCompat.SCROLL_AXIS_VERTICAL) != 0; } @Override public void onNestedPreScroll(@NonNull CoordinatorLayout coordinatorLayout, @NonNull TextView child, @NonNull View target, int dx, int dy, @NonNull int[] consumed, int type) { super.onNestedPreScroll(coordinatorLayout, child, target, dx, dy, consumed, type); //dy 下 爲 負數 && 上 爲 正數 if (target instanceof RecyclerView) { RecyclerView recyclerView = (RecyclerView) target; LinearLayoutManager layoutManager = (LinearLayoutManager) recyclerView.getLayoutManager(); int firstPosition = layoutManager.findFirstCompletelyVisibleItemPosition(); float translationY = child.getTranslationY() - dy; // LogUtil.d("translationY===" + translationY); // child.setTranslationY(translationY); // if (firstPosition == 0 && translationY == 0) { // isUpLimit = true; // } // if (firstPosition == 0 && canScroll(dy)) { if (translationY > child.getHeight()) { translationY = child.getHeight(); isUpLimit = false; isDownLimit = true; } else if (translationY < 0) { translationY = 0; isUpLimit = true; isDownLimit = false; } child.setTranslationY(translationY); consumed[1] = dy; } } } private boolean canScroll(int dy) { if (isUpLimit && dy > 0) { return false; } if (isDownLimit && dy < 0) { return false; } return true; } }
基本上就這樣,這也是醉簡單的滑動效果了。
-
下面我們在添加一個聯動的子view
我們添加一個尾部的View,它也監聽另外的一個View(就是RecyclerView了),如果View滑動了一段距離,尾部的View也會有對應的動作。代碼如下:
/**
* @Author : dongfang
* @Created Time : 2019-05-28 15:08
* @Description:
*/
public class TextBottomBehavior extends CoordinatorLayout.Behavior<View> {
/**
* 上邊界
*/
private boolean isUpLimit = false;
/**
* 下邊界
*/
private boolean isDownLimit = false;
public TextBottomBehavior() {
}
public TextBottomBehavior(Context context, AttributeSet attrs) {
super(context, attrs);
}
@Override
public boolean onInterceptTouchEvent(@NonNull CoordinatorLayout parent, @NonNull View child, @NonNull MotionEvent ev) {
switch (ev.getAction()) {
case MotionEvent.ACTION_DOWN:
isUpLimit = false;
isDownLimit = false;
default:
break;
}
return super.onInterceptTouchEvent(parent, child, ev);
}
@Override
public boolean onStartNestedScroll(@NonNull CoordinatorLayout coordinatorLayout, @NonNull View child, @NonNull View directTargetChild, @NonNull View target, int axes, int type) {
return (axes & ViewCompat.SCROLL_AXIS_VERTICAL) != 0;
}
@Override
public void onNestedPreScroll(@NonNull CoordinatorLayout coordinatorLayout, @NonNull View child, @NonNull View target, int dx, int dy, @NonNull int[] consumed, int type) {
super.onNestedPreScroll(coordinatorLayout, child, target, dx, dy, consumed, type);
//dy 下 爲 負數 && 上 爲 正數
if (target instanceof RecyclerView) {
float translationY = child.getTranslationY() + dy;
LogUtil.d("translationY===" + child.getTranslationY() + "finalY==" + translationY);
if (canScroll(dy)) {
if (translationY < -child.getHeight()) {
translationY = -child.getHeight();
isUpLimit = false;
isDownLimit = true;
} else if (translationY > 0) {
translationY = 0;
isUpLimit = true;
isDownLimit = false;
}
child.setTranslationY(translationY);
// consumed[1] = dy;
}
}
}
private boolean canScroll(int dy) {
if (isUpLimit && dy > 0) {
return false;
}
if (isDownLimit && dy < 0) {
return false;
}
return true;
}
}
這個和上個基本是一致的,只是運動的方向是反的。
還有一點要注意,我們的被監聽的view只是跟隨頭部的view滑動,並不跟隨尾部View滑動
結尾部分
鑑於能力有限,寫的不是很清楚的話,希望大家指出,如果有更好的資料也請給我留言。
final 謝謝