View 事件傳遞體系知識梳理(2) 嵌套滑動

1、引言

  • 嵌套滑動處理的難點在於:當子控件消費了事件,那麼父控件就不會再有機會處理事件了。
  • 嵌套滑動的基本原理是在子控件接收到滑動一段距離的請求時,先詢問父控件是否要滑動,若是滑動了父控件就通知子控件它消耗了一部分滑動距離,子控件就只處理剩下的滑動距離,而後子控件滑動完畢後再把剩餘的滑動距離傳給父控件
  • 這樣父控件和子控件就有機會對滑動操做做出響應,尤爲父控件可以分別在子控件處理滑動距離以前和以後對滑動距離進行響應。

2、兼容性問題

  • SDK21以後,嵌套滑動相關的邏輯被寫入了ViewViewGroup類。
  • android.support.v4中提供了接口NestedScrollingChildNestedScrollingParent,他們分別定義了ViewViewParent中新增的方法,還有兩個相關輔助類NestedScrollingChildHelperNestedScrollingParentHelper
  • 若是版本是SDK21以前,那麼就會判斷控件是否實現了接口,而後調用接口的方法,若是是SDK21以後,那麼就能夠直接調用對應的方法。

3、默認處理邏輯

雖然ViewViewGroup自己就具備嵌套滑動的相關方法,可是默認狀況是不會調用,由於ViewViewGroup自己不支持滑動,即自己不支持滑動的控件即便有嵌套滑動的相關方法也不能進行嵌套滑動。 所以,要讓控件支持嵌套滑動,那麼要知足:android

  • 控件類具備嵌套滑動的相關方法,要麼僅支持21以後的版本,要麼實現對應的接口。
  • 控件要在合適的位置主動調起嵌套滑動方法。

4、相關方法

4.1 NestedScrollingChild

  • startNestedScroll:起始方法,主要做用是找到接收滑動距離信息的外控件。
  • dispatchNestedPreScroll:在內控件處理滑動前把滑動信息分發給外控件。
  • dispatchNestedScroll:在內控件處理完滑動後把剩下的距離信息分發給外控件。
  • stopNestedScroll:結束方法,主要做用是清空嵌套滑動的相關狀態。
  • setNestedScrollingEnabledisNestedScrollingEnabled:用來判斷控件是否支持嵌套滑動。
  • dispatchNestedPreFlingdispatchNestedFling:和Scroll的對應方法相似,可是分發的是Fling信息。

4.2 NestedScrollingParent

由於內控件是發起者,因此外控件的大部分方法都是被內控件的對應方法所回調的。數組

  • onStartNestedScroll:對應startNestedScroll,內控件經過調用外控件的這個方法來肯定外控件是否接收滑動信息。
  • onNestedScrollAccepted:當外控件肯定接收滑動信息後該方法被回調,可讓外控件作一些前期工做。
  • onNestedPreScroll:關鍵方法,接收內控件處理滑動前的距離信息,在這裏外控件能夠優先響應滑動操做,消耗部分或者所有滑動距離。
  • onNestedScroll:關鍵方法,接收內控件處理完滑動後的距離信息,在這裏外控件能夠選擇是否處理剩餘的滑動信息。
  • onStopNestedScroll:對應stopNestedScroll,用來作一些收尾工做。
  • getNestedScrollAxes:返回嵌套滑動的方向。
  • onNestedPreFlingonNestedFling:同上。

5、NestedScrollView

5.1 收到down事件,尋找外控件

NestedScrollView其實是一個FrameLayout,同時它實現了NestedScrollingParent、NestedScrollingChild、ScrollingView這三個接口,它既能夠用來做爲外控件,也能夠用來做爲內控件。bash

咱們先從入口函數startNestedScroll方法看起,它在NestedScrollView中調用的地方有如下三處:app

  • public boolean onInterceptTouchEvent(MotionEvent ev)
  • public boolean onTouchEvent(MotionEvent ev)
  • public void onNestedScrollAccepted(View child, View target, int nestedScrollAxes)

