android ui 學(xué)習(xí)系列 -自定義Behavior (1) - 相關(guān)原理

寫在開頭

不吐不快啊,關(guān)于自定義Behavior 這個東東真是讓我撓頭啊,N 多回調(diào)方法,看了很多資料,這 N 多回調(diào)方法差不多清楚了,但是又出來一個 view 依賴綁定,概念有依賴和依賴的 view,到底誰提供滾動事件,誰來消費(fèi)滾動事件,這個仔細(xì)看了那是很多資料才算是明白。那么問題又來了,2個 view 要建立依賴關(guān)系,才能實(shí)現(xiàn)滾動聯(lián)動,那么為啥我寫一個自定義 Behavior 消費(fèi)滾動事件了,我這個自定義 Behavior 沒有和任何 view 實(shí)現(xiàn)依賴綁定啊......

這里面一堆的相關(guān)概念和原理,真是讓人撓頭啊,真是費(fèi)了不少時間找資料,才總算是搞明白了,在這里說一句 : 真 NM 費(fèi)勁 ?。?!

在這里非常感謝這篇文章:

把 5.0 的 nestedScrolling 嵌套滾動和Behavior解釋的很清楚。下面的內(nèi)容我也是把文章里面 大段的理論簡單描述一下,便于理解,還是推薦大家去詳細(xì)看這篇文章,想要搞懂 5.0 的 nestedScrolling 嵌套滾動,非這篇博文不行。


NestedScrollingParent # NestedScrollingChild

在以前,我們要實(shí)現(xiàn)控件滾動間的聯(lián)動,只能去監(jiān)聽滾動控件,在這個滾動控件上注冊監(jiān)聽器,或者是寫一個自定義 view,在滾動事件里去操作其他的 view,實(shí)現(xiàn)狀態(tài)變換。這樣呢滾動效果代碼就和具體的業(yè)務(wù)代碼放在一起了,無法分離,自定義 view 的方式也是很不靈活,所以呢隨著 5.0的推出,在 MD 中 google 推出了一套新的滾動監(jiān)聽套件,就是標(biāo)題中的 NestedScrollingParent / NestedScrollingChild

對于控件滾動間的聯(lián)動效果來說,分2種角色:一種是產(chǎn)生發(fā)送滾動事件,另一種是消費(fèi)滾動事件,因此 google 對這2種角色分別抽象除了接口:

  • NestedScrollingChild :產(chǎn)生發(fā)送滾動事件
  • NestedScrollingParent :消費(fèi)滾動事件

其中分別有 NestedScrollingParentHelper / NestedScrollingChildrenHelper輔助類來幫助處理的大部分邏輯


NestedScrollView.png

借著這張圖我們可以看到這2個接口全部信息和所有回調(diào)方法。

child 的滾動邏輯如下:

滾動的簡單邏輯順序:

  • 如果要準(zhǔn)備開始滑動了,需要告訴 Parent,Child 要準(zhǔn)備進(jìn)入滑動狀態(tài)了,調(diào)用
    startNestedScroll()。
  • Child 在滑動之前,先問一下你的 Parent 是否需要滑動,也就是調(diào)用
    dispatchNestedPreScroll()。如果父類消耗了部分滑動事件,Child 需要重新計(jì)算一下父類消耗后剩下給 Child 的滑動距離余量。然后,Child 自己進(jìn)行余下的滑動。
  • 最后,如果滑動距離還有剩余,Child 就再問一下,Parent 是否需要在繼續(xù)滑動你剩下的距離,也就是調(diào)用 dispatchNestedScroll()。

