NestedScrollView嵌套RecyclerView


  天氣漸寒,然學習不可懈怠,記錄一下使用NestedScrollView嵌套RecyclerView的兩個問題,以后遇到可以來這里溫故.

  應該說在MD中,RecyclerView代替了ListView,而NestedScrollView代替了ScrollView,他們兩個都可以用來跟ToolBar交互,實現上拉下滑中ToolBar的變化。在NestedScrollView的名字中其實就可以看出他的作用了,Nested是嵌套的意思,而ToolBar基本需要嵌套使用.

  • 問題一,使用NestedScrollView嵌套RecyclerView時,滑動lRecyclerView列表會出現強烈的卡頓感.

體驗極其不流暢,這不是我們希望的.於是,百度了一下輕松找到解決辦法.

mRecyclerView.setNestedScrollingEnabled(false);

加上這句之后,整個世界都平靜了,非常流暢啊!

這里面做了啥?

public class RecyclerView extends ViewGroup implements ScrollingView, NestedScrollingChild {
...
}

點擊RecyclerView源碼,發現其實現了ScrollingView和NestedScrollingChild兩個接口.

ScrollingView點進源碼查看.

public interface ScrollingView {
    /**
     * <p>Compute the horizontal range that the horizontal scrollbar
     * represents.</p>
     *
     * @return the total horizontal range represented by the horizontal
     *         scrollbar
     *
     * @see #computeHorizontalScrollExtent()
     * @see #computeHorizontalScrollOffset()
     * @see android.widget.ScrollBarDrawable
     */
    int computeHorizontalScrollRange();

    /**
     * <p>Compute the horizontal offset of the horizontal scrollbar's thumb
     * within the horizontal range. This value is used to compute the position
     * of the thumb within the scrollbar's track.</p>
     * @return the horizontal offset of the scrollbar's thumb
     *
     * @see #computeHorizontalScrollRange()
     * @see #computeHorizontalScrollExtent()
     * @see android.widget.ScrollBarDrawable
     */
    int computeHorizontalScrollOffset();

    /**
     * <p>Compute the horizontal extent of the horizontal scrollbar's thumb
     * within the horizontal range. This value is used to compute the length
     * of the thumb within the scrollbar's track.</p>
     *
     * @return the horizontal extent of the scrollbar's thumb
     *
     * @see #computeHorizontalScrollRange()
     * @see #computeHorizontalScrollOffset()
     * @see android.widget.ScrollBarDrawable
     */
    int computeHorizontalScrollExtent();

    /**
     * <p>Compute the vertical range that the vertical scrollbar represents.</p>
     *
     *
     * @return the total vertical range represented by the vertical scrollbar
     *
     * <p>The default range is the drawing height of this view.</p>
     *
     * @see #computeVerticalScrollExtent()
     * @see #computeVerticalScrollOffset()
     * @see android.widget.ScrollBarDrawable
     */
    int computeVerticalScrollRange();

    /**
     * <p>Compute the vertical offset of the vertical scrollbar's thumb
     * within the horizontal range. This value is used to compute the position
     * of the thumb within the scrollbar's track.</p>
     *
     * @return the vertical offset of the scrollbar's thumb
     *
     * @see #computeVerticalScrollRange()
     * @see #computeVerticalScrollExtent()
     * @see android.widget.ScrollBarDrawable
     */
    int computeVerticalScrollOffset();

    /**
     * <p>Compute the vertical extent of the vertical scrollbar's thumb
     * within the vertical range. This value is used to compute the length
     * of the thumb within the scrollbar's track.</p>
     *
     *
     * @return the vertical extent of the scrollbar's thumb
     *
     * @see #computeVerticalScrollRange()
     * @see #computeVerticalScrollOffset()
     * @see android.widget.ScrollBarDrawable
     */
    int computeVerticalScrollExtent();
}

可以看出該接口主要關聯橫向和縱向滑動距離的計算.那NestedScrollingChild呢?這是個什么?

點進其源碼,經過一頓翻譯和分析,發現其定義的方法並不多:

public interface NestedScrollingChild {  
    /** 
     * 設置嵌套滑動是否能用
     * 
     *  @param enabled true to enable nested scrolling, false to disable
     */  
    public void setNestedScrollingEnabled(boolean enabled);  
  
    /** 
     * 判斷嵌套滑動是否可用 
     * 
     * @return true if nested scrolling is enabled
     */  
    public boolean isNestedScrollingEnabled();  
  
    /** 
     * 開始嵌套滑動
     * 
     * @param axes 表示方向軸,有橫向和豎向
     */  
    public boolean startNestedScroll(int axes);  
  
    /** 
     * 停止嵌套滑動 
     */  
    public void stopNestedScroll();  
  
