Android 仿京东,淘宝RecyclerView嵌套ViewPager嵌套RecyclerView商品展示

最近看到京东,淘宝都有RecyclerView嵌套ViewPager嵌套RecyclerView商品展示的效果,效果挺好,废话不多说先看效果图:

GIF.gif

技能点:
1.Android事件分发机制等

需求点:
1.列表嵌套,内层的列表可以左右切换
2.ViewPager可以点击和滑动切换

最近在淘宝京东看到类似的效果,有时间就写了一下,效果实现了,但是感觉解决问题的思路和代码有很多瑕疵,写出来抛砖引玉,希望大佬们不吝赐教,写的不好不喜勿喷!

下面进入正题,先看下布局结构:

screen.png

就是标题所说的布局结构 RecyclerView+ViewPager+RecyclerView`

很多同学看到这里肯定想到要处理滑动冲突,没错,我们简单分析一下好撸代码(虽然是撸好的代码)

  1. 横向滑动
  • 横向滑动很简单,RecyclerView不需要处理,ViewPager处理

  1. 纵向滑动
  • 纵向滑动就稍微复杂点,本文的解决滑动冲突主要就就是解决外层RecyclerView以及内层RecyclerView的滑动冲突,仔细看下交互效果,不难发现我们需要用Tab是否吸顶作为判断的节点来将滑动事件交给外层或内层RecyclerView处理. 即: 1.Tab未吸顶时外层RecyclerView处理滑动事件,2.Tab吸顶时内层RecyclerView处理滑动事件. 这里解释一下,原来的方案是吸顶,后来我想了一下如果这个ViewPager下面没有跟多其它的样式的话,可以不用吸顶的(不能再有了,交互处理也太麻烦,有的话排版应该也不好看),\color{red}{所以下面的吸顶都是假的,只是看起来是吸顶效果}

大概就是这样,思路很清晰,这里先提几个接下来遇到的问题:

  • RecyclerView嵌套ViewPager时ViewPager的高度为0
  • 滑动冲突
  •  

     

    操作步骤:滑动到Tab吸顶->滑动内层RecyclerView至中间->切换一个Tab(内层RecyclerView的状态已经滑动到顶部,就是初始状态)->这时候将Tab滑动到非吸顶->切换到最初内层RecyClerView滑动到中间的Tab,这时候展示的就是Tab未吸顶,内层RecyclerView不在顶部的尴尬局面.说了这么多应该需要一张gif解释一下上图:

    GIF1.gif

对于上图所提到的情况,这个时候用户手指纵向滑动红色区域,滑动事件交给谁都不合适

.那先说下淘宝和京东采取的方式:

  1. 淘宝和京东部分页面切换ViewPager时候重新拉取数据(可能没有重新拉数据,只是notify了一下)将RecyclerView直接展示到初始状态
  2. 京东的部分界面(京东->我的->下拉->为你推荐)处理方式为:当Tab为非吸顶状态时候切换ViewPager,外层RecyclerView滑动到Tab吸顶
  3. demo因为用的是假数据,所以没做处理,但是代码中有在tab非吸顶状态时候,外层RecyclerView优先处理滑动事件的代码

个人感觉第一种处理方式比较好一点,demo的代码如下(需要请自行修改,PagerFragment.java)

                        if(! ((MainActivity)getActivity()).isStick){
                            ((MainActivity)getActivity()).adjustScroll(true);
                            return false;
                        }

下面说下实现方式,以及问题的解决(布局等细节就不贴出来了,详情见demo):

  1. 外部的RecyclerView为自定义的View继承自RecyclerView重写onInterceptTouchEvent方法
    处理滑动事件:

   private float downX ;    //按下时 的X坐标
    private float downY ;    //按下时 的Y坐标
    @Override
    public boolean onInterceptTouchEvent(MotionEvent e) {
        float x= e.getX();
        float y = e.getY();
        switch (e.getAction()){
            case MotionEvent.ACTION_DOWN:
                //将按下时的坐标存储
                downX = x;
                downY = y;
                break;
            case MotionEvent.ACTION_MOVE:
                //获取到距离差
                float dx= x-downX;
                float dy = y-downY;
                //通过距离差判断方向
                int orientation = getOrientation(dx, dy);
                switch (orientation) {
                        //左右滑动交给ViewPager处理
                    case 'r':
                        setNeedIntercept(false);
                        break;
                    //左右滑动交给ViewPager处理
                    case 'l':
                        setNeedIntercept(false);
                        break;
                }
                return isNeedIntercept;
        }
        return super.onInterceptTouchEvent(e);
    }

    public void setNeedIntercept(boolean needIntercept) {
        isNeedIntercept = needIntercept;
    }

    private int getOrientation(float dx, float dy) {
        if (Math.abs(dx)>Math.abs(dy)){
            //X轴移动
            return dx>0?'r':'l';//右,左
        }else{
            //Y轴移动
            return dy>0?'b':'t';//下//上
        }
    }

isNeedIntercept为是否拦截滑动事件,自己处理.并提供了一个setNeedIntercept方法供外部调用.代码可以看出,横向的滑动直接放行,让ViewPager处理,向上滑动时候如果tab吸顶了且已经滑动到底部,交给内部的RecyclerView处理,否则自己处理.

我们对内层的RecyclerView进行处理,重写其onTouchEvent方法

     @Override
    public boolean onTouchEvent(MotionEvent e) {
        float x= e.getX();
        float y = e.getY();
        switch (e.getAction()){
            case MotionEvent.ACTION_DOWN:
                //将按下时的坐标存储
                downX = x;
                downY = y;
                getParent().requestDisallowInterceptTouchEvent(true);
                break;
            case MotionEvent.ACTION_MOVE:
                        //获取到距离差
                        float dx= x-downX;
                        float dy = y-downY;
               
                        //通过距离差判断方向
                        int orientation = getOrientation(dx, dy);
                        int[] location={0,0};
                        getLocationOnScreen(location);
                        switch (orientation) {
                            case 'b':
                                //内层RecyclerView下拉到最顶部时候不再处理事件
                                if(!canScrollVertically(-1)){
                                    getParent().requestDisallowInterceptTouchEvent(false);
                                    if(needIntercepectListener!=null){
                                        needIntercepectListener.needIntercepect(false);
                                    }
                                }else{
                                    getParent().requestDisallowInterceptTouchEvent(true);
                                    if(needIntercepectListener!=null){
                                        needIntercepectListener.needIntercepect(true);
                                    }
                                }
                                break;
                            case 't':
                            
                                if(location[1]<=maxY){
                                    getParent().requestDisallowInterceptTouchEvent(true);
                                    if(needIntercepectListener!=null){
                                        needIntercepectListener.needIntercepect(true);
                                      
                                    }
                                }else{
                                    getParent().requestDisallowInterceptTouchEvent(false);
                                    if(needIntercepectListener!=null){
                                        needIntercepectListener.needIntercepect(false);
                                        return true;
                                    }
                                }
                                break;
                            case 'r':
                                getParent().requestDisallowInterceptTouchEvent(false);
                                break;
                            //左右滑动交给ViewPager处理
                            case 'l':
                                getParent().requestDisallowInterceptTouchEvent(false);
                                break;
                        }
                        break;
        }
        return super.onTouchEvent(e);
    }



    private int getOrientation(float dx, float dy) {
        if (Math.abs(dx)>Math.abs(dy)){
            //X轴移动
            return dx>0?'r':'l';//右,左
        }else{
            //Y轴移动
            return dy>0?'b':'t';//下//上
        }
    }

    public void setMaxY(int height) {
        this.maxY=height;
    }

    public interface NeedIntercepectListener{
        void needIntercepect(boolean needIntercepect);
    }
    public void setNeedIntercepectListener(NeedIntercepectListener needIntercepectListener) {
        this.needIntercepectListener = needIntercepectListener;
    }

其中的回调是为了告诉外层的RecyclerView需不需要拦截事件.

滑动冲突到这里基本上处理完了,下面说下吸顶的问题,其实只是思路的问题,这里采取的方式是将TabLayout和ViewPager当做一个外层RecyclerView的最后一个item,并且高度为屏幕高度-状态栏高度,这样当外层RecyclerView滑动到底部,Tab看上去是吸顶的.

简单说下:这个demo之前是按真正的吸顶做的,所以文章改动过,哪里说得不清楚的请直接看demo,主要是处理滑动事件冲突,难度不大,纯属抛砖引玉.

最后暴露一个问题,在外层RecyclerView滑动到底部时,需要将触摸事件交给内层的RecyclerView处理时,按照Demo里的处理方式,手指抬起之后重新滑动,内层RecyclerView才能拿到事件,原因是Demo判断外层RecyclerView是否滑动到底部的代码写在onInterceptTouchEvent里面,这个方法并不会实时调用,试过将判断写在onTouchEvent里面,实时判断再调用onInterceptTouchEvent,但是好像因为内层的RecyclerView并没有消费掉事件,所以这么做并没有效果,并没有实时的将触摸事件交给内层RecyclerView处理,这里尝试了很多方式,都不太理想,希望有思路的大佬给指点一下,效果如下:

GIF2.gif

项目地址:Github



作者:大名鼎鼎刘小厨
链接:https://www.jianshu.com/p/a5100ac471ae
來源:简书
简书著作权归作者所有,任何形式的转载都请联系作者获得授权并注明出处。

### 回答1: 在使用ViewPager2嵌套RecyclerView时,建议使用FragmentStateAdapter来设置ViewPager2的适配器,并在每个Fragment中使用RecyclerView。这样可以确保在滑动ViewPager2时,RecyclerView能够正确地重用并显示不同的数据。 同时,由于RecyclerView默认会拦截滑动事件,导致ViewPager2的滑动失效,因此需要在RecyclerView中禁用滑动事件的拦截。可以通过设置RecyclerView的NestedScrollingEnabled属性为false来实现,即recyclerView.setNestedScrollingEnabled(false)。 另外,还需要在RecyclerView的Adapter中实现getItemCount()和getItemViewType()方法,并根据需要显示不同的布局类型。在使用多个RecyclerView的情况下,建议使用不同的ViewType来避免布局重复和数据错乱等问题。 总之,正确地使用ViewPager2和RecyclerView嵌套需要考虑多方面的因素,包括适配器、布局、事件处理等等。需要仔细思考和实践,才能达到最佳效果。 ### 回答2: ViewPager2 是 Android 系统中的一个控件,可以用来创建包含多个页面的用户界面。而 RecyclerView 则是一个用于显示大量数据列表的控件。嵌套 ViewPager2 和 RecyclerView 可以带来更加丰富的用户界面和更好的交互体验。 在将 RecyclerView 嵌套ViewPager2 中时,需要注意以下几点: 1. 使用 FragmentStateAdapter 或 RecyclerView.Adapter ViewPager2 中的每一页都可以是一个 Fragment 或 View,我们可以使用 FragmentStateAdapter 或 RecyclerView.Adapter 作为 ViewPager2 的数据源。如果我们使用 RecyclerView.Adapter,可以创建多个 RecyclerView,每个 RecyclerView 显示不同的数据列表,而每个列表可以是独立的数据流。而使用 FragmentStateAdapter,我们可以创建不同的 Fragment,每个 Fragment 显示自己独立的数据流。 2.设置recyclerView为可滑动 当我们将 RecyclerView 嵌套ViewPager2 中时,需要为 RecyclerView 设置合适的滑动方式。默认情况下,RecyclerView 会拦截 ViewPager2 的滑动事件,导致 ViewPager2 的滑动失效。我们可以使用 setNestedScrollingEnabled 方法为 RecyclerView 开启嵌套滑动,或使用 ViewPager2.OnPageChangeCallback 监听 ViewPager2 的滑动事件,并通过调用 RecyclerView 的 scrollBy 和 scrollToPosition 方法使得 RecyclerView 能够正确滑动。 3.注意 RecyclerView 的布局 在将 RecyclerView 嵌套ViewPager2 中时,需要给 RecyclerView 设置适当的布局,以免出现滑动冲突、数据显示过大等问题。我们可以对 RecyclerView 进行水平或垂直的滚动,但需要注意 RecyclerView 的布局高度。 综上,ViewPager2 和 RecyclerView 的组合可以带来更加丰富和高效的用户界面和交互体验。它可以用于显示各种类型的列表数据,并通过 ViewPager2 的分页显示功能提供更好的用户体验。但在使用时如上文所述,需要注意一些细节问题。 ### 回答3: ViewPager2和RecyclerView都是Android中常用的控件之一。ViewPager2是一个可滑动的容器,常用于页面之间的切换和滑动;RecyclerView是一个高度可定制的列表工具,可用于呈现大量数据,并提供了很多的回收和性能优化功能。 在某些场景下,需要ViewPager2嵌套RecyclerView来实现滑动和展示数据的需求,这种需求可能出现在新闻客户端中,每个tab对应一种类型的新闻,每种类型的新闻数据量很大。这时候就可以考虑使用ViewPager2嵌套RecyclerView来优化用户体验和性能。 具体的实现方法如下: 1、创建一个Activity或Fragment来承载ViewPager2; 2、在ViewPager2中添加多个Fragment,每个Fragment都对应一个tab,包含一个RecyclerView; 3、在Fragment中创建一个合适的适配器类Adapter; 4、在Adapter中重写onCreateViewHolder、onBindViewHolder和getItemCount等方法,并将RecyclerView需要的数据进行绑定; 5、在使用RecyclerView时考虑合适的数据源和异步加载等优化。将RecyclerView中的数据源从Main Thread中移除,使用异步线程进行数据的加载和显示; 6、在ViewPager2中添加TabLayout用于切换不同的Fragment。 需要注意的是,ViewPager2嵌套RecyclerView能够实现数据的高效切换和渐变。为了更好地优化性能,应该尽量减少RecyclerView嵌套层数,并考虑分页加载等策略来优化加载速度和性能。 总之,ViewPager2嵌套RecyclerView是一种常用的Android开发技术,可以使用它来优化用户体验和性能,提高应用的质量。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值