從事件分發(fā)的角度來看:

  • case MotionEvent.ACTION_DOWN:
    • child 先回調(diào) startNestedScroll 方法,里面?zhèn)魅氲氖菨L動方向,startNestedScroll(ViewCompat.SCROLL_AXIS_VERTICAL)
    • startNestedScroll 方法又會去詢問 Parent 的 onStartNestedScroll / onNestedScrollAccepted 方法,只要 Parent 愿意優(yōu)先處理這次的滑動事件,在結(jié)束的時候 Parent 還會收到 onStopNestedScroll 回調(diào)
  • case MotionEvent.ACTION_MOVE:
    • 在消費(fèi)滾動事件之前,會提供一個讓 Parent 實(shí)現(xiàn)聯(lián)合滾動的機(jī)會,因此在 child 滾動之前, Parent 可以消費(fèi)一部分或者全部的滑動事件,注意若是 parent 先消費(fèi)了部分滾動數(shù)值, child 是無效再去消費(fèi)這部分滾動數(shù)值的
      • child 調(diào)用 dispatchNestedPreScroll 方法
      • dispatchNestedPreScroll 方法會調(diào)用 parent 的 onNestedPreScroll
    • 在 child 消費(fèi)完滾動數(shù)值后,會再去詢問 parent 還要不要滾動
      • child 調(diào)用 dispatchNestedScroll 方法
      • dispatchNestedScroll 方法會調(diào)用 parent 的 onNestedScroll 方法
  • case MotionEvent.ACTION_CANCEL | MotionEvent.ACTION_UP:
    • 在滾動結(jié)束后,會分別調(diào)用 child 和 parent 的 stopNestedScroll方法
      • stopNestedScroll();

parent 的滾邏輯如下:

  • 滑動動作是 Child主動發(fā)起的,Parent 接收滑動回調(diào)并作出響應(yīng)。從上面的 Child 分析可知,滑動開始的調(diào)用 startNestedScroll(),Parent收到 onStartNestedScroll() 回調(diào),決定是否需要配合 Child 一起進(jìn)行處理滑動,如果需要配合,還會回調(diào) onNestedScrollAccepted()

  • 每次滑動前,Child 先詢問 Parent 是否需要滑動,即 dispatchNestedPreScroll() ,這就回調(diào)到 Parent 的 onNestedPreScroll(),Parent 可以在這個回調(diào)中消費(fèi)掉 Child 的 Scroll 事件,也就是優(yōu)先于 Child 滑動

  • Child 滑動以后,會調(diào)用 dispatchNestedScroll() ,回調(diào)到 Parent 的 onNestedScroll() ,這里就是 Child 滑動后,剩下的給 Parent 處理,也就是后于 Child 滑動

  • 最后,滑動結(jié)束 Child 調(diào)用 stopNestedScroll,回調(diào) Parent 的 onStopNestedScroll() 表示本次處理結(jié)束

其實(shí)不寫 parent 的邏輯思路 ,單看 child 的也能知道的。

總之我寫的比較簡單,這樣便于理解,想看詳細(xì)的去看上面貼出的地址,那篇文章寫的很詳細(xì)。這就是 Google 新的嵌套滾動的核心,在具體滾動控件消費(fèi)滾動數(shù)值的前后都去問問有誰需要消費(fèi)滾動數(shù)值,這樣就實(shí)現(xiàn)了聯(lián)動的效果。你可以選擇我們先消費(fèi)滾動事件人,然后具體的滾動控件再滾動剩下的值?;蛘哌x擇跟隨滾動控件滾動之后再消費(fèi)滾動值。

Behavior 扮演的角色

上面說了 google 的 nestedScrolling 嵌套滾動的實(shí)現(xiàn)思路,那么 Behavior 具體在這期中是個什么位置呢,為啥我們要使用 Behavior 呢

從 MD 的控件使用思路上來看,NestedScrollView / RecyclerView 實(shí)現(xiàn)了 NestedScrollingChild 接口,發(fā)送滾動事件。CoordinatorLayout 一定要作跟布局使用,CoordinatorLayout 實(shí)現(xiàn)了 NestedScrollingParent 接口,他遍歷所有的直接子 view,找到期中實(shí)現(xiàn)了 NestedScrollingChild 接口的可滾動 view, 然后把自己 set 進(jìn)去,實(shí)現(xiàn)和可滾動控件的綁定,進(jìn)而可以作為跟容器分發(fā)滾動事件,至于如何分發(fā)滾動事件,這里就用到 Behavior 了。CoordinatorLayout 本身并不直接實(shí)現(xiàn) NestedScrollingChild 接口,而是把相關(guān)方法再次抽象成一個 Behavior 接口拋出去,交給需要的直接子 view 去實(shí)現(xiàn),自己作為 Behavior 接口的代理,NestedScrollingParent 的每個方法 CoordinatorLayout 都會遍歷所有直接子 view 獲取其中的 Behavior ,執(zhí)行對應(yīng)的方法,從而實(shí)現(xiàn)在跟節(jié)點(diǎn)上分發(fā)滾動事件。

