前言: 並不是熱淚盈眶才叫青春,也不是莽撞熱血才叫年輕。不忘初心,便始終都是年輕。多少人把放縱當熱血,並把早熟和自律當做陳腐來嬉笑。歲月還未過多流逝之前,他們的身體和精神就已經被掏空,提早告別了青春。
一、概述
上一篇文章分析了RecyclerView的滑動原理,依然是由onTouchEvent()
觸控事件響應的,最終通過遍歷所有子View,每個子View調用了底層View的offsetTopAndBottom()
或者offsetLeftAndRight()
方法來實現滑動的。不同的是RecyclerView採用嵌套滑動機制,會把滑動事件通知給支持嵌套滑動的父View先做決定。那麼什麼是嵌套滑動呢?RecyclerView是怎麼處理嵌套滑動的呢?
什麼是嵌套滑動?
我們來看看一個RecyclerView的滑動效果圖:
這就是嵌套滑動的效果,當一個View產生嵌套滑動事件時,會先通知他的父View,詢問父View是否處理這個事件,如果處理那麼子View不處理,如果不處理那麼子View處理,實際上父View只處理部分滑動距離的情況。可以看到由下往上滑動RecyclerView的時候,先處理頭部的滑動事件,然後才處理RecyclerView自身的滑動事件,由上往下滑時,也是先詢問父View是否處理滑動事件,如果不處理則交給RecyclerView自身的處理滑動事件。
<?xml version="1.0" encoding="utf-8"?>
<androidx.coordinatorlayout.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:fitsSystemWindows="true">
<com.google.android.material.appbar.AppBarLayout
android:id="@+id/app_bar"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:fitsSystemWindows="true">
<com.google.android.material.appbar.CollapsingToolbarLayout
android:id="@+id/toolbar_layout"
android:layout_width="match_parent"
android:layout_height="match_parent"
app:contentScrim="#FF5722"
app:layout_scrollFlags="scroll|exitUntilCollapsed|snap">
<ImageView
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:src="@mipmap/flower"
android:scaleType="centerCrop"
app:layout_scrollFlags="scroll|enterAlwaysCollapsed" />
<androidx.appcompat.widget.Toolbar
android:id="@+id/toolbar"
android:layout_width="match_parent"
android:layout_height="40dp"
app:layout_collapseMode="pin"
app:title="二級標題" />
</com.google.android.material.appbar.CollapsingToolbarLayout>
</com.google.android.material.appbar.AppBarLayout>
<androidx.recyclerview.widget.RecyclerView
android:id="@+id/recyclerView"
android:layout_width="match_parent"
android:layout_height="match_parent"
app:layout_behavior="@string/appbar_scrolling_view_behavior" />
</androidx.coordinatorlayout.widget.CoordinatorLayout>
這種高大上的視覺交互效果叫做“協調者佈局”,實現這種效果的核心類就是一個CoordinatorLayout,它遵循Material Design風格,結合AppbarLayout,CollapsingToolbarLayout等可以產生各種炫酷的摺疊懸浮效果。
二、CoordinatorLayout與Behavior
照着上面的xml佈局,效果是完成了,但是你是否有很多問號,CoordinatorLayout是啥,幹什麼的?AppBarLayout有什麼用?CollapsingToolbarLayout又是啥?上面的佈局控件除了ImageView,Toolbar,RecyclerView這幾個控件外,其他的我基本不認識。
這是官網CoordinatorLayout的解析截圖,它是一個超級的FrameLayout,注意它繼承自ViewGroup,並沒有繼承FrameLayout,然後實現了NestedScrollingParent接口。
CoordinatorLayout可以作爲一個容器與一個或多個子View進行特定的交互。通過指定Behaviors爲子視圖,可以在單個父視圖中提供許多不同的交互,這些視圖可以彼此交互。
Behavior是CoordinatorLayout的子視圖的交互行爲插件,一個行爲實現了一個或多個用戶可以在子視圖上進行的交互。這些交互可能包括拖動,滑動,甩動或任何其他手勢。
插件也就代表如果一個子View需要某種交互,它就需要加載對應的Behavior,否則它就是不具備交互能力的。而Behavior是一個抽象類,它的實現類都是爲了讓用戶作用在一個View上面進行拖拽、滑動、快速滑動等手勢操作。如果你需要其他的交互動作,則需要自定義Behavior。
但是,我們有了解到Behavior的真正含義嗎?它到底是具體幹什麼的?
前面內容有講過,CoordinatorLayout可以定義與它子View的交互或某些子View之間的交互。先來看看Behavior的源碼:
public static abstract class Behavior<V extends View> {
public Behavior() {}
public Behavior(Context context, AttributeSet attrs) {}
public boolean layoutDependsOn(CoordinatorLayout parent, V child, View dependency) {
return false;
}
public boolean onDependentViewChanged(CoordinatorLayout parent, V child, View dependency) {
return false;
}
public void onDependentViewRemoved(CoordinatorLayout parent, V child, View dependency) {
}
public boolean onStartNestedScroll(CoordinatorLayout coordinatorLayout, View child, View directTargetChild,
View target, int axes, int type) {
return false;
}
public void onNestedScrollAccepted(CoordinatorLayout coordinatorLayout, View child, View directTargetChild,
View target, int axes, int type) {
// Do nothing
}
public void onStopNestedScroll(CoordinatorLayout coordinatorLayout, V child, View target, int type) {
// Do nothing
}
public void onNestedScroll(CoordinatorLayout coordinatorLayout, View child, View target, int dxConsumed,
int dyConsumed, int dxUnconsumed, int dyUnconsumed, int type) {
// Do nothing
}
public void onNestedPreScroll(CoordinatorLayout coordinatorLayout, View child, View target, int dx,
int dy, int[] consumed, int type) {
// Do nothing
}
public boolean onNestedFling(CoordinatorLayout coordinatorLayout, View child, View target,
float velocityX, float velocityY, boolean consumed) {
return false;
}
public boolean onNestedPreFling(CoordinatorLayout coordinatorLayout, View child, View target,
float velocityX, float velocityY) {
return false;
}
}
我們自定義Behavior的一般目的只有兩個,一是根據某些依賴的View的位置改變進行相應的操作;二是響應CoordinatorLayout中某些組件的滑動事件。先來看第一種情況:
2.1 兩個View之間的依賴關係
如果一個View需要依賴另一個View,可能需要操作下面的API:
//確定提供的子視圖是否有另一個特定的同級視圖作爲佈局依賴項
public boolean layoutDependsOn(CoordinatorLayout parent, V child, View dependency) { return false; }
//子View依賴的View的改變做出響應,當依賴視圖在標準佈局流之外的大小或者位置發生變化,此方法被調用。
//Behavior可以使用此方法更新子視圖,如果子視圖大小或者位置發生變化則返回true。
public boolean onDependentViewChanged(CoordinatorLayout parent, V child, View dependency) { return false; }
//響應子View在依賴的視圖中移除
public void onDependentViewRemoved(CoordinatorLayout parent, V child, View dependency) {}
通過layoutDependsOn()
確定一個View是否對另一個View進行依賴,注意:child是測試的子View,dependency該依賴的子View,如果child的佈局依賴於dependency的佈局則返回true,即依賴成立;反之不成立。當然你可以複寫該方法對dependency進行類型判斷然後再決定是否依賴,只有在layoutDependsOn()
返回true的時候後面的onDependentViewChanged()
和onDependentViewRemoved()
纔會執行。
註釋上面有說,無論子View 的順序如何,總是在依賴的子View被佈局後再佈局這個子View,當依賴視圖的佈局發生改變時回調Behavior的onDependentViewChanged()
方法,可以適當更新響應的子視圖。如果Behavior改變了child的位置和尺寸時,則返回true,默認返回false。
onDependentViewRemoved()
響應child從屬性視圖中被刪除,從parent中移除dependency後調用此方法,
Behavior也可以使用此方法適當更新子視圖來作爲響應。
但是這樣說有點抽象,到底是誰依賴誰?何爲依賴?
下面舉個例子加深理解:首先定義一個可以響應屏幕拖拽的View,DependencyView效果如下:
它的代碼很簡單,繼承自TextView,在觸摸事件onTouchEvent()
根據觸摸點移動對自身位置進行位移。
public class DependencyView extends AppCompatTextView {
private int mTouchSlop;
private float mLastY;
private float mLastX;
public DependencyView(Context context) {
this(context, null);
}
public DependencyView(Context context, @Nullable AttributeSet attrs) {
this(context, attrs, 0);
}
public DependencyView(Context context, @Nullable AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
setClickable(true);
//在用戶發生滾動之前,以像素爲單位的觸摸距離可能會發生飄移
mTouchSlop = ViewConfiguration.get(context).getScaledTouchSlop();
}
@Override
public boolean onTouchEvent(MotionEvent event) {
switch (event.getAction()) {
case MotionEvent.ACTION_DOWN://按下
mLastX = event.getX();
mLastY = event.getY();
break;
case MotionEvent.ACTION_MOVE://移動
float moveX = event.getX() - mLastX;
float moveY = event.getY() - mLastY;
if (Math.abs(moveX) > mTouchSlop || Math.abs(moveY) > mTouchSlop) {
ViewCompat.offsetLeftAndRight(this, (int) moveX);
ViewCompat.offsetTopAndBottom(this, (int) moveY);
mLastX = event.getX();
mLastY = event.getY();
}
break;
case MotionEvent.ACTION_UP://擡起
mLastX = event.getX();
mLastY = event.getY();
break;
}
return true;
}
}
然後實現一個Behavior,讓它只配一個View,去緊緊依賴所支配的View。這裏我們讓依賴的View顯示在被依賴的View的下面,不論被依賴的View位置如何變化,依賴的View都跟着變化:
public class MyBehavior extends CoordinatorLayout.Behavior<View> {
private static final String TAG = "MyBehavior";
public MyBehavior() {
}
public MyBehavior(Context context, AttributeSet attrs) {
super(context, attrs);
}
//確定提供的子視圖是否有另一個特定的同級視圖作爲佈局依賴項
@Override
public boolean layoutDependsOn(@NonNull CoordinatorLayout parent, @NonNull View child, @NonNull View dependency) {
return dependency instanceof DependencyView;
}
//子View依賴的View的改變做出響應,當依賴視圖在標準佈局流之外的大小或者位置發生變化,此方法被調用。
//Behavior可以使用此方法更新子視圖,如果子視圖大小或者位置發生變化則返回true。
@Override
public boolean onDependentViewChanged(@NonNull CoordinatorLayout parent, @NonNull View child, @NonNull View dependency) {
int bottom = dependency.getBottom();
child.setY(bottom );
child.setX(dependency.getLeft());
Log.d(TAG, "onDependentViewChanged: " + dependency);
return true;
}
}
在layoutDependsOn()
中通過判斷dependency是否爲DependencyView類型決定是否對其進行依賴,然後再onDependentViewChanged()
獲取dependency的位置參數來設置child的位置參數,從而實現了child跟隨dependency的位置改變而發生位置改變。
下面來驗證MyBehavior,我們在佈局文件中對ImageView設置了MyBehavior,然後觀察它的現象:
<?xml version="1.0" encoding="utf-8"?>
<androidx.coordinatorlayout.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:fitsSystemWindows="true">
<ImageView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:src="@mipmap/ic_head"
app:layout_behavior="com.antiphon.recyclerviewdemo.weight.MyBehavior" />
<com.antiphon.recyclerviewdemo.weight.DependencyView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:background="@color/colorAccent"
android:padding="4dp"
android:text="DependencyView"
android:textColor="#fff"
android:textSize="18sp" />
</androidx.coordinatorlayout.widget.CoordinatorLayout>
注意:佈局最外層需要是CoordinatorLayout作爲父佈局,效果如下:
可以看到,我們在拖動DependencyView的時候,ImageView也跟隨着DependencyView移動。當然這種依賴並非只有一對一的關係,也可能是一對多或者多對多。
我們再修改一下MyBehavior中的代碼,如果child是一個TextView則顯示在 dependency的上方,否則顯示在下方:
@Override
public boolean onDependentViewChanged(@NonNull CoordinatorLayout parent, @NonNull View child, @NonNull View dependency) {
//child座標
float childX = child.getX();
float childY = child.getY();
//dependency頂部底部座標
int dependencyTop = dependency.getTop();
int dependencyBottom = dependency.getBottom();
childX = dependency.getX();
if (child instanceof TextView) {//如果是TextView則顯示在dependency上面,否則顯示在下面
childY = dependencyTop + child.getHeight();
} else {
childY = dependencyBottom;
}
child.setX(childX);
child.setY(childY);
return true;
我們在xml佈局文件再添加一個TextView,設置MyBehavior:
<ImageView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:src="@mipmap/ic_head"
app:layout_behavior="com.antiphon.recyclerviewdemo.weight.MyBehavior" />
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="helloWord"
android:textColor="#000"
android:textSize="14sp"
app:layout_behavior="com.antiphon.recyclerviewdemo.weight.MyBehavior" />
效果如下:
到這裏我們知道,在Behavior中針對被依賴對象尺寸及位置變化時,依賴方該如何處理的流程,接下來就是處理滑動相關操作了。
2.2 Behavior對滑動事件的響應
我們一般接觸到的滑動控件一般是 ScrollView和 RecyclerView,而CoordinatorLayout本身能滑動嗎?如果能是怎麼滑動的,到底是誰響應誰的滑動。
Behavior的相關滑動代碼如下:
public boolean onStartNestedScroll(CoordinatorLayout coordinatorLayout, View child, View directTargetChild,
View target, int axes, int type) {
return false;
}
public void onNestedScrollAccepted(CoordinatorLayout coordinatorLayout, View child, View directTargetChild,
View target, int axes, int type) {
// Do nothing
}
public void onStopNestedScroll(CoordinatorLayout coordinatorLayout, V child, View target, int type) {
// Do nothing
}
public void onNestedScroll(CoordinatorLayout coordinatorLayout, View child, View target, int dxConsumed,
int dyConsumed, int dxUnconsumed, int dyUnconsumed, int type) {
// Do nothing
}
public void onNestedPreScroll(CoordinatorLayout coordinatorLayout, View child, View target, int dx,
int dy, int[] consumed, int type) {
// Do nothing
}
public boolean onNestedFling(CoordinatorLayout coordinatorLayout, View child, View target,
float velocityX, float velocityY, boolean consumed) {
return false;
}
public boolean onNestedPreFling(CoordinatorLayout coordinatorLayout, View child, View target,
float velocityX, float velocityY) {
return false;
}
爲了觀察Behavior行爲,我在相關滑動方法添加了log:
@Override
public boolean onStartNestedScroll(CoordinatorLayout coordinatorLayout, View child, View directTargetChild,
View target, int axes, int type) {
Log.e(TAG, "onStartNestedScroll:axes == " + axes);
return super.onStartNestedScroll(coordinatorLayout, child, directTargetChild, target, axes, type);
}
@Override
public void onNestedScrollAccepted(CoordinatorLayout coordinatorLayout, View child, View directTargetChild,
View target, int axes, int type) {
Log.e(TAG, "onNestedScrollAccepted:axes == " + axes + " | type == " + type);
super.onNestedScrollAccepted(coordinatorLayout, child, directTargetChild, target, axes, type);
}
@Override
public void onNestedScroll(CoordinatorLayout coordinatorLayout, View child, View target, int dxConsumed,
int dyConsumed, int dxUnconsumed, int dyUnconsumed, int type) {
Log.e(TAG, "onNestedScrollAccepted:dxConsumed == " + dxConsumed + " | dyConsumed == "
+ dyConsumed + " | dxUnconsumed == " + dxUnconsumed + " | dyUnconsumed == " + dyUnconsumed);
super.onNestedScroll(coordinatorLayout, child, target, dxConsumed, dyConsumed, dxUnconsumed, dyUnconsumed, type);
}
@Override
public void onNestedPreScroll(CoordinatorLayout coordinatorLayout, View child, View target, int dx,
int dy, int[] consumed, int type) {
Log.e(TAG, "onNestedScrollAccepted:dx == " + dx + " | dy == " + dy + " | type == " + type);
super.onNestedPreScroll(coordinatorLayout, child, target, dx, dy, consumed, type);
}
@Override
public void onStopNestedScroll(CoordinatorLayout coordinatorLayout, View child, View target, int type) {
Log.e(TAG, "onNestedScrollAccepted:type == " + type);
super.onStopNestedScroll(coordinatorLayout, child, target, type);
}
@Override
public boolean onNestedFling(CoordinatorLayout coordinatorLayout, View child, View target,
float velocityX, float velocityY, boolean consumed) {
Log.e(TAG, "onNestedScrollAccepted:velocityX == " + velocityX + " | velocityY == "
+ velocityY + " | consumed == " + consumed);
return super.onNestedFling(coordinatorLayout, child, target, velocityX, velocityY, consumed);
}
@Override
public boolean onNestedPreFling(CoordinatorLayout coordinatorLayout, View child, View target,
float velocityX, float velocityY) {
Log.e(TAG, "onNestedScrollAccepted:velocityX == " + velocityX + " | velocityY == " + velocityY);
return super.onNestedPreFling(coordinatorLayout, child, target, velocityX, velocityY);
}
然後我在模擬器上面用鼠標滑動CoordinatorLayout製造一些滑動 事件,觀看MyBehavior相關的滑動函數API是否能觸發,然後觀察log:
可以看到,並沒有觸發任何的函數。那麼先來了解嵌套滑動事件的API:
/**
* 如果CoordinatorLayout後代的View嘗試發起嵌套滑動時調用
* 任何與CoordinatorLayout的任何直接子元素相關聯的Behavior都可以響應這個事件並返回true,
* 以指示CoordinatorLayout應該作爲這個滑動嵌套滑動的父View,只有返回true才能接收後續的嵌套滑動事件。
*/
public boolean onStartNestedScroll(CoordinatorLayout coordinatorLayout, View child, View directTargetChild,
View target, int axes, int type) {
return false;
}
註釋中說明,當一個CoordinatorLayout的後代想要觸發嵌套滑動事件時,這個方法被調用,只有onStartNestedScroll()
返回true,後續的嵌套滑動事件纔會響應。後續響應的函數指的是這幾個函數:
public void onNestedScrollAccepted(CoordinatorLayout coordinatorLayout, View child, View directTargetChild,
View target, int axes, int type) {}
public void onStopNestedScroll(CoordinatorLayout coordinatorLayout, V child, View target, int type) {}
public void onNestedScroll(CoordinatorLayout coordinatorLayout, View child, View target, int dxConsumed,
int dyConsumed, int dxUnconsumed, int dyUnconsumed, int type) {}
public void onNestedPreScroll(CoordinatorLayout coordinatorLayout, View child, View target, int dx,
int dy, int[] consumed, int type) {}
public boolean onNestedFling(CoordinatorLayout coordinatorLayout, View child, View target,
float velocityX, float velocityY, boolean consumed) {}
public boolean onNestedPreFling(CoordinatorLayout coordinatorLayout, View child, View target,
float velocityX, float velocityY) {}
那麼,我們先從onStartNestedScroll()
開始分析,查找在哪裏調用到這個 方法:
可以看到MyBehavior的onStartNestedScroll()
在哪裏被調用了,原來它是在CoordinatorLayout中被調用:
@Override
public boolean onStartNestedScroll(View child, View target, int axes, int type) {
boolean handled = false;
final int childCount = getChildCount();
for (int i = 0; i < childCount; i++) {
final View view = getChildAt(i);
if (view.getVisibility() == View.GONE) {
// If it's GONE, don't dispatch
continue;
}
final LayoutParams lp = (LayoutParams) view.getLayoutParams();
final Behavior viewBehavior = lp.getBehavior();
if (viewBehavior != null) {
final boolean accepted = viewBehavior.onStartNestedScroll(this, view, child,
target, axes, type);
handled |= accepted;
lp.setNestedScrollAccepted(type, accepted);
} else {
lp.setNestedScrollAccepted(type, false);
}
}
return handled;
}
這是CoordinatorLayout中的一個方法,先獲取CoordinatorLayout下的子View,再獲取子View中的Behavior,然後調用Behavior的onStartNestedScroll()
方法。
繼續深入,那麼誰調用了CoordinatorLayout的onStartNestedScroll()
呢?
繼續追蹤下去:
可以看到有多個地方調用它,其實歸根到底最終都是View或者ViewParentCompat,這裏以View爲例:
public boolean startNestedScroll(int axes) {
if (hasNestedScrollingParent()) {
// Already in progress
return true;
}
if (isNestedScrollingEnabled()) {//是否啓用嵌套滑動
ViewParent p = getParent();
View child = this;
while (p != null) {
try {
if (p.onStartNestedScroll(child, this, axes)) {
mNestedScrollingParent = p;
p.onNestedScrollAccepted(child, this, axes);
return true;
}
}
if (p instanceof View) {
child = (View) p;
}
p = p.getParent();
}
}
return false;
}
當一個View觸發onStartNestedScroll()
的時候,如果符合嵌套滑動,則獲取Parent通過while循環調用Parent的onStartNestedScroll()
方法。因爲CoordinatorLayout就是一個ViewGroup,所以它就是一個ViewParent對象,如果一個CoordinatorLayout的後代View觸發了onStartNestedScroll()
方法,如果符合某種條件,那麼CoordinatorLayout 的onStartNestedScroll()
方法就會被調用,再進一步調用Behavior的onStartNestedScroll()
方法。
當isNestedScrollingEnabled() = true時,它的ViewParent的onStartNestedScroll()
才能被觸發,它是被判斷自身是否能嵌套滑動,如果爲true則在使用時作爲嵌套滑動的子視圖,可以通過setNestedScrollingEnabled(boolean enabled)
來設置View是否擁有嵌套滑動的能力。
使用TextView產生嵌套滑動事件
如果一個View符合嵌套滑動的事件,也就是通過setNestedScrollingEnabled(true)
,然後調用它的onStartNestedScroll()
方法,理論上是可以產生嵌套滑動事件的。我們來嘗試一下,在佈局裏面添加一個普通的TextView,然後設置點擊事件。
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
mTv_nested_scroll.setNestedScrollingEnabled(true);
mTv_nested_scroll.startNestedScroll(View.SCROLL_AXIS_HORIZONTAL);
}
之前在MyBehavior中對應的嵌套滑動方法打印了log,所以如果CoordinatorLayout中發生嵌套滑動的事件,log就有輸出:
可以看到在一個View符合嵌套滑動的事件,則調用它的onStartNestedScroll()
方法。不過在安卓版本21(LOLLIPOP,5.0)及以上時才能調用View的嵌套滑動相關的API,那麼在低於5.0版本呢?其實系統做了兼容:
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.LOLLIPOP){
ViewCompat.setNestedScrollingEnabled(mTv_nested_scroll, true);
ViewCompat.startNestedScroll(mTv_nested_scroll, ViewCompat.SCROLL_AXIS_HORIZONTAL);
}
我們知道5.0以上版本View已經自帶嵌套滑動功能和相關屬性,可以根據ViewCompat
這個類完成低版本的兼容操作,繼續跟蹤ViewCompat.startNestedScroll()
:
public static void setNestedScrollingEnabled(@NonNull View view, boolean enabled) {
if (Build.VERSION.SDK_INT >= 21) {
view.setNestedScrollingEnabled(enabled);
} else {
if (view instanceof NestedScrollingChild) {
((NestedScrollingChild) view).setNestedScrollingEnabled(enabled);
}
}
}
public static boolean startNestedScroll(@NonNull View view, @ScrollAxis int axes) {
if (Build.VERSION.SDK_INT >= 21) {
return view.startNestedScroll(axes);
}
if (view instanceof NestedScrollingChild) {
return ((NestedScrollingChild) view).startNestedScroll(axes);
}
return false;
}
可以看到ViewCompat
的源碼裏面也做了版本兼容,在API>=21則會直接調用view.startNestedScroll()
相關嵌套滑動方法,否則會判斷view是否爲NestedScrollingChild
的實例,如果是則調用NestedScrollingChild
的startNestedScroll()
方法,NestedScrollingChild
是一個接口。
所以如果在5.0以上版本我們可以view.startNestedScroll()
,如果在5.0以下版本,如果一個View想發起嵌套滑動事件,你得保證這個View實現NestedScrollingChild
接口。
想觸發事件嗎?你是NestedScrollingChild嗎?
我們來看看NestedScrollingChild
:
public interface NestedScrollingChild {
/**
* 啓用或者禁用此View的嵌套滑動。
* 注意:如果此屬性設置爲true,則允許該View使用當前層結構中兼容的父View啓動嵌套滑動操作。
* 如果這個View沒有任務嵌套滑動,則這個方法沒有任何作用。
*/
void setNestedScrollingEnabled(boolean enabled);
/**
* 如果此View啓用了嵌套滑動,則返回true。
* 如果啓動了嵌套滑動,並且這個View類實現支持嵌套滑動,那麼這個View將在適用時充當嵌套滑動子View,
* 將有關正在進行的滾動操作的數據轉發到兼容且協作嵌套滑動的父View。
*/
boolean isNestedScrollingEnabled();
/**
* 沿着給定的方向(垂直或水平方向)開始一個可嵌套滾動的滑動操作。
* 返回true表示找到一個協作的父節點,並且爲當前手勢窮嵌套滑動。
*/
boolean startNestedScroll(int axes);
/**
* 停止正在進行的嵌套滑動
*/
void stopNestedScroll();
/**
* 如果該View有一個嵌套的滑動父View,則返回true
*/
boolean hasNestedScrollingParent();
boolean dispatchNestedScroll(int dxConsumed, int dyConsumed,int dxUnconsumed,
int dyUnconsumed, int[] offsetInWindow);
boolean dispatchNestedPreScroll(int dx, int dy, int[] consumed, int[] offsetInWindow);
boolean dispatchNestedFling(float velocityX, float velocityY, boolean consumed);
boolean dispatchNestedPreFling(float velocityX, float velocityY);
}
這個接口應該由View的子類實現並且希望支持將嵌套滑動操作分派到協作的父View(ViewGroup)。實現這個接口的類應該創建NestedScrollingChildHelper
並將任何View方法委託給它。調用嵌套滑動功能的View應該始終從ViewCompat,ViewGroupCompat,ViewParentCompat兼容性墊板靜態方法執行,確保Android 5.0及以上版本的嵌套滑動視圖的相護操作性。
通過AndroidStudio快捷鍵Ctrl+H,可以看到NestedScrollingChild
目前的實現者:
NestedScrollingChild
的實現者有兩個SwipleRefreshLayout
和NestedScrollingChild2
,而NestedScrollingChild2
也有兩個實現者RecyclerView
和NestedScrollingChild3
,那麼RecyclerView也是NestedScrollingChild
間接實現者。而NestedScrollingChild3
的實現者是RecyclerView和NestedScrollView。
那麼來到這裏,NestedScrollingChild
可以說有三個實現類,RecyclerView,NestedScrollView,SwipleRefreshLayout,上面三個控件我們都認識,都是自帶滑動的控件。
Behavior是如何響應滑動事件的?
我們在上面驗證了child之間的依賴互動關係,那麼Behavior是如果響應滑動事件的?我們需要找到挑起嵌套滑動的View,我們往CoordinatorLayout佈局中添加RecyclerView,滑動RecyclerView的內容,能產生嵌套滑動事件:
<?xml version="1.0" encoding="utf-8"?>
<androidx.coordinatorlayout.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:fitsSystemWindows="true">
<ImageView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginLeft="5dp"
android:layout_marginTop="10dp"
android:src="@mipmap/ic_gesture_down"
app:layout_behavior="com.antiphon.recyclerviewdemo.weight.MyBehavior" />
<androidx.recyclerview.widget.RecyclerView
android:id="@+id/recyclerView"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:layout_marginLeft="50dp" />
</androidx.coordinatorlayout.widget.CoordinatorLayout>
Behavior需要針對自身的業務邏輯進行處理,當我們滑動RecyclerView內容的時候,MyBehavior規定關聯的ImageView進行相應的位移,主要是在垂直方向上。在MyBehavior的onStartNestedScroll()
做一些特別的處理:
@Override
public boolean onStartNestedScroll(CoordinatorLayout coordinatorLayout, View child, View directTargetChild,
View target, int axes, int type) {
Log.e(TAG, "onStartNestedScroll:axes == " + axes);
return child instanceof ImageView && axes == ViewCompat.SCROLL_AXIS_VERTICAL;
}
只要child是ImageView並且滑動方向是垂直方向,返回true響應後續的嵌套滑動事件,針對滑動事件返回的位移對child進行操作:
@Override
public void onNestedPreScroll(CoordinatorLayout coordinatorLayout, View child, View target, int dx,
int dy, int[] consumed, int type) {
Log.e(TAG, "onNestedScrollAccepted:dx == " + dx + " | dy == " + dy + " | type == " + type);
super.onNestedPreScroll(coordinatorLayout, child, target, dx, dy, consumed, type);
ViewCompat.offsetTopAndBottom(child, dy);//讓child進行垂直方向移動
}
複寫onNestedPreScroll()
方法,通過讀取dy值,讓child進行垂直方向移動。dx是滑動水平方向的位移,dy是滑動垂直方向的位移,它是在滑動事件滑動onNestedScroll()
之前調用,然後把消耗的距離傳遞給consumed數組中。而onNestedScroll()
是滑動事件時調用,它的參數包括位移信息,以及已經在onNestedPreScroll()
消耗過的位移數。實現onNestedPreScroll()
方法就可以了。
@Override
public boolean layoutDependsOn(@NonNull CoordinatorLayout parent, @NonNull View child, @NonNull View dependency) {
return false;
// return dependency instanceof DependencyView;
}
將MyBehavior之前做的一些處理,將它與DependencyView接觸依賴。效果如下:
這裏還有兩個和慣性滑動相關的API:
public boolean onNestedFling(CoordinatorLayout coordinatorLayout, View child, View target,
float velocityX, float velocityY, boolean consumed) {
return false;
}
public boolean onNestedPreFling(CoordinatorLayout coordinatorLayout, View child, View target,
float velocityX, float velocityY) {
return false;
}
Fling慣性滑動,RecyclerView和NestedScrollView快速滑動的時候,手指停下來的時候,滑動操作並沒有停止,還會滑動一段距離。同樣我們在Fling動作即將發生的時候,通過onNestedPreFling()
如果返回true則會攔截這次Fling動作,表明響應中的child自己處理這裏fling事件,那麼RecyclerView反而操作不了這個動作,因爲child消耗了這個fling事件。
我們驗證一下,將MyBehavior響應fling事件的時候,如果滑動向下,則ImageView放大,滑動向上,則ImageView縮小。
@Override
public boolean onNestedPreFling(CoordinatorLayout coordinatorLayout, View child, View target,
float velocityX, float velocityY) {
Log.e(TAG, "onNestedScrollAccepted:velocityX == " + velocityX + " | velocityY == " + velocityY);
if (velocityY > 0) {//向下慣性滑動
child.animate().scaleX(2f).scaleY(2f).setDuration(2000).start();
} else {//向上慣性滑動
child.animate().scaleX(1f).scaleY(1f).setDuration(2000).start();
}
return false;
// return super.onNestedPreFling(coordinatorLayout, child, target, velocityX, velocityY);
}
效果如下:
Behavior的總結
1.確定CoordinatorLayout中View與View之間的關係,通過layoutDependsOn()
方法返回true則表示依賴,否則不依賴;
2.當一個被依賴項dependency尺寸或者位置發生變化時,依賴方會通過Behavior獲取到,然後在onDependentViewChanged()
中處理,如果方法中child的尺寸或者位置發生了變化,則需要返回true;
3.當Behavior中的View準備響應嵌套滑動時,它不需要通過layoutDependsOn()
來進行依賴綁定,只需要在onStartNestedScroll()
通過返回值告訴ViewParent,是否開啓嵌套滑動功能,返回true後續的嵌套滑動事件才能響應。
4.嵌套滑動包括普通滑動(scroll)和慣性滑動(fling)兩種。
前面文章我有嘗試找出誰能產生嵌套滑動事件,結果發現他需要是NestedScrollChild對象,但是NestedScrollChild在調用startNestedScroll()
方法時,它需要藉助它父View的力量,只有父View的startNestedScroll()
返回true的時候,它的後續事件才能延續下去。
View.java
public boolean startNestedScroll(int axes) {
if (hasNestedScrollingParent()) {
// Already in progress
return true;
}
if (isNestedScrollingEnabled()) {
ViewParent p = getParent();
View child = this;
while (p != null) {
try {
if (p.onStartNestedScroll(child, this, axes)) {
mNestedScrollingParent = p;
p.onNestedScrollAccepted(child, this, axes);
return true;
}
}
if (p instanceof View) {
child = (View) p;
}
p = p.getParent();
}
}
return false;
}
可以看到ViewParent充當了非常重要的角色,回調了父View的onStartNestedScroll()
方法:
public interface ViewParent {
public void requestLayout();
public ViewParent getParent();
public boolean onStartNestedScroll(View child, View target, int nestedScrollAxes);
public void onNestedScrollAccepted(View child, View target, int nestedScrollAxes);
public void onStopNestedScroll(View target);
public void onNestedScroll(View target, int dxConsumed, int dyConsumed,
int dxUnconsumed, int dyUnconsumed);
public void onNestedPreScroll(View target, int dx, int dy, int[] consumed);
public boolean onNestedFling(View target, float velocityX, float velocityY, boolean consumed);
public boolean onNestedPreFling(View target, float velocityX, float velocityY);
}
ViewParent是一個接口,常見的實現類是ViewGroup,它提供了嵌套滑動的相關API,實在安卓5.0才加進去的,如果要兼容的話需要分析ViewParentCompat這個類,它爲View以及它的父類提供了執行嵌套滑動的初始配置的機會,如果有這個方法的實現則應該調用父類來實現這個方法:
public static boolean onStartNestedScroll(ViewParent parent, View child, View target,
int nestedScrollAxes, int type) {
if (parent instanceof NestedScrollingParent2) {
// First try the NestedScrollingParent2 API
return ((NestedScrollingParent2) parent).onStartNestedScroll(child, target,
nestedScrollAxes, type);
} else if (type == ViewCompat.TYPE_TOUCH) {
// Else if the type is the default (touch), try the NestedScrollingParent API
if (Build.VERSION.SDK_INT >= 21) {//5.0及以上
try {
return parent.onStartNestedScroll(child, target, nestedScrollAxes);
} catch (AbstractMethodError e) {
Log.e(TAG, "ViewParent " + parent + " does not implement interface "
+ "method onStartNestedScroll", e);
}
} else if (parent instanceof NestedScrollingParent) {//5.0以下
return ((NestedScrollingParent) parent).onStartNestedScroll(child, target,
nestedScrollAxes);
}
}
return false;
}
可以看到在安卓5.0版本以下,如果ViewParent需要響應嵌套滑動事件,則需要保證自己是一個NestedScrollingParent對象:
public interface NestedScrollingParent {
boolean onStartNestedScroll(View child, View target, int axes);
void onNestedScrollAccepted(View child, View target, int axes);
void onStopNestedScroll(View target);
void onNestedScroll(View target, int dxConsumed, int dyConsumed, int dxUnconsumed, int dyUnconsumed);
void onNestedPreScroll(View target, int dx, int dy, int[] consumed);
boolean onNestedFling(View target, float velocityX, float velocityY, boolean consumed);
boolean onNestedPreFling(View target, float velocityX, float velocityY);
int getNestedScrollAxes();
}
NestedScrollingParent的實現類有以下幾個:
因爲NestedScrollingParent2也是NestedScrollingParent的一個實現類,CoordinatorLayout間接實現了NestedScrollingParent,所以有四個實現了類:CoordinatorLayout、NestedScrollView、SwipeRefreshLayout、ActionBarOverlayLayout。所以CoordinatorLayout之所以能處理嵌套滑動事件,是因爲它本身就是一個NestedScrollingParent。
嵌套滑動的流程:
RecyclerView產生的嵌套滑動事件響應Behavior的相關嵌套滑動事件方法,接着響應CoordinatorLayout 的onStartNestedScroll()等嵌套滑動方法。
總的來說一個嵌套滑動事件的起始,它是由一個NestedScrollingChild發起,通過向上遍歷parent,藉助parent的嵌套滑動相關方法來完成交互。注意安卓5.0以下版本,parent要保證是一個NestedScrollingParent對象。
三、RecyclerView的嵌套滑動事件
<未完待續,後續會抓緊時間補上>
請尊重原創者版權,轉載請標明出處:https://blog.csdn.net/m0_37796683/article/details/105065358 謝謝!
相關文章:
理解RecyclerView(五)
● RecyclerView的繪製流程
理解RecyclerView(六)
● RecyclerView的滑動原理
理解RecyclerView(七)
● RecyclerView的嵌套滑動機制
理解RecyclerView(八)
● RecyclerView的回收複用緩存機制詳解
理解RecyclerView(九)
● RecyclerView的自定義LayoutManager