而在startNestedScroll又會調用mChildHelper/ViewstartNestedScroll方法,下面咱們來看一下它的實現,它遍歷它全部的祖先節點,並調用每一個節點的onStartNestedScroll(child, this,axes)方法,若是該方法返回了true,那麼就將他做爲嵌套滑動的外控件記錄下來,以後全部和外控件的交互都是經過mNestedScrollingParent來實現的,接下來調用它的onNestedScrollAccepted(child, this, axes)方法,並中止遍歷,返回true。若是它全部的祖先結點都不知足嵌套滑動的條件,那麼最終返回falseide

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;
                    }
                } catch (AbstractMethodError e) {
                    Log.e(VIEW_LOG_TAG, "ViewParent " + p + " does not implement interface " +
                            "method onStartNestedScroll", e);
                    // Allow the search upward to continue
                }
                if (p instanceof View) {
                    child = (View) p;
                }
                p = p.getParent();
            }
        }
        return false;
    }
複製代碼

接下來,咱們看一下mParentHelper/ViewGrouppublic boolean onStartNestedScroll(View child, View target, int nestedScrollAxes),它在ViewGroup默認值是返回false函數

@Override
    public boolean onStartNestedScroll(View child, View target, int nestedScrollAxes) {
        return false;
    }
複製代碼

而在NestedScrollView中的條件是:佈局

@Override
    public boolean onStartNestedScroll(View child, View target, int nestedScrollAxes) {
        return (nestedScrollAxes & ViewCompat.SCROLL_AXIS_VERTICAL) != 0;
    }
複製代碼

在接着調用的onNestedScrollAccepted中,ViewGroup記錄下axes的值:ui

@Override
    public void onNestedScrollAccepted(View child, View target, int axes) {
        mNestedScrollAxes = axes;
    }
複製代碼

NestedScrollView則會繼續調用startNestedScroll來尋找它的外控件:this

@Override
    public void onNestedScrollAccepted(View child, View target, int nestedScrollAxes) {
        mParentHelper.onNestedScrollAccepted(child, target, nestedScrollAxes);
        startNestedScroll(ViewCompat.SCROLL_AXIS_VERTICAL);
    }
複製代碼

總結:第一個階段主要是爲了尋找到嵌套滑動的外控件,並肯定滑動的方向。spa

5.2 收到move事件,交給外控件處理一部分的滑動距離

以後的滑動就須要經過public boolean onTouchEvent(MotionEvent ev)中的ACTION_MOVE來處理了,咱們來看一下NestedScrollView的處理邏輯:

case MotionEvent.ACTION_MOVE:
                final int activePointerIndex = MotionEventCompat.findPointerIndex(ev,
                        mActivePointerId);
                if (activePointerIndex == -1) {
                    Log.e(TAG, "Invalid pointerId=" + mActivePointerId + " in onTouchEvent");
                    break;
                }
                //1.得到當前的y座標
                final int y = (int) MotionEventCompat.getY(ev, activePointerIndex);
                //2.記錄該次滑動的距離
                int deltaY = mLastMotionY - y;
                //3.若是有外控件,那麼交給它先處理滑動事件,這裏傳入了3個參數:
                if (dispatchNestedPreScroll(0, deltaY, mScrollConsumed, mScrollOffset)) {
                    deltaY -= mScrollConsumed[1];
                    vtev.offsetLocation(0, mScrollOffset[1]);
                    mNestedYOffset += mScrollOffset[1];
                }
                if (!mIsBeingDragged && Math.abs(deltaY) > mTouchSlop) {
                    final ViewParent parent = getParent();
                    if (parent != null) {
                        parent.requestDisallowInterceptTouchEvent(true);
                    }
                    mIsBeingDragged = true;
                    if (deltaY > 0) {
                        deltaY -= mTouchSlop;
                    } else {
                        deltaY += mTouchSlop;
                    }
                }
                //.....
