[原]Android NestedScrolling機制完全解析 帶你玩轉嵌套滑動

LupeStrickl 8年前發布 | 16K 次閱讀 安卓開發 Android開發 移動開發

一、概述

Android在 support.v4 包中為大家提供了兩個非常神奇的類:

  • NestedScrollingParent
  • NestedScrollingChild

如果你從未聽說過這兩個類,沒關系,聽我慢慢介紹,你就明白這兩個類可以用來干嘛了。相信大家都見識過或者使用過 CoordinatorLayout ,通過這個類可以非常便利的幫助我們完成一些炫麗的效果,例如下面這樣的:

這樣的效果就非常適合使用 NestedScrolling機制 去完成,并且 CoordinatorLayout 背后其實也是利用著這套機制,So,我相信你已經明白這套機制可以用來干嘛了。

但是,我相信你還有個問題

  • 這個機制相比傳統的自定義ViewGroup事件分發處理有什么優越的地方嗎?

恩,我們簡單分析下:

按照上圖:

假設我們按照傳統的事件分發去理解,首先我們滑動的是下面的內容區域,而移動卻是外部的ViewGroup在移動,所以按照傳統的方式,肯定是外部的Parent攔截了內部的Child的事件;但是,上述效果圖,當Parent滑動到一定程度時,Child又開始滑動了,中間整個過程是沒有間斷的。從正常的事件分發(不手動調用分發事件,不手動去發出事件)角度去做是不可能的,因為當Parent攔截之后,是沒有辦法再把事件交給Child的,事件分發,對于攔截,相當于一錘子買賣,只要攔截了,當前手勢接下來的事件都會交給Parent(攔截者)來處理。

但是 NestedScrolling機制 來處理這個事情就很好辦,所以對這個機制進行深入學習,一來有助于我們編寫嵌套滑動時一些特殊的效果;二來是我為了對CoordinatorLayout做分析的鋪墊~~~

ps:具體在哪個v4版本中添加的,就不去深究了,如果你的v4中沒有上述兩個類,升級下你的v4版本。 NestedScrolling機制 這個詞,個人稱呼,不清楚官方有沒有這么叫,勿深究。

二、預期效果

當然講解這兩個類,肯定要有案例的支撐,不然太過于空洞了。好在,我這里有個非常好的案例可以來描述:

很久以前,我寫過這樣一篇文章:

完全按照傳統的方式去編寫的,而且為了連續滑動,做了一些非常特殊處理,比如手動去分發DOWN事件類的,有興趣可以閱讀下。

效果圖是這樣的:

今天我們就利用這個效果,作為NestedSroll機制的案例,最后我們還會簡單分析一下源碼,其實源碼還是比較簡單的~~

ps:CoordinatorLayout可以很方便實現該效果,后續的文章也會對CoordinateLayout做一些分析。

三、實現

上述效果圖,分為3部分:頂部布局;中間的ViewPager指示器;以及底部的RecyclerView;

RecyclerView其實就是NestedSrollingChild的實現類,所以本例主要的角色是去實現NestedScrollingParent.

(1)布局文件

首先預覽下布局文件,腦子里面有個大致的布局:

<com.zhy.view.StickyNavLayout xmlns:tools="

<com.zhy.view.SimpleViewPagerIndicator
    android:id="@id/id_stickynavlayout_indicator"
    android:layout_width="match_parent"
    android:layout_height="50dp"
    android:background="#ffffffff" >
</com.zhy.view.SimpleViewPagerIndicator>

<android.support.v4.view.ViewPager
    android:id="@id/id_stickynavlayout_viewpager"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
     >
</android.support.v4.view.ViewPager>

</com.zhy.view.StickyNavLayout></code></pre>

StickyNavLayout是直接繼承自LinearLayout的,并且設置的是 orientation="vertical" ,所以直觀的就是控件按順序縱向排列,至于測量需要做一些特殊的處理,因為不是本文的重點,可以自己查看源碼,或者上面提到的文章。