    /** 
     * 判斷是否有父View 支持嵌套滑動 
     * @return whether this view has a nested scrolling parent
     */  
    public boolean hasNestedScrollingParent();  
  
    /** 
     * 在子View的onInterceptTouchEvent或者onTouch中,調用該方法通知父View滑動的距離
     *
     * @param dx  x軸上滑動的距離
     * @param dy  y軸上滑動的距離
     * @param consumed 父view消費掉的scroll長度
     * @param offsetInWindow   子View的窗體偏移量
     * @return 支持的嵌套的父View 是否處理了 滑動事件 
     */  
    public boolean dispatchNestedPreScroll(int dx, int dy, int[] consumed, int[] offsetInWindow);  

    /** 
     * 子view處理scroll后調用
     *
     * @param dxConsumed x軸上被消費的距離(橫向) 
     * @param dyConsumed y軸上被消費的距離(豎向)
     * @param dxUnconsumed x軸上未被消費的距離 
     * @param dyUnconsumed y軸上未被消費的距離 
     * @param offsetInWindow 子View的窗體偏移量
     * @return  true if the event was dispatched, false if it could not be dispatched.
     */  
    public boolean dispatchNestedScroll(int dxConsumed, int dyConsumed,  
          int dxUnconsumed, int dyUnconsumed, int[] offsetInWindow);  
  

  
    /** 
     * 滑行時調用 
     *
     * @param velocityX x 軸上的滑動速率
     * @param velocityY y 軸上的滑動速率
     * @param consumed 是否被消費 
     * @return  true if the nested scrolling parent consumed or otherwise reacted to the fling
     */  
    public boolean dispatchNestedFling(float velocityX, float velocityY, boolean consumed);  
  
    /** 
     * 進行滑行前調用
     *
     * @param velocityX x 軸上的滑動速率
     * @param velocityY y 軸上的滑動速率 
     * @return true if a nested scrolling parent consumed the fling
     */  
    public boolean dispatchNestedPreFling(float velocityX, float velocityY);  
}

再回頭看下RecyclerView中

