天天看點

Android嵌套滑動機制(NestedScrolling)

來源 http://segmentfault.com/a/1190000002873657

編輯推薦:稀土掘金,這是一個針對技術開發者的一個應用,你可以在掘金上擷取最新最優質的技術幹貨,不僅僅是Android知識、前端、後端以至于産品和設計都有涉獵,想成為全棧工程師的朋友不要錯過!

轉載這篇文章是因為

NestedScrolling

的文檔太少,雖然本文也是輕描淡寫,但是也是最詳細的了。不過文檔少也證明了

NestedScrolling

的功能很容易用其他方法替代,是以大家看看就好,不必太細究。

Android 在釋出 

Lollipop

版本之後,為了更好的使用者體驗,Google為Android的滑動機制提供了

NestedScrolling

特性

NestedScrolling

的特性可以展現在哪裡呢?

比如你使用了

Toolbar

,下面一個

ScrollView

,向上滾動隐藏

Toolbar

,向下滾動顯示

Toolbar

,這裡在邏輯上就是一個

NestedScrolling

 —— 因為你在滾動整個

Toolbar

在内的View的過程中,又

嵌套

滾動了裡面的

ScrollView

Android嵌套滑動機制(NestedScrolling)

效果如上圖【别嫌棄我】

在這之前,我們知道

Android

Touch

事件的分發是有自己一套機制的。主要是有是三個函數:

dispatchTouchEvent

onInterceptTouchEvent

onTouchEvent

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

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

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

  注:本站這種觀點值得商量。

但是Google新的

NestedScrolling

機制就很好的解決了這個問題。

我們看看如何實作這個

NestedScrolling

,首先有幾個類(接口)我們需要關注一下

NestedScrollingChild

NestedScrollingParent

NestedScrollingChildHelper

NestedScrollingParentHelper

以上四個類都在

support-v4

包中提供,Lollipop的View預設實作了幾種方法。

實作接口很簡單,這邊我暫時用到了

NestedScrollingChild

系列的方法(因為Parent是support-design提供的

CoordinatorLayout

  1.    @Override
  2.     public void setNestedScrollingEnabled(boolean enabled) {
  3.         super.setNestedScrollingEnabled(enabled);
  4.         mChildHelper.setNestedScrollingEnabled(enabled);
  5.     }
  6.     @Override
  7.     public boolean isNestedScrollingEnabled() {
  8.         return mChildHelper.isNestedScrollingEnabled();
  9.     }
  10.     @Override
  11.     public boolean startNestedScroll(int axes) {
  12.         return mChildHelper.startNestedScroll(axes);
  13.     }
  14.     @Override
  15.     public void stopNestedScroll() {
  16.         mChildHelper.stopNestedScroll();
  17.     }
  18.     @Override
  19.     public boolean hasNestedScrollingParent() {
  20.         return mChildHelper.hasNestedScrollingParent();
  21.     }
  22.     @Override
  23.     public boolean dispatchNestedScroll(int dxConsumed, int dyConsumed, int dxUnconsumed, int dyUnconsumed, int[] offsetInWindow) {
  24.         return mChildHelper.dispatchNestedScroll(dxConsumed, dyConsumed, dxUnconsumed, dyUnconsumed, offsetInWindow);
  25.     }
  26.     @Override
  27.     public boolean dispatchNestedPreScroll(int dx, int dy, int[] consumed, int[] offsetInWindow) {
  28.         return mChildHelper.dispatchNestedPreScroll(dx, dy, consumed, offsetInWindow);
  29.     }
  30.     @Override
  31.     public boolean dispatchNestedFling(float velocityX, float velocityY, boolean consumed) {
  32.         return mChildHelper.dispatchNestedFling(velocityX, velocityY, consumed);
  33.     }
  34.     @Override
  35.     public boolean dispatchNestedPreFling(float velocityX, float velocityY) {
  36.         return mChildHelper.dispatchNestedPreFling(velocityX, velocityY);
  37.     }

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

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

startNestedScroll

方法

  1.     /**
  2.      * Start a new nested scroll for this view.
  3.      *
  4.      * <p>This is a delegate method. Call it from your {@link android.view.View View} subclass
  5.      * method/{@link NestedScrollingChild} interface method with the same signature to implement
  6.      * the standard policy.</p>
  7.      *
  8.      * @param axes Supported nested scroll axes.
  9.      *             See {@link NestedScrollingChild#startNestedScroll(int)}.
  10.      * @return true if a cooperating parent view was found and nested scrolling started successfully
  11.      */
  12.     public boolean startNestedScroll(int axes) {
  13.         if (hasNestedScrollingParent()) {
  14.             // Already in progress
  15.             return true;
  16.         }
  17.         if (isNestedScrollingEnabled()) {
  18.             ViewParent p = mView.getParent();
  19.             View child = mView;
  20.             while (p != null) {
  21.                 if (ViewParentCompat.onStartNestedScroll(p, child, mView, axes)) {
  22.                     mNestedScrollingParent = p;
  23.                     ViewParentCompat.onNestedScrollAccepted(p, child, mView, axes);
  24.                     return true;
  25.                 }
  26.                 if (p instanceof View) {
  27.                     child = (View) p;
  28.                 }
  29.                 p = p.getParent();
  30.             }
  31.         }
  32.         return false;
  33.     }

可以看到這裡是幫你實作一些跟

NestedScrollingParent

互動的一些方法。

ViewParentCompat

是一個和父view互動的相容類,它會判斷api version,如果在Lollipop以上,就是用view自帶的方法,否則判斷是否實作了

NestedScrollingParent

接口,去調用接口的方法。

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

一、startNestedScroll

首先子view需要開啟整個流程(内部主要是找到合适的能接受nestedScroll的parent),通知父View,我要和你配合處理TouchEvent

二、dispatchNestedPreScroll

在子View的

onInterceptTouchEvent

或者

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後調用。

四、stopNestedScroll

結束整個流程。

整個對應流程是這樣

子view 父view
startNestedScroll onStartNestedScroll、onNestedScrollAccepted
dispatchNestedPreScroll onNestedPreScroll
dispatchNestedScroll onNestedScroll
stopNestedScroll onStopNestedScroll

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

我們最需要關注的是

dispatchNestedPreScroll

中的

consumed

參數。

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

它是一個

int

型的數組,長度為2,第一個元素是父view消費的

x

方向的滾動距離;第二個元素是父view消費的

y

方向的滾動距離,如果這兩個值不為0,則子view需要對滾動的量進行一些修正。正因為有了這個參數,使得我們處理滾動事件的時候,思路更加清晰,不會像以前一樣被一堆的滾動參數搞混。

對NestedScroll的介紹暫時到這裡,下一次将講一下

CoordinatorLayout

的使用(其中讓人較難了解的Behavior對象),以及在

SegmentFault Android用戶端

中的實踐。謝謝支援。

繼續閱讀