複製代碼

ViewdispatchNestedPreScroll,它經過先前保存下來的外控件變量,把當前滑動的距離傳給它來處理,在ViewGroup中這個函數什麼事情也沒有作,若是咱們要實現本身的嵌套滑動邏輯,那麼就要在這裏面進行處理:

public boolean dispatchNestedPreScroll(int dx, int dy,
            @Nullable @Size(2) int[] consumed, @Nullable @Size(2) int[] offsetInWindow) {
        if (isNestedScrollingEnabled() && mNestedScrollingParent != null) {
            if (dx != 0 || dy != 0) {
                int startX = 0;
                int startY = 0;
                if (offsetInWindow != null) {
                    getLocationInWindow(offsetInWindow);
                    startX = offsetInWindow[0];
                    startY = offsetInWindow[1];
                }

                if (consumed == null) {
                    if (mTempNestedScrollConsumed == null) {
                        mTempNestedScrollConsumed = new int[2];
                    }
                    consumed = mTempNestedScrollConsumed;
                }
                consumed[0] = 0;
                consumed[1] = 0;
                //調用父控件的接口,詢問它是否要消耗滑動事件.
                mNestedScrollingParent.onNestedPreScroll(this, dx, dy, consumed);

                if (offsetInWindow != null) {
                    getLocationInWindow(offsetInWindow);
                    offsetInWindow[0] -= startX;
                    offsetInWindow[1] -= startY;
                }
                return consumed[0] != 0 || consumed[1] != 0;
            } else if (offsetInWindow != null) {
                offsetInWindow[0] = 0;
                offsetInWindow[1] = 0;
            }
        }
        return false;
    }
複製代碼

這個階段的過程,能夠理解爲:

  • 獲得當前y座標的值
  • 根據上次y座標的值計算出此次滑動的距離deltaY
  • 把這個deltaY值交給外控件處理
  • 外控件返回兩個數組,mScrollConsumed表示該階段外控件消耗的距離,mScrollOffset表示本次交給外控件以後,內控件窗口變更的座標值,若是消耗的xy值不爲0,那麼該函數返回true
  • deltaY - mScrollConsumed[1]獲得內控件接下來要處理的距離。

5.3 外控件處理完滑動距離後,交給內控件滾動

if (mIsBeingDragged) {
                    // Scroll to follow the motion event
                    mLastMotionY = y - mScrollOffset[1];

                    final int oldY = getScrollY();
                    final int range = getScrollRange();
                    final int overscrollMode = ViewCompat.getOverScrollMode(this);
                    boolean canOverscroll = overscrollMode == ViewCompat.OVER_SCROLL_ALWAYS ||
                            (overscrollMode == ViewCompat.OVER_SCROLL_IF_CONTENT_SCROLLS &&
                                    range > 0);

                    // Calling overScrollByCompat will call onOverScrolled, which
                    // calls onScrollChanged if applicable.
                    if (overScrollByCompat(0, deltaY, 0, getScrollY(), 0, range, 0,
                            0, true) && !hasNestedScrollingParent()) {
                        // Break our velocity if we hit a scroll barrier.
                        mVelocityTracker.clear();
                    }
                    //.....
                 }
複製代碼

5.4 內控件滾動完畢後,交給外控件繼續處理

final int scrolledDeltaY = getScrollY() - oldY;
                    final int unconsumedY = deltaY - scrolledDeltaY;
                    if (dispatchNestedScroll(0, scrolledDeltaY, 0, unconsumedY, mScrollOffset)) {
                        mLastMotionY -= mScrollOffset[1];
                        vtev.offsetLocation(0, mScrollOffset[1]);
                        mNestedYOffset += mScrollOffset[1];
                    } else if (canOverscroll) {
                        //..
                    }
複製代碼

這裏調用了mChildHelper/ViewdispatchNestedScroll方法,它裏面會經過mNestedScrollingParent來通知外控件來處理剩餘的距離,在ViewGrouponNestedScroll方法中,什麼也沒有作:

