淺析:Android 嵌套滑動機制(NestedScrolling)

谷歌在發佈安卓 Lollipop版本之後,爲了更好的用戶體驗,GoogleAndroid的滑動機制提供了NestedScrolling特性

NestedScrolling的特性可以體現在哪裏呢?
<!--[if !supportLineBreakNewLine]-->
<!--[endif]-->

比如你使用了Toolbar,下面一個ScrollView,向上滾動隱藏Toolbar,向下滾動顯示Toolbar,這裏在邏輯上就是一個NestedScrolling —— 因爲你在滾動整個Toolbar在內的View的過程中,又嵌套滾動了裏面的ScrollView
<!--[if !supportLineBreakNewLine]-->
<!--[endif]-->

在這之前,我們知道AndroidTouch事件的分發是有自己一套機制的。主要是有是三個函數:

dispatchTouchEventonInterceptTouchEventonTouchEvent

 

這種分發機制讓移動應用安全檢測平臺-愛內測(ineice.com)發現有一個漏洞,據愛內測的CTO介紹:

如果子view獲得處理touch事件機會的時候,父view就再也沒有機會去處理這個touch事件了,直到下一次手指再按下。

 

也就是說,我們在滑動子View的時候,如果子View對這個滑動事件不想要處理的時候,只能拋棄這個touch事件,而不會把這些傳給父view去處理。

 

但是Google新的NestedScrolling機制就很好的解決了這個問題。
<!--[if !supportLineBreakNewLine]-->
<!--[endif]-->

我們看看如何實現這個NestedScrolling,首先有幾個類(接口)我們需要關注一下

NestedScrollingChild

NestedScrollingParent

NestedScrollingChildHelper

NestedScrollingParentHelper

以上四個類都在support-v4包中提供,LollipopView默認實現了幾種方法。

 

實現接口很簡單,這邊我暫時用到了NestedScrollingChild系列的方法(因爲Parentsupport-design提供的CoordinatorLayout

    @Override

    public void setNestedScrollingEnabled(boolean enabled) {

        super.setNestedScrollingEnabled(enabled);

        mChildHelper.setNestedScrollingEnabled(enabled);

    }

 

    @Override

    public boolean isNestedScrollingEnabled() {

        return mChildHelper.isNestedScrollingEnabled();

    }

    @Override

    public boolean startNestedScroll(int axes) {

        return mChildHelper.startNestedScroll(axes);

    }

    @Override

    public void stopNestedScroll() {

        mChildHelper.stopNestedScroll();

    }

    @Override

    public boolean hasNestedScrollingParent() {

        return mChildHelper.hasNestedScrollingParent();

    }

    @Override

    public boolean dispatchNestedScroll(int dxConsumed, int dyConsumed, int dxUnconsumed, int dyUnconsumed, int[] offsetInWindow) {

        return mChildHelper.dispatchNestedScroll(dxConsumed, dyConsumed, dxUnconsumed, dyUnconsumed, offsetInWindow);

    }

    @Override

    public boolean dispatchNestedPreScroll(int dx, int dy, int[] consumed, int[] offsetInWindow) {

        return mChildHelper.dispatchNestedPreScroll(dx, dy, consumed, offsetInWindow);

    }

    @Override

    public boolean dispatchNestedFling(float velocityX, float velocityY, boolean consumed) {

        return mChildHelper.dispatchNestedFling(velocityX, velocityY, consumed);

    }

    @Override

    public boolean dispatchNestedPreFling(float velocityX, float velocityY) {

        return mChildHelper.dispatchNestedPreFling(velocityX, velocityY);

    }

對,簡單的話你就這麼實現就好了。

 

這些接口都是我們在需要的時候自己調用的。childHelper幹了些什麼事呢?,看一下startNestedScroll方法

    /**

     * Start a new nested scroll for this view.

     *

     * <p>This is a delegate method. Call it from your {@link android.view.View View} subclass

     * method/{@link NestedScrollingChild} interface method with the same signature to implement

     * the standard policy.</p>

     *

     * @param axes Supported nested scroll axes.

     *             See {@link NestedScrollingChild#startNestedScroll(int)}.

     * @return true if a cooperating parent view was found and nested scrolling started successfully

     */

    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交互的一些方法。

 

ViewParentCompat是一個和父view交互的兼容類,它會判斷api version,如果在Lollipop以上,就是用view自帶的方法,否則判斷是否實現了NestedScrollingParent接口,去調用接口的方法。

 

那麼具體我們怎麼使用這一套機制呢?比如子View這時候我需要通知父view告訴它我有一個嵌套的touch事件需要我們共同處理。那麼針對一個只包含scroll交互,它整個工作流是這樣的:

 

一、startNestedScroll

首先子view需要開啓整個流程(內部主要是找到合適的能接受nestedScrollparent),通知父View,我要和你配合處理TouchEvent

 

二、dispatchNestedPreScroll

在子ViewonInterceptTouchEvent或者onTouch(一般在MontionEvent.ACTION_MOVE事件裏),調用該方法通知父View滑動的距離。該方法的第三第四個參數返回父view消費掉的scroll長度和子View的窗體偏移量。如果這個scroll沒有被消費完,則子view進行處理剩下的一些距離,由於窗體進行了移動,如果你記錄了手指最後的位置,需要根據第四個參數offsetInWindow計算偏移量,才能保證下一次的touch事件的計算是正確的。

如果父view接受了它的滾動參數,進行了部分消費,則這個函數返回true,否則爲false

這個函數一般在子view處理scroll前調用。

 

三、dispatchNestedScroll

向父view彙報滾動情況,包括子view消費的部分和子view沒有消費的部分。

如果父view接受了它的滾動參數,進行了部分消費,則這個函數返回true,否則爲false

這個函數一般在子view處理scroll後調用。
<!--[if !supportLineBreakNewLine]-->
<!--[endif]-->

四、stopNestedScroll

結束整個流程。

整個對應流程是這樣

view   view

startNestedScroll onStartNestedScrollonNestedScrollAccepted

dispatchNestedPreScroll    onNestedPreScroll

dispatchNestedScroll  onNestedScroll

stopNestedScroll onStopNestedScroll

一般是子view發起調用,父view接受回調。

 

我們最需要關注的是dispatchNestedPreScroll中的consumed參數。

    public boolean dispatchNestedPreScroll(int dx, int dy, int[] consumed, int[] offsetInWindow) ;

 

它是一個int型的數組,長度爲2,第一個元素是父view消費的x方向的滾動距離;第二個元素是父view消費的y方向的滾動距離,如果這兩個值不爲0,則子view需要對滾動的量進行一些修正。正因爲有了這個參數,使得我們處理滾動事件的時候,思路更加清晰,不會像以前一樣被一堆的滾動參數搞混。

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