(2) 實現NestedScrollingParent

NestedScrollingParent是一個接口,實現它需要實現如下方法:

public boolean onStartNestedScroll(View child, View target, int nestedScrollAxes);

public void onNestedScrollAccepted(View child, View target, int nestedScrollAxes);

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();</code></pre>

在寫具體的實現前,先對需要用到的上述方法做一下簡單的介紹:

  • onStartNestedScroll該方法,一定要按照自己的需求返回true,該方法決定了當前控件是否能接收到其內部View(非并非是直接子View)滑動時的參數;假設你只涉及到縱向滑動,這里可以根據nestedScrollAxes這個參數,進行縱向判斷。
  • onNestedPreScroll該方法的會傳入內部View移動的dx,dy,如果你需要消耗一定的dx,dy,就通過最后一個參數consumed進行指定,例如我要消耗一半的dy,就可以寫 consumed[1]=dy/2
  • onNestedFling你可以捕獲對內部View的fling事件,如果 return true 則表示攔截掉內部View的事件。

主要關注的就是這三個方法~

這里內部View表示不一定非要是直接子View,只要是內部View即可。

下面看一下我們具體的實現:

public class StickyNavLayout extends LinearLayout implements NestedScrollingParent
{
    @Override
    public boolean onStartNestedScroll(View child, View target, int nestedScrollAxes)
    {
        return (nestedScrollAxes & ViewCompat.SCROLL_AXIS_VERTICAL) != 0;
    }
    @Override
    public void onNestedPreScroll(View target, int dx, int dy, int[] consumed)
    {
        boolean hiddenTop = dy > 0 && getScrollY() < mTopViewHeight;
        boolean showTop = dy < 0 && getScrollY() > 0 && !ViewCompat.canScrollVertically(target, -1);

    if (hiddenTop || showTop)
    {
        scrollBy(0, dy);
        consumed[1] = dy;
    }
}
@Override
public boolean onNestedPreFling(View target, float velocityX, float velocityY)
{
    if (getScrollY() >= mTopViewHeight) return false;
    fling((int) velocityY);
    return true;
}

}</code></pre>

  • onStartNestedScroll中,我們判斷了如果是縱向返回true,這個一般是需要內部的View去傳入的,你要是不確定,或者擔心內部View編寫的不規范,你可以直接return true;
  • onNestedPreScroll中,我們判斷,如果是上滑且頂部控件未完全隱藏,則消耗掉dy,即 consumed[1]=dy ;如果是下滑且內部View已經無法繼續下拉,則消耗掉dy,即 consumed[1]=dy ,消耗掉的意思,就是自己去執行scrollBy,實際上就是我們的StickNavLayout滑動。
  • 此外,這里還處理了fling,通過onNestedPreFling方法,這個可以根據自己需求定了,當頂部控件顯示時,fling可以讓頂部控件隱藏或者顯示。

以上代碼就能實現下面的效果:

對于fling方法,我們利用了OverScroll的fling的方法,對于邊界檢測,是重寫了scrollTo方法:

public void fling(int velocityY)
{
    mScroller.fling(0, getScrollY(), 0, velocityY, 0, 0, 0, mTopViewHeight);
    invalidate();
}

@Override public void scrollTo(int x, int y) { if (y < 0) { y = 0; } if (y > mTopViewHeight) { y = mTopViewHeight; } if (y != getScrollY()) { super.scrollTo(x, y); } }</code></pre>

詳細的解釋可以看上面提到的博客,這里就不重復了。

到這里呢,可以看到NestedScrolling機制說白了非常簡單:

就是NestedScrollingParent內部的View,在滑動到時候,會首先將dx、dy傳入給NestedScrollingParent,NestedScrollingParent可以決定是否對其進行消耗,一般會根據需求消耗部分或者全部(不過這里并沒有實際的約束,你可以隨便寫消耗多少,可能會對內部View造成一定的影響)。