public class RecyclerView extends ViewGroup implements ScrollingView, NestedScrollingChild {

public RecyclerView(Context context, @Nullable AttributeSet attrs, int defStyle) {
    super(context, attrs, defStyle);
    if (attrs != null) {
        TypedArray a = context.obtainStyledAttributes(attrs, CLIP_TO_PADDING_ATTR, defStyle, 0);
        mClipToPadding = a.getBoolean(0, true);
        a.recycle();
    } else {
        mClipToPadding = true;
    }
    setScrollContainer(true);
  //默認獲取焦點 setFocusableInTouchMode(
true); final ViewConfiguration vc = ViewConfiguration.get(context); mTouchSlop = vc.getScaledTouchSlop(); mMinFlingVelocity = vc.getScaledMinimumFlingVelocity(); mMaxFlingVelocity = vc.getScaledMaximumFlingVelocity(); setWillNotDraw(getOverScrollMode() == View.OVER_SCROLL_NEVER); mItemAnimator.setListener(mItemAnimatorListener); initAdapterManager(); initChildrenHelper(); // If not explicitly specified this view is important for accessibility. if (ViewCompat.getImportantForAccessibility(this) == ViewCompat.IMPORTANT_FOR_ACCESSIBILITY_AUTO) { ViewCompat.setImportantForAccessibility(this, ViewCompat.IMPORTANT_FOR_ACCESSIBILITY_YES); } mAccessibilityManager = (AccessibilityManager) getContext() .getSystemService(Context.ACCESSIBILITY_SERVICE); setAccessibilityDelegateCompat(new RecyclerViewAccessibilityDelegate(this)); // Create the layoutManager if specified. boolean nestedScrollingEnabled = true; if (attrs != null) { int defStyleRes = 0; TypedArray a = context.obtainStyledAttributes(attrs, R.styleable.RecyclerView, defStyle, defStyleRes); String layoutManagerName = a.getString(R.styleable.RecyclerView_layoutManager); int descendantFocusability = a.getInt( R.styleable.RecyclerView_android_descendantFocusability, -1); if (descendantFocusability == -1) { setDescendantFocusability(ViewGroup.FOCUS_AFTER_DESCENDANTS); } a.recycle(); createLayoutManager(context, layoutManagerName, attrs, defStyle, defStyleRes); if (Build.VERSION.SDK_INT >= 21) { a = context.obtainStyledAttributes(attrs, NESTED_SCROLLING_ATTRS, defStyle, defStyleRes); nestedScrollingEnabled = a.getBoolean(0, true); a.recycle(); } } else { setDescendantFocusability(ViewGroup.FOCUS_AFTER_DESCENDANTS); }   //默認支持嵌套滾動 // Re-set whether nested scrolling is enabled so that it is set on all API levels setNestedScrollingEnabled(nestedScrollingEnabled); }   @Override public void setNestedScrollingEnabled(boolean enabled) { getScrollingChildHelper().setNestedScrollingEnabled(enabled); } @Override public boolean isNestedScrollingEnabled() { return getScrollingChildHelper().isNestedScrollingEnabled(); } @Override public boolean startNestedScroll(int axes) { return getScrollingChildHelper().startNestedScroll(axes); } @Override public void stopNestedScroll() { getScrollingChildHelper().stopNestedScroll(); } @Override public boolean hasNestedScrollingParent() { return getScrollingChildHelper().hasNestedScrollingParent(); } @Override public boolean dispatchNestedScroll(int dxConsumed, int dyConsumed, int dxUnconsumed, int dyUnconsumed, int[] offsetInWindow) { return getScrollingChildHelper().dispatchNestedScroll(dxConsumed, dyConsumed, dxUnconsumed, dyUnconsumed, offsetInWindow); } @Override public boolean dispatchNestedPreScroll(int dx, int dy, int[] consumed, int[] offsetInWindow) { return getScrollingChildHelper().dispatchNestedPreScroll(dx, dy, consumed, offsetInWindow); } @Override public boolean dispatchNestedFling(float velocityX, float velocityY, boolean consumed) { return getScrollingChildHelper().dispatchNestedFling(velocityX, velocityY, consumed); } @Override public boolean dispatchNestedPreFling(float velocityX, float velocityY) { return getScrollingChildHelper().dispatchNestedPreFling(velocityX, velocityY); }

這是全部都交給getScrollingChildHelper()這個方法的返回對象處理了啊,看看這個方法是怎么實現的。

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

這樣的話.NestedScrollingChild 接口的方法都交給NestedScrollingChildHelper這個代理對象處理了.

那么到這個類的源碼中定位到我們關注方法的實現;

public class NestedScrollingChildHelper {
   private final View mView;
private ViewParent mNestedScrollingParent;
private boolean mIsNestedScrollingEnabled;
private int[] mTempNestedScrollConsumed;

/**
* Construct a new helper for a given view.
*/
public NestedScrollingChildHelper(View view) {
mView = view;
}

/**
* Enable nested scrolling.
*
* <p>This is a delegate method. Call it from your {@link android.view.View View} subclass
* method/{@link android.support.v4.view.NestedScrollingChild} interface method with the same
* signature to implement the standard policy.</p>
*
* @param enabled true to enable nested scrolling dispatch from this view, false otherwise
*/
public void setNestedScrollingEnabled(boolean enabled) {
if (mIsNestedScrollingEnabled) {
ViewCompat.stopNestedScroll(mView);
}
mIsNestedScrollingEnabled = enabled;
}
  /**
  * Check if nested scrolling is enabled for this view.
  *
  * <p>This is a delegate method. Call it from your {@link android.view.View View} subclass
  * method/{@link android.support.v4.view.NestedScrollingChild} interface method with the same
  * signature to implement the standard policy.</p>
  *
  * @return true if nested scrolling is enabled for this view
  */
  public boolean isNestedScrollingEnabled() {
return mIsNestedScrollingEnabled;
  }
.
.
.
}

可以看出RecyclerView默認是setNestedScrollingEnabled(true),是支持嵌套滾動的,也就是說當它嵌套在NestedScrollView中時,默認會隨着NestedScrollView滾動而滾動,放棄了自己的滾動.

所以給我們的感覺就是滯留、卡頓.主動將該值置false可以有效解決該問題.

  • 問題二,使用NestedScrollView嵌套RecyclerView時,每次打開界面都是定位在RecyclerView在屏幕頂端,列表上面的布局都被頂上去了.

這個問題花了我不少時間去查原因,最終定位到是RecyclerView搶占了焦點,自動滾動導致的.

查看RecyclerView的源碼發現,它會在構造方法中調用setFocusableInTouchMode(true),所以搶到焦點后一定會定位到第一行的位置突出RecyclerView的顯示

解決方法就是NestScrollView節點添加

android:focusableInTouchMode="true"

然后在NestScrollView的子節點view添加:

android:descendantFocusability="blocksDescendants"

或者 直接mRecyclerVIew.setFocusableInTouchMode(false)

  • 總結

不明白的地方還是要多看源碼 , 看源碼有幾個好處.第一點,有些問題只能從源碼里找出究竟 , 一點一點剖析才能看出問題的根本, 弄透一個接口的作用 , 那么有類似需求的時候,可以自定義控件實現該接口;再一個,就是在看的過程中, 我們會熟悉源碼的風格,命令的方式和邏輯的先后 .總之 , 看一個好輪子的源碼 , 是為了寫輪子做准備.

 


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM