Android 进阶之旅 | NestedScrollView 嵌套RecycleView

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;
}

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)

参考资料:

https://www.cnblogs.com/fuyaozhishang/p/8232378.html

https://www.jianshu.com/p/23306c5886a7?utm_campaign

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 194,242评论 5 459
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 81,769评论 2 371
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 141,484评论 0 319
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 52,133评论 1 263
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 61,007评论 4 355
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 46,080评论 1 272
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 36,496评论 3 381
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 35,190评论 0 253
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 39,464评论 1 290
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 34,549评论 2 309
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 36,330评论 1 326
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 32,205评论 3 312
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 37,567评论 3 298
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 28,889评论 0 17
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 30,160评论 1 250
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 41,475评论 2 341
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 40,650评论 2 335

推荐阅读更多精彩内容