解决NestedScrollView+RecycleView+TwinklingRefreshLayout的四大痛点

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

  • 0
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

L-->R

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值