Android NestedScrolling 解析

產生原因

由於在Android上進行滑動的控件在手機性能越來越好的情況下,人們已經習慣於常用的手勢進行操作,出現許多控件滑動時需要去協調同一個界面的滑動的情況。例如在同一個方向內外(上下)的嵌套,不同方向(上下與左右方向)的嵌套等。解決這類嵌套可以通過Android開發藝術書上講的內部攔截法外部攔截法去解決,但是,在處理多個View的協調時使用外部攔截法,特別是一些第三方庫,在使用時就必須去修改源碼裏的onTouch()等方法。並且在處理如下圖所示,當滑動到一定距離又需要攔截的View去響應滑動,這種情況是需要自己去手動處理事件分發,相對就複雜不少。於是Google在5.0後推出了NestedScrolling解決方式。

NestedScrolling

這是Google官方從5.0後引入的滑動嵌套解決方案,同時能夠向後兼容。最有代表性的就是Handling Scrolls with CoordinatorLayout中的效果, 下面的效果就是從5.0後常見的效果。那我們需要去了解背後的原因。再舉一反三去擴展使用到自己想要的效果。

Expanding or contracting the Toolbar or header space to make room for the main content

<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裏嵌套着RecyclerViewToolbar,我們上下滑動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;
    }

以上方法主要做了:

  1. 判斷是否有嵌套滑動的父View,返回值 true 表示找到了嵌套滑動的父View和同意一起處理 Scroll 事件。

  2. 用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這個代理類,這和我們上文的方式是一樣的,就不再重複了

調用流程

  1. 當 NestedScrollingChild(下文用Child代替) 要開始滑動的時候會調用 onStartNestedScroll ,然後交給代理類NestedScrollingChildHelper(下文ChildHelper代替)的onStartNestedScroll請求給最近的NestedScrollingParent(下文Parent代替).

  2. 當ChildHelper的onStartNestedScroll方法 返回 true 表示同意一起處理 Scroll 事件的時候時候,ChildHelper會通知Parent回調onNestedScrollAccepted 做一些準備動作

  3. 當Child 要開始滑動的時候,會先發送onNestedPreScroll,交給ChildHelper的onNestedPreScroll 請求給Parent ,告訴它我現在要滑動多少距離,你覺得行不行,這時候Parent 根據實際情況告訴Child 現在只允許你滑動多少距離.然後 ChildHelper根據 onNestedPreScroll 中回調回來的信息對滑動距離做相對應的調整.

  4. 在滑動的過程中 Child 會發送onNestedScroll通知ChildeHelpaer的onNestedScroll告知Parent 當前 Child 的滑動情況.

  5. 當要進行滑行的時候,會先發送onNestedFling 請求給Parent,告訴它 我現在要滑行了,你說行不行, 這時候Parent會根據情況告訴 Child 你是否可以滑行.

  6. Child 通過onNestedFling 返回的 Boolean 值來覺得是否進行滑行.如果要滑行的話,會在滑行的時候發送onNestedFling 通知告知 Parent 滑行情況.

  7. 當滑動事件結束就會child發送onStopNestedScroll通知 Parent 去做相關操作.

我做一個圖,做得不好不要見笑。

child-parent

再來個Material Design風格的動態效果
這裏寫圖片描述

參考鏈接

Handling Scrolls with CoordinatorLayout
NestedScrolling事件機制源碼解析
NestedScrollingChild
NestedScrollingChildHelper
NestedScrollingParent
NestedScrollingParentHelper
SwipeRefreshLayout 解析
Android NestedScrolling機制完全解析 帶你玩轉嵌套滑動
從源碼角度分析嵌套滑動機制NestedScrolling


發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章