目录
1.其他View+RecycleView 的滑动问题
先来看看布局,一个线性布局,三个View,但是在滑动RecycleView的时候,上面两个View 并不会自动往上滑。
这个问题很简单,只需要在所有控件外部再嵌套一层 NestedScrollView 即可(因为NestedScrollView只能拥有一个孩子,所以需要再包一层LinearLayout)。布局嵌套如下:
嵌套完之后,所有控件就能一起滑动了,但是也蹦出了一个新问题。
2.NestedScrollView+RecycleView 嵌套后ViewHolder重复创建的优化
我们来看看嵌套之后 RecycleView 对于 ViewHolder 的创建。
它把我们第一次加载的数据全部加载出来了,VIewHolder占用内存在加载控件之后会越来越大,那么我们怎么解决呢?
关于ViewHolder,可以看看这篇文章https://blog.csdn.net/xyq046463/article/details/51800095
我们先看看现在加载完一百多个数据后,控件的高度。由于高度属性设置为 march_parent
实际高度竟然过万了。
D/HomePagerFragment: recyclerView height=====>13780
我们再试试设定高度为确定的200dp
<androidx.recyclerview.widget.RecyclerView
android:id="@+id/rv_home_pager_content"
android:layout_width="match_parent"
android:layout_height="200dp"
android:overScrollMode="never" />
再打log看看数据加载了几条
D/HomeContentPagerAdapter: onCreateViewHolder 3
D/HomeContentPagerAdapter: bindView 0
D/HomeContentPagerAdapter: onCreateViewHolder 4
D/HomeContentPagerAdapter: bindView 1
D/HomeContentPagerAdapter: onCreateViewHolder 5
D/HomeContentPagerAdapter: bindView 2
只有5条数据,可以达到缓存优化的目的。那我们直接动态计算出控件大小,动态设置回去是不是就行了呢?
如图,两个 VIew 的高度 m 、手机的高度 x 我们是可以直接获取的,然后动态设置 Layout size 和 RecycleView size。我们滑动的时候,红色区域整体往上滑动,我们就能正常滑动RecyView了。如下图:
现在来动态设置 RecycleView 的高度
//parentLayout就是整个Layout 的最外层Layout
parentLayout.getViewTreeObserver().addOnGlobalLayoutListener(new ViewTreeObserver.OnGlobalLayoutListener() {
@Override
public void onGlobalLayout() {
LogUtils.d(HomePagerFragment.this,"recyclerView height=====>"+recyclerView.getMeasuredHeight());
int measureHeight = parentLayout.getHeight();
LinearLayout.LayoutParams layoutParams = (LinearLayout.LayoutParams) recyclerView.getLayoutParams();
layoutParams.height = measureHeight;
//相当于设置为手机屏幕的高度
recyclerView.setLayoutParams(layoutParams);
if (measureHeight != 0) {
//释放资源 parentLayout.getViewTreeObserver().removeOnGlobalLayoutListener(this);
}
}
});
但是设置完后的效果如下:
滑动上部分时可以滑动,滑动 RecycleView 时不能正确滑动。这里就涉及到事件的消费了。
3.NestedScrollView+RecycleView 嵌套后滑动事件消费问题
NestedScrollView 、RecycleView 都实现了NestEdScrollingChild3 接口。RecycleView中,通过Helper 来实现二者之间的通信。
RecycleView 调用NestScrollView 的滑动时,我们只需要在NestScrollView中复写相应方法,在 RecycleView 滑动时,让 NestScrollView 消费掉 RecycleView 滑动的方法,然后在 NestScrollView 中调用自身的滑动即可。
public class TbScrollView extends NestedScrollView {
//...复写构造方法
//主要用到的是两个滑动控件的方法
@Override
public void onNestedPreScroll(@NonNull View target, int dx, int dy, @NonNull int[] consumed, int type) {
//这里是NestedScrollView的滑动
super.onNestedPreScroll(target, dx, dy, consumed, type);
}
@Override
protected void onScrollChanged(int l, int t, int oldl, int oldt) {
//这里是RecyclerView的滑动
super.onScrollChanged(l, t, oldl, oldt);
}
首先,如果 RecycleView 滑动了,先保存一下滑动的高度(这个高度用来判断NestScrollVIew是否需要滑动),让 RecycleView 自身滑动。
@Override
protected void onScrollChanged(int l, int t, int oldl, int oldt) {
this.originHeight = t;
super.onScrollChanged(l, t, oldl, oldt);
LogUtils.d(this, "onScrollChanged--t----" + t);
}
随后在 NestScrollView 预滑动时,判断originHeight
是否小于我们整个屏幕的高度,小于的话说明RecycleView还没占满整个屏幕,需要上移 NestScrollView ,大于的话说明占满了整个屏幕,我们就不让 NestScrollView 滑动。
@Override
public void onNestedPreScroll(@NonNull View target, int dx, int dy, @NonNull int[] consumed, int type) {
LogUtils.d(this, "onNestedPreScroll---dy---->" + dy);
if (originHeight < mHeadHeight) {
//滑动完之后消费掉
scrollBy(dx,dy);
consumed[0]=dx;
consumed[1]=dy;
}
//消费完了就不会再滑动NestScrollView
super.onNestedPreScroll(target, dx, dy, consumed, type);
}
mHeadHeight
可以由外部传入,内部提供一个setHeight(int height)
方法即可。
//parentLayout就是该Layout的最大父控件
parentLayout.getViewTreeObserver().addOnGlobalLayoutListener(new ViewTreeObserver.OnGlobalLayoutListener() {
@Override
public void onGlobalLayout() {
LogUtils.d(HomePagerFragment.this,"recyclerView height=====>"+recyclerView.getMeasuredHeight());
//这里直接设置最外层控件的高度,即屏幕高度
tbScrollView.setHeadHeight(looperHeaderLayout.getHeight());
int measureHeight = parentLayout.getHeight();
LinearLayout.LayoutParams layoutParams = (LinearLayout.LayoutParams) recyclerView.getLayoutParams();
layoutParams.height = measureHeight;
recyclerView.setLayoutParams(layoutParams);
if (measureHeight != 0) {parentLayout.getViewTreeObserver().removeOnGlobalLayoutListener(this);
}
}
});
至此,NestedScrollView+RecycleView 的适配完成。
4.TwinklingRefreshLayout+NestedScrollView+RecycleView 的滑动问题
TwinklingRefreshLayout
嵌套两者的时候会出现如下情况:
这里使用的是TwinklingRefreshLayout
,当然还有其他一些有优秀的刷新控件。可以试试Github 高star的一个控件 SmartRefreshLayout
功能更多,并且适配了很多常见的View。
这里仅为提供的一个NestScrollView+RecycleView 适配TwinklingRefreshLayout
的方法。
寻找原因
查阅源码发现,TwinklingRefreshLayout
采用的是事件分发和拦截的机制。先找一下ChildView,这个view就是我们所放的 NestScrollView 了
@Override
protected void onFinishInflate() {
super.onFinishInflate();
//获得子控件
//onAttachedToWindow方法中mChildView始终是第0个child,把header、footer放到构造函数中,mChildView最后被inflate
mChildView = getChildAt(3);
cp.init();
decorator = new OverScrollDecorator(cp, new RefreshProcessor(cp));
initGestureDetector();
}
对于事件处理,肯定会判断一下子 View 的滑动位置,这里 childView 只有一些调用,所以找到 get
方法。
然后找到了判断子View是否到达底部的方法
在这我们发现,该刷新控件能够适配的控件ListView RecycleView WebView 等
。由于 NestScrollView 继承了 ViewGroup 所以会走isViewGroupToBottom
.
public static boolean isViewToBottom(View view, int mTouchSlop) {
if (view instanceof AbsListView) return isAbsListViewToBottom((AbsListView) view);
if (view instanceof RecyclerView) return isRecyclerViewToBottom((RecyclerView) view);
if (view instanceof WebView) return isWebViewToBottom((WebView) view, mTouchSlop);
if (view instanceof ViewGroup) return isViewGroupToBottom((ViewGroup) view);
return false;
}
看看如何判断的
public static boolean isViewGroupToBottom(ViewGroup viewGroup) {
//这里的ViewGroup就是targetView ,也就是NestScrollView
View subChildView = viewGroup.getChildAt(0);
//由于我们固定了RecycleView 的大小,因此一定会小于viewGroup.getHeight()
//所以恒为true
//通知出去之后,展示出来的就是一直认为到了底部,需要刷新
return (subChildView != null && subChildView.getMeasuredHeight() <= viewGroup.getScrollY() + viewGroup.getHeight());
}
解决办法
因此,我们只要通过 NestScrollView 拿到 RecycleView 来判断是否到达底部,最后通知回来即可
在源码的ScrollUtils中,添加一个判断
public static boolean isViewToBottom(View view, int mTouchSlop) {
if (view instanceof AbsListView) return isAbsListViewToBottom((AbsListView) view);
if (view instanceof RecyclerView) return isRecyclerViewToBottom((RecyclerView) view);
if (view instanceof WebView) return isWebViewToBottom((WebView) view, mTouchSlop);
//判断是否是NestedScrollView
if(view instanceof NestedScrollView) return isNestedScrollView((TbScrollView) view) ;
if (view instanceof ViewGroup) return isViewGroupToBottom((ViewGroup) view);
return false;
}
private static boolean isNestedScrollView(TbScrollView view) {
Log.d(TAG, "isNestedScrollView: ----->0");
return view.isBottom();
}
再在自定义的 TbScrollView 中定义isBottom()
方法:
@Override
public void onNestedPreScroll(@NonNull View target, int dx, int dy, @NonNull int[] consumed, int type) {
//这里回调的时候拿到RecycleView
if (target instanceof RecyclerView) {
mChildRecycleView = target;
}
if (originHeight < mHeadHeight) {
scrollBy(dx,dy);
consumed[0]=dx;
consumed[1]=dy;
}
super.onNestedPreScroll(target, dx, dy, consumed, type);
}
public void setHeadHeight(int headHeight){
this.mHeadHeight = headHeight;
}
@Override
protected void onScrollChanged(int l, int t, int oldl, int oldt) {
this.originHeight = t;
super.onScrollChanged(l, t, oldl, oldt);
}
public boolean isBottom() {
if (mChildRecycleView != null) {
//正数是判断是否可以上拉
//取非后返回是否到达底部
return !mChildRecycleView.canScrollVertically(1);
}
return false;
}
至此,完成了完美的适配~
来看看最终效果吧:
小结
这个控件已经好久没有更新了,用到的话这个方案还是可以解决NestedScrollView+RecycleView+TwinklingRefreshLayou
三者嵌套的问题。如果想用到适配更好、视觉效果更好的控件,可以用一下SmartRefreshLayout
。
https://github.com/scwang90/SmartRefreshLayout