十分鐘理解 Android 中的嵌套滾動機制

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

從是什么開始

我們先來看一個動圖,直觀的感受下什么是嵌套滾動(nested scrolling):

既然是嵌套,就說明是一層套著一層,存在兩個滾動行為。在上圖中,當我們滾動下面的UI控件時,先滾動的卻是外頭的父容器,當父容器滾動到一定程度后,下面的UI控件才開始滾動。這樣看來,確實存在著兩個滾動行為。

那么嵌套關系體現在哪呢?我們先來看下實現上圖效果的布局文件結構:

<ParentView>

  <ImageView />

  <NestedScrollView>
    <WebView />
  </NestedScrollView>

</CoordinatorLayout>

可以看到,ImageView和NestedScrollView都是ParentView的子View,ParentView使我們自定義的一個繼承自LinearLayout的布局管理器。實際上,ParentView類實現了NestedScrollingParent接口,NestedScrollView實現了NestedScrollingChild接口。從名字上我們可以做出這樣的猜測:嵌套滾動中需要一個子View和一個作為容器的父View,子View需要實現NestedScrollingChild接口,父View需要實現NestedScrollingParent接口。分別實現了上述兩個接口的父View把子View套起來,就可以實現所謂的嵌套滾動。

現在,我們知道了嵌套滾動中的兩個主角:

  • 一個實現了NestedScrollingParent的父容器,在本文的其余部分,我們簡稱為nestedParent;

  • 一個實現了NestedScrollingChild的子View,下文簡稱為nestedChild。

本文接下來的部分會以上圖效果為例,講解嵌套滾動究竟是如何實現的。

NestedScrollingParent接口

我們來看下本文例子中會涉及到的NestedScrollingParent接口中的方法:

  • onStartNestedScroll(View child, View target, int nestedScrollAxes) :當nestedChild想要進行嵌套滾動時,會調用nestedParent的這個方法。這個芳法用于指示是否支持嵌套滾動,比如我們只想支持垂直方向上的嵌套滾動,可以在nestedParent中這樣實現這個方法:

@Overridepublic boolean onStartNestedScroll(View child, View target, int nestedScrollAxes) {    
  if (nestedScrollAxes == ViewCompat.SCROLL_AXIS_VERTICAL) {        
    return true;    
  }    
  return false;
}
  • onNestedPreScroll(View target, int dx, int dy, int[] consumed) :當我們滾動nestedChild時,nestedChild進行實際的滾動前,會先調用nestParent的這個方法。nestedParent在這個方法中可以把子View想要滾動的距離消耗掉一部分或是全部消耗,比如我們的例子中,當我們向上滾動nestedChild時,nestParent會搶在它前頭先滾動,直到ImageView完全隱藏,才讓nestedChild開始滾動。

在這個例子中,我們自定義的ParentView繼承了LinearLayout類,實現了NestedScrollingParent接口,并重寫了上面提到的兩個方法。相關代碼如下:

public class ParentView extends LinearLayout implements NestedScrollingParent{    
  int ivHeight = 300;
  . . . 
  @Override
  public boolean onStartNestedScroll(...) {
    . . .
  }    
  @Override
  public void onNestedPreScroll(...,int dy,int[] consumed) {        
    if ((dy > 0 && getScrollY() < ivHeight) ||
        (dy < 0 && getScrollY() > 0)) {
      consumed[1] = dy;
      scrollBy(dx, dy);
    }
  }

}

onStartNestedScroll()方法的實現上面我們已經介紹過,這里不再贅述。簡單說下onNestedPreScroll()方法的實現。不過在這之前我們補下滾動相關的知識。

滾動相關知識補充

先來看一張圖:

在上圖中,黑色邊框代表了View的邊框,藍色邊框代表了View的內容的邊框。其實我們平時對ListView等控件進行滾動時,實際滾動的是View的內容。比如在上圖中,我們向右滾動一個控件,可以看到,實際上是它的內容向右進行滾動了,View的邊界線的位置始終是固定的。上圖中藍色右邊框和黑色右邊框間的距離就是View滾動的距離。

每個View都有名為mScrollX和mScrollY的兩個成員變量,前者記錄了View在水平方向上滾動的距離,后者記錄了View在豎直方向上滾動的距離。mScrollX的絕對值為View的左邊框與View的內容的左邊框的距離,當View向左滾動時,mScrollX是正的;當View向右滾動時,mScrollX是負的。mScrollY的絕對值為View的上邊框與View的內容的上邊框的距離,當View向上滾動時,mScrollY是正的;當View向下滾動時,mScrollY是負的。

理解了上面的內容后,讓我們看看onNestedPreScroll()為什么要像上面那樣實現。

在onNestedPreScroll()方法中,參數dy代表了本次NestedScrollView想要滑動的距離。若我們向上滑動NestedScrollView,dy就是正的,向下就是負的。getScorllY()會返回ParentView的mScrollY參數,為正則表示當前ParentView的內容已經向上滾動了一段距離,否則表示向下滾動過一段距離。ivHeight表示ImageView的高度。理解了上面這些,這個方法的邏輯就很好理解了,這里不再贅述。

NestedScrollingChild接口

我們在布局文件中使用的NestedScrollView就實現了NestedScrollingChild接口。當我們滾動nestedChild時,這個接口的方法會先于nestedParent中的方法被調用。這里我們介紹下本文例子中涉及到的方法:

  • startNestedScroll(int axes) :開始沿著參數中指定的方向(水平 or 垂直)進行嵌套滾動

  • dispatchNestedPreScroll(...) :這個方法會調用nestedParent的onNestedPreScroll()方法。這樣就使得nestedParent有機會搶在NestedScroll之前消耗滾動事件。

嵌套滾動工作原理探索

現在相信各位同學都了解了如何實現基本的嵌套滾動,那么大家是否能夠猜到它的實現原理呢?實際上,是nestedChild的onTouchEvent()方法中會對發生的Touch事件進行判斷,若為DOWN事件則會調用startNestedScroll()方法;若為MOVE事件則會調用dispatchNestedPreScroll()方法。我們來看下NestedScrollView的onTouchEvent()方法:

public boolean onTouchEvent(MotionEvent ev) {
  . . . 
  final int actionMasked = . . .;
  . . . 
  switch (actionMasked) {    
    case MotionEvent.ACTION_DOWN: {
      . . .
      startNestedScroll(ViewCompat.SCROLL_AXIS_VERTICAL);      
      break;
    }    
    case MotionEvent.ACTION_MOVE:
      . . .      
      if (dispatchNestedPreScroll(...) {
        deltaY -= mScrollConsumed[1];
        . . .
      }
  . . .
}

我們可以看到,對于DOWN事件,確實會調用startNestedScroll()方法;在MOVE事件時,調用了dispatchNestedPreScroll()方法,deltaY表示nestedChild實際應該滾動的距離,我們可以看到它的值是本該滾動的距離減去nestedParent已經消耗掉的距離。

到這里,對嵌套滾動的啟蒙介紹就完畢了:

 

來自:http://mp.weixin.qq.com/s?__biz=MzIzMjE1Njg4Mw==&mid=2650117793&idx=1&sn=3d9bc24a0138be98521ad4b952535ff3&chksm=f0980d1dc7ef840b3e6ad3db76be4d7133d79307581164b8f4a31ad4acaebd0318feb3fe98cc

 

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