用白話和原本的事件分發機制作對比就是這樣的(針對正常流程下一次手勢):

  • 事件分發是這樣的:子View首先得到事件處理權,處理過程中,父View可以對其攔截,但是攔截了以后就無法再還給子View(本次手勢內)。
  • NestedScrolling機制是這樣的:內部View在滾動的時候,首先將dx,dy交給NestedScrollingParent,NestedScrollingParent可對其進行部分消耗,剩余的部分還給內部View。

具體的源碼會比本博文復雜,因為涉及到觸摸非內部View區域的一些交互,非本博文重點,可以參考源碼。

四、原理

原理其實就是看內部View什么時候回調NestedScrollingParent各種方法的,直接定位到內部View的onTouchEvent:

@Override
public boolean onTouchEvent(MotionEvent e) {
    switch (action) {
        case MotionEvent.ACTION_DOWN: {
            int nestedScrollAxis = ViewCompat.SCROLL_AXIS_NONE;
            if (canScrollHorizontally) {
                nestedScrollAxis |= ViewCompat.SCROLL_AXIS_HORIZONTAL;
            }
            if (canScrollVertically) {
                nestedScrollAxis |= ViewCompat.SCROLL_AXIS_VERTICAL;
            }
            startNestedScroll(nestedScrollAxis);
        } break;
        case MotionEvent.ACTION_MOVE: {
            if (dispatchNestedPreScroll(dx, dy, mScrollConsumed, mScrollOffset)) {
                dx -= mScrollConsumed[0];
                dy -= mScrollConsumed[1];
                vtev.offsetLocation(mScrollOffset[0], mScrollOffset[1]);
            }
        } break;
        case MotionEvent.ACTION_UP: {
            fling((int) xvel, (int) yvel);
            resetTouch();
        } break;

    case MotionEvent.ACTION_CANCEL: {
        cancelTouch();
    } break;
}
return true;

}</code></pre>

可以看到:

ACTION_DOWN 調用了startNestedScroll; ACTION_MOVE 中調用了dispatchNestedPreScroll; ACTION_UP 可能會觸發fling以調用resetTouch。

startNestedScroll內部實際上:

#NestedScrollingChildHelper
public boolean startNestedScroll(int axes) {
    if (hasNestedScrollingParent()) {
        // Already in progress
        return true;
    }
    if (isNestedScrollingEnabled()) {
        ViewParent p = mView.getParent();
        View child = mView;
        while (p != null) {
            if (ViewParentCompat.onStartNestedScroll(p, child, mView, axes)) {
                mNestedScrollingParent = p;
                ViewParentCompat.onNestedScrollAccepted(p, child, mView, axes);
                return true;
            }
            if (p instanceof View) {
                child = (View) p;
            }
            p = p.getParent();
        }
    }
    return false;
}

去尋找NestedScrollingParent,然后回調onStartNestedScroll和onNestedScrollAccepted。

dispatchNestedPreScroll中會回調onNestedPreScroll方法,內部的scrollByInternal中還會回調onNestedScroll方法。

fling中會回調onNestedPreFling和onNestedFling方法。

resetTouch中則會回調onStopNestedScroll。

代碼其實沒什么貼的,大家直接找到onTouchEvent一眼就能看到,調用的方法名都是dispatchNestedXXX方法,實際內部都是通過NestedScrollingChildHelper實現的。

所以如果你需要實現和NestedScrollingParent協作的內部View,記得實現NestedScrollingChild,然后內部借助NestedScrollingChildHelper這個輔助類,核心的方法都封裝好了,你只需要在恰當的實際去傳入參數調用方法即可。

 

來自:http://blog.csdn.net/lmj623565791/article/details/52204039

 

 本文由用戶 LupeStrickl 自行上傳分享,僅供網友學習交流。所有權歸原作者,若您的權利被侵害,請聯系管理員。
 轉載本站原創文章,請注明出處,并保留原始鏈接、圖片水印。
 本站是一個以用戶分享為主的開源技術平臺,歡迎各類分享!