產生原因
由於在Android上進行滑動的控件在手機性能越來越好的情況下,人們已經習慣於常用的手勢進行操作,出現許多控件滑動時需要去協調同一個界面的滑動的情況。例如在同一個方向內外(上下)的嵌套,不同方向(上下與左右方向)的嵌套等。解決這類嵌套可以通過Android開發藝術書上講的內部攔截法
和外部攔截法
去解決,但是,在處理多個View的協調時使用外部攔截法,特別是一些第三方庫,在使用時就必須去修改源碼裏的onTouch()等方法。並且在處理如下圖所示,當滑動到一定距離又需要攔截的View去響應滑動,這種情況是需要自己去手動處理事件分發,相對就複雜不少。於是Google在5.0後推出了NestedScrolling解決方式。
NestedScrolling
這是Google官方從5.0後引入的滑動嵌套解決方案,同時能夠向後兼容。最有代表性的就是Handling Scrolls with CoordinatorLayout中的效果, 下面的效果就是從5.0後常見的效果。那我們需要去了解背後的原因。再舉一反三去擴展使用到自己想要的效果。
<android.support.design.widget.AppBarLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:fitsSystemWindows="true"
android:theme="@style/ThemeOverlay.AppCompat.Dark.ActionBar">
<android.support.design.widget.CollapsingToolbarLayout
android:id="@+id/collapsing_toolbar"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:fitsSystemWindows="true"
app:contentScrim="?attr/colorPrimary"
app:expandedTitleMarginEnd="64dp"
app:expandedTitleMarginStart="48dp"
app:layout_scrollFlags="scroll|exitUntilCollapsed">
<android.support.v7.widget.Toolbar
android:id="@+id/toolbar"
android:layout_width="match_parent"
android:layout_height="?attr/actionBarSize"
app:layout_scrollFlags="scroll|enterAlways"></android.support.v7.widget.Toolbar>
</android.support.design.widget.CollapsingToolbarLayout>
</android.support.design.widget.AppBarLayout>
@CoordinatorLayout.DefaultBehavior(AppBarLayout.Behavior.class)
public class AppBarLayout extends LinearLayout {
上面知道了AppBarLayout是LinearLayout並且註解說明使用AppBarLayout.Behavior.class
來處理協調滑動。
具體 看看這個Behavior,可以看到幾個關鍵的NestedScroll相關方法,後面會具體說明。
再瞭解一下CoordinatorLayout,實現了NestedScrollingParent接口
public class CoordinatorLayout extends ViewGroup implements NestedScrollingParent
從上面瞭解的一些,我們接下來就全面來了解以下幾個類。
- NestedScrollingChild
NestedScrollingChildHelper
NestedScrollingParent
- NestedScrollingParentHelper
NestedScrollingChild
先看看NestedScrollingChild的細節。
public interface NestedScrollingChild {
/**
* 設置嵌套滑動是否能用
*
* @param enabled true to enable nested scrolling, false to disable
*/
public void setNestedScrollingEnabled(boolean enabled);
/**
* 判斷嵌套滑動是否可用
*
* @return true if nested scrolling is enabled
*/
public boolean isNestedScrollingEnabled();
/**
* 開始嵌套滑動
*
* @param axes 表示方向軸,有橫向和豎向
*/
public boolean startNestedScroll(int axes);
/**
* 停止嵌套滑動
*/
public void stopNestedScroll();
/**
* 判斷是否有父View 支持嵌套滑動
* @return whether this view has a nested scrolling parent
*/
public boolean hasNestedScrollingParent();
/**
* 在子View的onInterceptTouchEvent或者onTouch中,調用該方法通知父View滑動的距離
*
* @param dx x軸上滑動的距離
* @param dy y軸上滑動的距離
* @param consumed 父view消費掉的scroll長度
* @param offsetInWindow 子View的窗體偏移量
* @return 支持的嵌套的父View 是否處理了 滑動事件
*/
public boolean dispatchNestedPreScroll(int dx, int dy, int[] consumed, int[] offsetInWindow);
/**
* 子view處理scroll後調用
*
* @param dxConsumed x軸上被消費的距離(橫向)
* @param dyConsumed y軸上被消費的距離(豎向)
* @param dxUnconsumed x軸上未被消費的距離
* @param dyUnconsumed y軸上未被消費的距離
* @param offsetInWindow 子View的窗體偏移量
* @return true if the event was dispatched, false if it could not be dispatched.
*/
public boolean dispatchNestedScroll(int dxConsumed, int dyConsumed,
int dxUnconsumed, int dyUnconsumed, int[] offsetInWindow);
/**
* 滑行時調用
*
* @param velocityX x 軸上的滑動速率
* @param velocityY y 軸上的滑動速率
* @param consumed 是否被消費
* @return true if the nested scrolling parent consumed or otherwise reacted to the fling
*/
public boolean dispatchNestedFling(float velocityX, float velocityY, boolean consumed);
/**
* 進行滑行前調用
*
* @param velocityX x 軸上的滑動速率
* @param velocityY y 軸上的滑動速率
* @return true if a nested scrolling parent consumed the fling
*/
public boolean dispatchNestedPreFling(float velocityX, float velocityY);
}
CoordinatorLayout
裏嵌套着RecyclerView
和Toolbar
,我們上下滑動RecyclerView
的時候,Toolbar
會隨之顯現隱藏,這是典型的嵌套滑動機制情景。這裏,RecyclerView
作爲嵌套的子View,我們猜測,它一定實現了NestedScrollingChild
接口。
public class RecyclerView extends ViewGroup implements ScrollingView, NestedScrollingChild {
// 省略
}
所以RecyclerView 實現了NestedScrollingChild 接口裏的方法,我們在跟進去看看各個方法是怎麼實現的?
@Override
public void setNestedScrollingEnabled(boolean enabled) {
getScrollingChildHelper().setNestedScrollingEnabled(enabled);
}
@Override
public boolean isNestedScrollingEnabled() {
return getScrollingChildHelper().isNestedScrollingEnabled();
}
@Override
public boolean startNestedScroll(int axes) {
return getScrollingChildHelper().startNestedScroll(axes);
}
@Override
public void stopNestedScroll() {
getScrollingChildHelper().stopNestedScroll();
}
@Override
public boolean hasNestedScrollingParent() {
return getScrollingChildHelper().hasNestedScrollingParent();
}
@Override
public boolean dispatchNestedScroll(int dxConsumed, int dyConsumed, int dxUnconsumed,
int dyUnconsumed, int[] offsetInWindow) {
return getScrollingChildHelper().dispatchNestedScroll(dxConsumed, dyConsumed,
dxUnconsumed, dyUnconsumed, offsetInWindow);
}
@Override
public boolean dispatchNestedPreScroll(int dx, int dy, int[] consumed, int[] offsetInWindow) {
return getScrollingChildHelper().dispatchNestedPreScroll(dx, dy, consumed, offsetInWindow);
}
@Override
public boolean dispatchNestedFling(float velocityX, float velocityY, boolean consumed) {
return getScrollingChildHelper().dispatchNestedFling(velocityX, velocityY, consumed);
}
@Override
public boolean dispatchNestedPreFling(float velocityX, float velocityY) {
return getScrollingChildHelper().dispatchNestedPreFling(velocityX, velocityY);
}
從上面的代碼可以看出,全部都交給getScrollingChildHelper()
這個方法的返回對象處理了,看看這個方法是怎麼實現的。
private NestedScrollingChildHelper getScrollingChildHelper() {
if (mScrollingChildHelper == null) {
mScrollingChildHelper = new NestedScrollingChildHelper(this);
}
return mScrollingChildHelper;
}
NestedScrollingChild
接口的方法都交給NestedScrollingChildHelper
這個代理對象處理了。現在我們繼續深入,隨意挑個,分析下NestedScrollingChildHelper
中開始嵌套滑動startNestedScroll(int axes)
方法是怎麼實現的。
NestedScrollingChildHelper#startNestedScroll
public boolean startNestedScroll(int axes) {
if (hasNestedScrollingParent()) {
return true;
}
if (isNestedScrollingEnabled()) {//判斷是否可以滑動
ViewParent p = mView.getParent();
View child = mView;
while (p != null) {
if (ViewParentCompat.onStartNestedScroll(p, child, mView, axes)) {//回調了父View的onStartNestedScroll方法
mNestedScrollingParent = p;
ViewParentCompat.onNestedScrollAccepted(p, child, mView, axes);
return true;
}
if (p instanceof View) {
child = (View) p;
}
p = p.getParent();
}
}
return false;
}
以上方法主要做了:
判斷是否有嵌套滑動的父View,返回值 true 表示找到了嵌套滑動的父View和同意一起處理 Scroll 事件。
用While的方式尋找最近嵌套滑動的父View ,如果找到調用父view的onNestedScrollAccepted.
從這裏至少可以得出 子view在調用某個方法都會回調嵌套父view相應的方法,比如子view開始了startNestedScroll,如果嵌套父view存在,就會回調父view的onStartNestedScroll、onNestedScrollAccepted方法。
NestedScrollingChildHelper#dispatchNestedPreScroll
NestedScrollingChildHelper#dispatchNestedScroll
NestedScrollingChildHelper#stopNestedScroll
以上Helper的實現也是這個思路。
NestedScrollingParent
public interface NestedScrollingParent {
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);
public int getNestedScrollAxes();
}
其實和子view差不多的方法,大致一一對應關係,而且它的具體實現也交給了NestedScrollingParentHelper
這個代理類,這和我們上文的方式是一樣的,就不再重複了
調用流程
當 NestedScrollingChild(下文用Child代替) 要開始滑動的時候會調用 onStartNestedScroll ,然後交給代理類NestedScrollingChildHelper(下文ChildHelper代替)的onStartNestedScroll請求給最近的NestedScrollingParent(下文Parent代替).
當ChildHelper的onStartNestedScroll方法 返回 true 表示同意一起處理 Scroll 事件的時候時候,ChildHelper會通知Parent回調onNestedScrollAccepted 做一些準備動作
當Child 要開始滑動的時候,會先發送onNestedPreScroll,交給ChildHelper的onNestedPreScroll 請求給Parent ,告訴它我現在要滑動多少距離,你覺得行不行,這時候Parent 根據實際情況告訴Child 現在只允許你滑動多少距離.然後 ChildHelper根據 onNestedPreScroll 中回調回來的信息對滑動距離做相對應的調整.
在滑動的過程中 Child 會發送onNestedScroll通知ChildeHelpaer的onNestedScroll告知Parent 當前 Child 的滑動情況.
當要進行滑行的時候,會先發送onNestedFling 請求給Parent,告訴它 我現在要滑行了,你說行不行, 這時候Parent會根據情況告訴 Child 你是否可以滑行.
Child 通過onNestedFling 返回的 Boolean 值來覺得是否進行滑行.如果要滑行的話,會在滑行的時候發送onNestedFling 通知告知 Parent 滑行情況.
當滑動事件結束就會child發送onStopNestedScroll通知 Parent 去做相關操作.
我做一個圖,做得不好不要見笑。
再來個Material Design風格的動態效果
參考鏈接
Handling Scrolls with CoordinatorLayout
NestedScrolling事件機制源碼解析
NestedScrollingChild
NestedScrollingChildHelper
NestedScrollingParent
NestedScrollingParentHelper
SwipeRefreshLayout 解析
Android NestedScrolling機制完全解析 帶你玩轉嵌套滑動
從源碼角度分析嵌套滑動機制NestedScrolling