public boolean dispatchNestedScroll(int dxConsumed, int dyConsumed,
            int dxUnconsumed, int dyUnconsumed, @Nullable @Size(2) int[] offsetInWindow) {
        if (isNestedScrollingEnabled() && mNestedScrollingParent != null) {
            if (dxConsumed != 0 || dyConsumed != 0 || dxUnconsumed != 0 || dyUnconsumed != 0) {
                int startX = 0;
                int startY = 0;
                if (offsetInWindow != null) {
                    getLocationInWindow(offsetInWindow);
                    startX = offsetInWindow[0];
                    startY = offsetInWindow[1];
                }

                mNestedScrollingParent.onNestedScroll(this, dxConsumed, dyConsumed,
                        dxUnconsumed, dyUnconsumed);

                if (offsetInWindow != null) {
                    getLocationInWindow(offsetInWindow);
                    offsetInWindow[0] -= startX;
                    offsetInWindow[1] -= startY;
                }
                return true;
            } else if (offsetInWindow != null) {
                // No motion, no dispatch. Keep offsetInWindow up to date.
                offsetInWindow[0] = 0;
                offsetInWindow[1] = 0;
            }
        }
        return false;
    }
複製代碼

5.5 收到up事件,中止嵌套滑動

經過調用stopNestedScroll方法來中止滑動:

  • public boolean onInterceptTouchEvent(MotionEvent ev)ACTION_UP
  • public boolean onTouchEvent(MotionEvent ev)ACTION_UPACTION_CANCEL

ViewstopNestedScroll方法中,調用外控件的onStopNestedScroll方法來通知它整個滑動結束:

public void stopNestedScroll() {
        if (mNestedScrollingParent != null) {
            mNestedScrollingParent.onStopNestedScroll(this);
            mNestedScrollingParent = null;
        }
    }
複製代碼

6、運用NestedScrollView

下面,咱們再經過一個簡單的例子,來看一下使用NestedScrollView的效果,佈局文件:

<?xml version="1.0" encoding="utf-8"?>
<android.support.design.widget.CoordinatorLayout
    xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    android:orientation="vertical"
    android:layout_width="match_parent"
    android:layout_height="match_parent">
    
    <!-- 標題部分 -->
    <android.support.design.widget.AppBarLayout
        android:id="@+id/appbar"
        android:layout_height="wrap_content"
        android:layout_width="match_parent">
        <android.support.v7.widget.Toolbar
            android:id="@+id/toolbar"
            app:layout_scrollFlags="scroll|enterAlways"
            android:background="@android:color/holo_blue_dark"
            android:layout_width="match_parent"
            android:layout_height="?attr/actionBarSize">
        </android.support.v7.widget.Toolbar>
    </android.support.design.widget.AppBarLayout>

    <!-- 內容部分 -->
    <android.support.v4.widget.NestedScrollView
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        app:layout_behavior="@string/appbar_scrolling_view_behavior">
        <LinearLayout
            android:orientation="vertical"
            android:layout_width="match_parent"
            android:layout_height="wrap_content">
            <TextView
                android:text="1"
                android:layout_width="match_parent"
                android:layout_height="200dp"/>
            <TextView
                android:text="2"
                android:layout_width="match_parent"
                android:layout_height="200dp"/>
            <TextView
                android:text="3"
                android:layout_width="match_parent"
                android:layout_height="200dp"/>
            <TextView
                android:text="4"
                android:layout_width="match_parent"
                android:layout_height="200dp"/>
        </LinearLayout>
    </android.support.v4.widget.NestedScrollView>
</android.support.design.widget.CoordinatorLayout>
複製代碼

咱們經過CoordinatorLayout把標題部分和內容部分包裹起來,這樣再滑動下面的NestedScrollView時,能夠實現標題欄的隱藏和顯示。

相關文章
相關標籤/搜索