我們來看一個 CoordinatorLayout 具體的方法:

   // 參數(shù)child:當(dāng)前實(shí)現(xiàn)`NestedScrollingParent`的ViewParent包含觸發(fā)嵌套滾動的直接子view對象
   // 參數(shù)target:觸發(fā)嵌套滾動的view  (在這里如果不涉及多層嵌套的話,child和target)是相同的
   // 參數(shù)nestedScrollAxes:就是嵌套滾動的滾動方向了.垂直或水平方法
   //返回參數(shù)代表當(dāng)前ViewParent是否可以觸發(fā)嵌套滾動操作
   //CoordiantorLayout的實(shí)現(xiàn)上是交由子View的Behavior來決定,并回調(diào)了各個acceptNestedScroll方法,告訴它們處理結(jié)果
    public boolean onStartNestedScroll(View child, View target, int nestedScrollAxes) {
        boolean handled = false;

        final int childCount = getChildCount();
        for (int i = 0; i < childCount; i++) {
            final View view = getChildAt(i);
            final LayoutParams lp = (LayoutParams) view.getLayoutParams();
            final Behavior viewBehavior = lp.getBehavior();
            if (viewBehavior != null) {
                final boolean accepted = viewBehavior.onStartNestedScroll(this, view, child, target,nestedScrollAxes);
                handled |= accepted;
                lp.acceptNestedScroll(accepted);
            } else {
                lp.acceptNestedScroll(false);
            }
        }
        return handled;
    }

可以很明顯的看到 CoordiantorLayout 就是遍歷了所有的字節(jié)子 view,獲取到子View的Behavior 來回調(diào)了相關(guān)方法。

繼續(xù)看圖:


NestedScroll2.png

Behavior 方法大全

從上面我們知道了 Behavior 實(shí)現(xiàn)的都是 CoordinatorLayout 拋出來的 NestedScrollingParent 接口的具體實(shí)現(xiàn),當(dāng)然肯定發(fā)還有其他的一些方法,這里我們先來看一看,最好結(jié)合上面我們講的 parent 的邏輯順序來看,你會發(fā)現(xiàn)簡單好理解多了

Behavior 提供了幾個重要的方法:

  • layoutDependsOn
  • onDependentViewChanged
  • onStartNestedScroll
  • onNestedPreScroll
  • onNestedScroll
  • onStopNestedScroll
  • onNestedScrollAccepted
  • onNestedPreFling
  • onStartNestedScroll
  • onLayoutChild
