NestedScrollView與RecyclerView的嵌套使用

使用NestedScrollView優化嵌套RecyclerView

在開發中經常會遇到ScrollView嵌套RecyclerView的情況。
例如界面需要一個banner,一段介紹文字,還有個列表,banner要可以劃出界面,介紹文字要滑動後固定在頂部

開發中有兩種解決辦法 :
1 整個頁面使用RecyclerView,根據類型返回不同的ViewHolder,這也是我正常用的,這次學習下下面的方法
2 使用NestedScrollView 包裹RecyclerView.(這個可以直接使用,但是需要點小優化)

NestedScrollView

NestedScrollView 和scrollView一樣的使用,直接包裹一個子控件就可以了,它實現了 NestedScrollingParent, NestedScrollingChild2這兩個方法

實現NestedScrollingParent的意思就是 我是個嵌套滑動的父控件,我可以和子滑動控件一起處理滑動事件。NestedScrollView嵌套RecyclerView主要就是關注這個
實現NestedScrollingChild2的意思是 我是個嵌套滑動的子控件,我滑動的時候要告訴父嵌套滑動控件,滑動之前要問問他是否消耗滑動事件。消耗掉的話 我就不滑動了,這個是NestedScrollView作爲子控件的時候關注的

而RecyclerView則是實現了NestedScrollingChild2 他只能作爲滑動的嵌套子控件

在滑動前通知父控件,如果父控件消耗了滑動距離 則返回的consumed裏面的值就不爲0
abstract boolean    dispatchNestedPreScroll(int dx, int dy, int[] consumed, int[] offsetInWindow, int type)
Dispatch one step of a nested scroll in progress before this view consumes any portion of it.

滑動的時候告訴父控件,因爲NestedScrollView和RecylerView裏面已經處理好了,我們這次沒用到
abstract boolean    dispatchNestedScroll(int dxConsumed, int dyConsumed, int dxUnconsumed, int dyUnconsumed, int[] offsetInWindow, int type)
Dispatch one step of a nested scroll in progress.

是否有嵌套的滑動父控件
abstract boolean    hasNestedScrollingParent(int type)
Returns true if this view has a nested scrolling parent for the given input type.

告訴父控件開始滑動了,如果有父滑動控件,並且父滑動控件想要和子控件一起處理滑動的話,就會返回True
abstract boolean    startNestedScroll(int axes, int type)
Begin a nestable scroll operation along the given axes, for the given input type.

停止嵌套滑動了
abstract void   stopNestedScroll(int type)
Stop a nested scroll in progress for the given input type.

RecyclerView中有個成員變量

private NestedScrollingChildHelper mScrollingChildHelper;
private NestedScrollingChildHelper getScrollingChildHelper() {
        if (mScrollingChildHelper == null) {
            mScrollingChildHelper = new NestedScrollingChildHelper(this);
        }
        return mScrollingChildHelper;
    }

對應的NestedScrollView裏面

private final NestedScrollingParentHelper mParentHelper;
mParentHelper = new NestedScrollingParentHelper(this);

NestedScrollingChildHelper和NestedScrollingParentHelper都是系統提供的幫助類,已經封裝好滑動調用邏輯,我們的關注點其實是在接口的回調上面。例如NestedScrollView的接口NestedScrollingParent

abstract int    getNestedScrollAxes()
Return the current axes of nested scrolling for this NestedScrollingParent.

abstract boolean    onNestedFling(View target, float velocityX, float velocityY, boolean consumed)
Request a fling from a nested scroll.

abstract boolean    onNestedPreFling(View target, float velocityX, float velocityY)
React to a nested fling before the target view consumes it.

abstract void   onNestedPreScroll(View target, int dx, int dy, int[] consumed)
React to a nested scroll in progress before the target view consumes a portion of the scroll.

abstract void   onNestedScroll(View target, int dxConsumed, int dyConsumed, int dxUnconsumed, int dyUnconsumed)
React to a nested scroll in progress.

abstract void   onNestedScrollAccepted(View child, View target, int axes)
React to the successful claiming of a nested scroll operation.

abstract boolean    onStartNestedScroll(View child, View target, int axes)
React to a descendant view initiating a nestable scroll operation, claiming the nested scroll operation if appropriate.

abstract void   onStopNestedScroll(View target)
React to a nested scroll operation ending.

要實現效果的話:
1 當banner在頂部的時候 不管手指在哪滑動,都是NestedScrollView滑動
2 當banner已經劃過頂部的時候,手指在RecyclerView中滑動的時候,是RecyclerView滑動

我們demo中這個閥值就是banner的高度,上面說的是相應切換,其實並沒有,只是父控件有沒有消耗掉滑動距離的問題。子控件滑動前都會告訴父控件,父控件消耗掉了話,子控件就不做響應
在RecyclerView的OnTouchEvent中

 if (dispatchNestedPreScroll(dx, dy, mScrollConsumed, mScrollOffset, TYPE_TOUCH)) {
    dx -= mScrollConsumed[0];
    dy -= mScrollConsumed[1];
    vtev.offsetLocation(mScrollOffset[0], mScrollOffset[1]);
    // Updated the nested offsets
    mNestedOffsets[0] += mScrollOffset[0];
    mNestedOffsets[1] += mScrollOffset[1];
 }

dx和dy都要減去父控件消耗的距離,如果父控件把滑動距離全消耗掉了的話,那麼RecyclerView就不會滑動了
我繼承了NestedScrollView並重寫了OnNestedPreScroll,邏輯是如果NestedScrollView的滑動距離沒有超過閥值,NestedScrollView就消耗掉全部的距離,超過了就全交給子控件自己處理。
只要做這一件事就可以了 就是這麼簡單
public void onNestedPreScroll(View target, int dx, int dy, int[] consumed) {
super.onNestedPreScroll(target, dx, dy, consumed);

        if (mScrollY < mParentScrollHeight) {
            consumed[0] = dx;
            consumed[1] = dy;
            scrollBy(0, dy);
        }

        Log.d(TAG,"dx " + dx + " dy "+ dy +  " " + consumed[0]  + " " + consumed[1] + " scrollY " + mScrollY);
    }

還有個問題是NestedScrollView嵌套RecyclerView的話,滑動問題解決了,但是RecyclerView會繪製出所有的item,如果列表很大的話就完蛋了,所以我們需要固定RecyclerView的高度。
高度就是rootView的高度-欄目類型view的高度

 rootView.getViewTreeObserver().addOnGlobalLayoutListener(new ViewTreeObserver.OnGlobalLayoutListener() {
            @Override
            public void onGlobalLayout() {
              rootView.getViewTreeObserver().removeOnGlobalLayoutListener(this);
              int rvNewHeight = rootView.getHeight() - topView2.getHeight();
              rv.setLayoutParams(new LinearLayout.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT,rvNewHeight));
 }

另外還遇到個問題,NestedScrollView嵌套RecyclerView時,固定高度後打開界面時會自動滑到底部。只需要在NestedScrollView的子view中加入 android:descendantFocusability="blocksDescendants"

最後 demo地址

 

最後文末放上一個技術交流羣:Android IOC架構設計

羣內有許多技術大牛,有任何問題,歡迎廣大網友一起來交流,羣內還不定期免費分享高階Android學習視頻資料和麪試資料包~

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