NestedScrolling(Android嵌套滑動機制)

終於要在segmentfault寫第一篇文章了,好雞凍,這篇文章原本打算寫在簡書上的,可是因爲頁面不能富文本和markdown同時支持,看到Gemini大神的文章中酷炫、賞心悅目的效果後果斷放棄簡書,看文章原本就會枯燥,若是再沒有美觀的效果,那豈不是要邊看邊睡? 互聯網給了咱們這麼多選擇,那我確定選擇體驗最棒的。感謝segmentfaulthtml

具體效果能夠對比一下:
segmentfault效果
簡書效果android

重點文字標記在segmentfault上支持code標籤,簡書上最多隻能經過粗體實現。(反正我是沒有找到更好的方法)segmentfault

說到Gemini,我也是這兩天由於瞭解NestedScrolling時接觸到的,粗略看了一下資料和文章瀏覽數,贊! 個人大神!數組

好,前番就到這了,開始正題NestedScrollingmarkdown

以前瞭解NestedScrolling的時候看過一些博客,其中就包括Gemini的segmentfault,當時看的時候由於不仔細不覺得然,最後才發現這篇博客是對NestedScrolling介紹最清楚的,做爲懲罰也好膜拜也罷,把原本能夠cv過來的博客手動敲一遍,順便補充一下本身的一些額外理解。ide

再次感謝Gemini函數


Android 在發佈 Lillipop 版本後,爲了更好的用戶體驗,Google爲Android的滑動機制提供了NestedScrolling機制。oop

NestedScrolling的特性能夠體如今哪兒呢?
好比你用了Toolbar,下面一個ScrollView,向上滾動隱藏Toolbar,向下滾動顯示Toolbar,這裏在邏輯上就是一個NestedScrolling ——由於你在滾動整個Toolbar在內的View的過程當中,又嵌套滾動了裏邊的ScrollViewthis

如圖:
NestedScrolling_sample_gifgoogle

在這以前,咱們知道Android對Touch事件分發是有本身的一套機制。主要是有三個函數:
dispatchTouchEvent, onInterceptTouchEvent, onTouchEvent

這種分發機制有一個漏洞:

若是子view得到處理touch事件機會的時候,父view就再也沒有機會處理這次touch事件,直到下一次手指觸發。

也就是說,咱們在滑動子view的時候,若是子view對這個滑動事件不須要處理的時候,只能拋棄這個touch事件,而不會傳給父view去處理。

但Google新的NestedScrolling機制就很好的解決了這個問題。

NestedScrolling主要有四個類須要關注:

NestedScrollingChild
NestedScrollingChildHelper
NestedScrollingParent
NestedScrollingParentHelper

以上四個類都在support-v4包中提供,Lollipop中部分View默認實現了NestedScrollingChildNestedScrollingParent

v4包中NestedScrollView同時實現了NestedScrollingChild和NestedScrollingParent。

通常實現NestedScrollingChild就能夠了,父View用support-design提供的實現了NestedScrollingParentCoordinatorLayout便可。

@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);
    }

簡單邏輯這樣就能夠實現嵌套滑動。

以上接口都是業務邏輯中本身調用,NestedScrollingChildHelper是如何實現的呢? 先看一下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的交互流程以下:

1、startNestedScroll

首先子View須要開啓整個流程(經過屏幕滑動觸發touch事件),經過NestedScrollingChildHelper找到並通知實現了NestedScrollingParent的父View中onStartNestedScrollonNestedScrollAccepted方法。

2、dispatchNestedPreScroll

在子View的onIterceptTouchEventonTouch中(通常在MontionEvent.ACTION_MOVE事件裏),調用該方法通知父View的滑動距離,該方法的第三第四個參數返回父View消費掉的scroll長度和子View的窗口偏移量,若是這個scroll沒有被消費完,則子View處理剩餘距離,因爲窗口被移動,若是記錄了手指最後的位置,須要根據第四個參數offsetInWindow計算偏移量,才能保證下一次touch事件的計算是正確的。

若是父View接受了滾動參數並部分消費,則該函數返回true,不然返回false。
該函數通常在子View處理Scroll前調用。

3、dispatchNestedScroll

向父View彙報滾動狀況,包括子View已消費和未消費的值。
若是父View接受了滾動參數,部分消費則函數返回true,不然返回false。
該函數通常在子View處理Scroll後調用。

4、stopNestedScroll

結束整個嵌套滑動流程。

流程中NestedScrollingChildNestedScrollingParent對應以下:

NestedScrollingChildImpl NestedScrollingParentImpl
onStartNestedScroll onStartNestedScroll, onNestedScrollAccepted
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已消費滾動距離,則須要對子View滾動距離進行修正,正由於有該參數,使得處理滾動事件時思路更加清晰,不會像之前同樣被一堆滾動參數搞混。


本身理解的NestedScrolling簡要流程圖(不包含Fling事件及返回值的邏輯):

NestedScrolling簡要流程圖


鳴謝:

Gemini大神segmentFault博客

crainzy CSDN博客

相關文章
相關標籤/搜索