/**
     * 表示是否給應(yīng)用了Behavior 的View 指定一個依賴的布局,通常,當(dāng)依賴的View 布局發(fā)生變化時
     * 不管被被依賴View 的順序怎樣,被依賴的View也會重新布局
     * @param parent
     * @param child 綁定behavior 的View
     * @param dependency   依賴的view
     * @return 如果child 是依賴的指定的View 返回true,否則返回false
     */
    @Override
    public boolean layoutDependsOn(CoordinatorLayout parent, View child, View dependency) {
        return super.layoutDependsOn(parent, child, dependency);
    }

    /**
     * 當(dāng)被依賴的View 狀態(tài)(如:位置、大小)發(fā)生變化時,這個方法被調(diào)用
     * @param parent
     * @param child
     * @param dependency
     * @return
     */
    @Override
    public boolean onDependentViewChanged(CoordinatorLayout parent, View child, View dependency) {
        return super.onDependentViewChanged(parent, child, dependency);
    }

    /**
     *  當(dāng)coordinatorLayout 的子View試圖開始嵌套滑動的時候被調(diào)用。當(dāng)返回值為true的時候表明
     *  coordinatorLayout 充當(dāng)nested scroll parent 處理這次滑動,需要注意的是只有當(dāng)返回值為true
     *  的時候,Behavior 才能收到后面的一些nested scroll 事件回調(diào)(如:onNestedPreScroll、onNestedScroll等)
     *  這個方法有個重要的參數(shù)nestedScrollAxes,表明處理的滑動的方向。
     *
     * @param coordinatorLayout 和Behavior 綁定的View的父CoordinatorLayout
     * @param child  和Behavior 綁定的View
     * @param directTargetChild
     * @param target
     * @param nestedScrollAxes 嵌套滑動 應(yīng)用的滑動方向,看 {@link ViewCompat#SCROLL_AXIS_HORIZONTAL},
     *                         {@link ViewCompat#SCROLL_AXIS_VERTICAL}
     * @return
     */
    @Override
    public boolean onStartNestedScroll(CoordinatorLayout coordinatorLayout, View child, View directTargetChild, View target, int nestedScrollAxes) {
        return super.onStartNestedScroll(coordinatorLayout, child, directTargetChild, target, nestedScrollAxes);
    }

    /**
     * 嵌套滾動發(fā)生之前被調(diào)用
     * 在nested scroll child 消費(fèi)掉自己的滾動距離之前,嵌套滾動每次被nested scroll child
     * 更新都會調(diào)用onNestedPreScroll。注意有個重要的參數(shù)consumed,可以修改這個數(shù)組表示你消費(fèi)
     * 了多少距離。假設(shè)用戶滑動了100px,child 做了90px 的位移,你需要把 consumed[1]的值改成90,
     * 這樣coordinatorLayout就能知道只處理剩下的10px的滾動。
     * @param coordinatorLayout
     * @param child
     * @param target
     * @param dx  用戶水平方向的滾動距離
     * @param dy  用戶豎直方向的滾動距離
     * @param consumed
     */
    @Override
    public void onNestedPreScroll(CoordinatorLayout coordinatorLayout, View child, View target, int dx, int dy, int[] consumed) {
        super.onNestedPreScroll(coordinatorLayout, child, target, dx, dy, consumed);
    }

    /**
     * 進(jìn)行嵌套滾動時被調(diào)用
     * @param coordinatorLayout
     * @param child
     * @param target
     * @param dxConsumed target 已經(jīng)消費(fèi)的x方向的距離
     * @param dyConsumed target 已經(jīng)消費(fèi)的y方向的距離
     * @param dxUnconsumed x 方向剩下的滾動距離
     * @param dyUnconsumed y 方向剩下的滾動距離
     */
    @Override
    public void onNestedScroll(CoordinatorLayout coordinatorLayout, View child, View target, int dxConsumed, int dyConsumed, int dxUnconsumed, int dyUnconsumed) {
        super.onNestedScroll(coordinatorLayout, child, target, dxConsumed, dyConsumed, dxUnconsumed, dyUnconsumed);
    }

    /**
     *  嵌套滾動結(jié)束時被調(diào)用,這是一個清除滾動狀態(tài)等的好時機(jī)。
     * @param coordinatorLayout
     * @param child
     * @param target
     */
    @Override
    public void onStopNestedScroll(CoordinatorLayout coordinatorLayout, View child, View target) {
        super.onStopNestedScroll(coordinatorLayout, child, target);
    }

    /**
     * onStartNestedScroll返回true才會觸發(fā)這個方法,接受滾動處理后回調(diào),可以在這個
     * 方法里做一些準(zhǔn)備工作,如一些狀態(tài)的重置等。
     * @param coordinatorLayout
     * @param child
     * @param directTargetChild
     * @param target
     * @param nestedScrollAxes
     */
    @Override
    public void onNestedScrollAccepted(CoordinatorLayout coordinatorLayout, View child, View directTargetChild, View target, int nestedScrollAxes) {
        super.onNestedScrollAccepted(coordinatorLayout, child, directTargetChild, target, nestedScrollAxes);
    }

    /**
     * 用戶松開手指并且會發(fā)生慣性動作之前調(diào)用,參數(shù)提供了速度信息,可以根據(jù)這些速度信息
     * 決定最終狀態(tài),比如滾動Header,是讓Header處于展開狀態(tài)還是折疊狀態(tài)。返回true 表
     * 示消費(fèi)了fling.
     *
     * @param coordinatorLayout
     * @param child
     * @param target
     * @param velocityX x 方向的速度
     * @param velocityY y 方向的速度
     * @return
     */
    @Override
    public boolean onNestedPreFling(CoordinatorLayout coordinatorLayout, View child, View target, float velocityX, float velocityY) {
        return super.onNestedPreFling(coordinatorLayout, child, target, velocityX, velocityY);
    }

    //可以重寫這個方法對子View 進(jìn)行重新布局
    @Override
    public boolean onLayoutChild(CoordinatorLayout parent, View child, int layoutDirection) {
        return super.onLayoutChild(parent, child, layoutDirection);
    }

或者看這個理解,這個好理解:

  • NestedScrollingParent : 下文簡稱 NSP

  • NestedScrollingChild : 下文簡稱 NSC

  • onStartNestedScroll
    用戶按下手指時觸發(fā),詢問 NSP 是否要處理這次滑動操作,如果返回 true 則表示“我要處理這次滑動”,如果返回 false 則表示“我不 care 你的滑動,你想咋滑就咋滑”,后面的一系列回調(diào)函數(shù)就不會被調(diào)用了。它有一個關(guān)鍵的參數(shù),就是滑動方向,表明了用戶是垂直滑動還是水平滑動,本例子只需考慮垂直滑動,因此判斷滑動方向?yàn)榇怪睍r就處理這次滑動,否則就不 care

  • onNestedScrollAccepted
    當(dāng) NSP 接受要處理本次滑動后,這個回調(diào)被調(diào)用,我們可以做一些準(zhǔn)備工作,比如讓之前的滑動動畫結(jié)束。

  • onNestedPreScroll
    當(dāng) NSC 即將被滑動時調(diào)用,在這里你可以做一些處理。值得注意的是,這個方法有一個參數(shù) int[] consumed,你可以修改這個數(shù)組來表示你到底處理掉了多少像素。假設(shè)用戶滑動了 100px,你做了 90px 的位移,那么就需要把 consumed[1] 改成 90(下標(biāo) 0、1 分別對應(yīng) x、y 軸),這樣 NSC 就能知道,然后繼續(xù)處理剩下的 10px。

  • onNestedScroll
    上一個方法結(jié)束后,NSC 處理剩下的距離。比如上面還剩 10px,這里 NSC 滾動 2px 后發(fā)現(xiàn)已經(jīng)到頭了,于是 NSC 結(jié)束其滾動,調(diào)用該方法,并將 NSC 處理剩下的像素?cái)?shù)作為參數(shù)(dxUnconsumed、dyUnconsumed)傳過來,這里傳過來的就是 8px。參數(shù)中還會有 NSC 處理過的像素?cái)?shù)(dxConsumed、dyConsumed)。這個方法主要處理一些越界后的滾動

  • onNestedPreFling
    用戶松開手指并且會發(fā)生慣性滾動之前調(diào)用。參數(shù)提供了速度信息,我們這里可以根據(jù)速度,決定最終的狀態(tài)是展開還是折疊,并且啟動滑動動畫。通過返回值我們可以通知 NSC 是否自己還要進(jìn)行滑動滾動,一般情況如果面板處于中間態(tài),我們就不讓 NSC 接著滾了,因?yàn)槲覀冞€要用動畫把面板完全展開或者完全折疊。

  • onStopNestedScroll

一切滾動停止后調(diào)用,如果不會發(fā)生慣性滾動,fling 相關(guān)方法不會調(diào)用,直接執(zhí)行到這里。這里我們做一些清理工作,當(dāng)然有時也要處理中間態(tài)問題。

自定義 Behavior 可以分2種實(shí)現(xiàn)思路:

  • 某個 view 監(jiān)聽另一個 view 的狀態(tài)變化,例如大小、位置、顯示狀態(tài)等
    這情況需要重寫 layoutDependsOn 和 onDependentViewChanged 方法
  • 某個 view 監(jiān)聽 CoordinatorLayout 內(nèi)的 NestedScrollingChild 的接口實(shí)現(xiàn)類的滑動狀態(tài)
    這情況需要重寫 onStartNestedScroll 和 onNestedPreScroll 系列方法,這就是NestedScrollingParent 的思路范圍了

結(jié)尾

說到這里基本 nestedScrolling 嵌套滾動原理和 自定義Behavior 的原理接都清楚了,剩下的我們?nèi)ザ喽鄬?shí)踐才能靈活的掌握。文章開頭的文章中,里面的例子有些難,不建議易上手就去看那個例子。

寫幾個涉及的單詞:

  • ScrollAxes 滾動方向
  • NestedFling 快速滾動,一般指手指已經(jīng)離開屏幕,但是屏幕還在快速滾動的狀態(tài)

參考文檔:

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時請結(jié)合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容