美文网首页Android开发学习自定义控件
RecycleView左右联动(仿拼多多搜索界面商品联动)

RecycleView左右联动(仿拼多多搜索界面商品联动)

作者: lsys | 来源:发表于2018-07-04 17:56 被阅读149次

    最近在一个交流群里看到有个朋友问两个RecycleView左右联动效果怎么实现,看了他的效果图就感觉很熟悉,发现是自己去年9月份写过的一个界面,于是乎回顾了当初所写的代码,并写了一个Demo。

    拼多多效果图:

    拼多多效果图.gif

    看图分析需求:

    1. 左边item点击选中时,显示状态改变,右边将相应的分类内容滑动到最顶部显示。
    2. 右边滑动时,左边item跟随着滑动改变选中状态,具体为右边所看见第一个item内容列表的显示高度小于自身一半时,左边自动跳转到下一个item。

    实现思路:

    图片分析.png
    1. 如上图所示,整体界面可用三个RecyclerView实现,1,2关联滑动,3为嵌套内容显示。
    2. 左边item点击实现为在数据源中添加一个isSelected布尔值,在holder中通过判断isSelected是否为true来改变背景颜色和字体颜色,每次点击后改变isSelected值,然后调用notifyItemChanged(position)方法刷新界面。
    3. 接下来就考虑左边点击时右边跟随着滑动,RecyclerView中滑动到指定position的方法有scrollToPosition(position)和smoothScrollToPosition(position)两种,两者区别为前者类似于直接跳到position位置,后者有一个滑动过程,故此我们选择后者来实现滑动效果。
    4. 右边监听滑动,设置addOnScrollListener在onScrolled()方法中获取第一个可见item,判断item的显示高度来改变左边item所选中的position。

    所遇到的问题

    1. 在点击后,右边滑动时,会再次刷新左边选中状态,例:当前选中为第一项,当点击第五项时,第五项设为选中状态,右边会将第五个item滑动到顶部显示,不过在滑动的时候会触发onScrolled()方法,这使得左边依次的刷新1、2、3、4、5个item的选中状态。
      解决办法:设置一个点击状态变量clicked,当是点击时将clicked设为true,然后在onScrolled判断若是点击的条件下,不触发左边的关联滑动,最后在onScrollStateChanged(RecyclerView recyclerView, int newState)方法中,判断clicked是否为真且newState为停止状态时将点击状态设为flase。
      Tips:newState有三种状态:SCROLL_STATE_IDLE:停止状态;SCROLL_STATE_DRAGGING:手指拖动状态;SCROLL_STATE_SETTLING:手指拖动后刚抬起手指的状态;
    2. 调用smoothScrollToPosition(position)时,item有时会在顶部,有时会在底部,而要求是item每次都滑动到顶部。
      解决办法:当初处理这个问题时使用了其他方法,不过我实现后还是不行,最后在源码中找到了解决方案。首先,思考问题,为什么会有时出现到顶部,有时出现在底部呢,经过多次探索,发现smoothScrollToPosition(position)的方法的目的是将该position的item显示在界面上。具体的效果可理解为若该position的item的top和bottom都在界面上,那么将不会滑动,若只有top在界面上,那么会向上滑动,将bottom显示出来即可,若只有bottom在界面上,那么会向下滑动,将top也展示在界面上,若两者都不在界面上,会自动计算上下滑动的距离,选择较小的值滑动。那么我们可不可以只让它滑动到顶部呢,去源码中找找具体的实现吧。
      首先进入smoothScrollToPosition(position)方法中,发现主要是调用了mLayout.smoothScrollToPosition(this, mState, position)方法。
     public void smoothScrollToPosition(int position) {
            if (mLayoutFrozen) {
                return;
            }
            if (mLayout == null) {
                Log.e(TAG, "Cannot smooth scroll without a LayoutManager set. "
                        + "Call setLayoutManager with a non-null argument.");
                return;
            }
            mLayout.smoothScrollToPosition(this, mState, position);
        }
    

    此处的mLayout是我们为RecyclerView设置的LayoutManager,所以具体的就在LinearLayoutManager中查看,在该类中找到重写的smoothScrollToPosition方法。

    public void smoothScrollToPosition(RecyclerView recyclerView, RecyclerView.State state,
                int position) {
            LinearSmoothScroller linearSmoothScroller =
                    new LinearSmoothScroller(recyclerView.getContext());
            linearSmoothScroller.setTargetPosition(position);
            startSmoothScroll(linearSmoothScroller);
        }
    

    在该方法中new了一个LinearSmoothScroller对象,然后调用startSmoothScroll方法,startSmoothScroll后续中并没有发现怎么计算滑动的,所以具体实现就在LinearSmoothScroller类中。

    /**
         * Align child view's left or top with parent view's left or top
         *
         * @see #calculateDtToFit(int, int, int, int, int)
         * @see #calculateDxToMakeVisible(android.view.View, int)
         * @see #calculateDyToMakeVisible(android.view.View, int)
         */
        public static final int SNAP_TO_START = -1;
    
        /**
         * Align child view's right or bottom with parent view's right or bottom
         *
         * @see #calculateDtToFit(int, int, int, int, int)
         * @see #calculateDxToMakeVisible(android.view.View, int)
         * @see #calculateDyToMakeVisible(android.view.View, int)
         */
        public static final int SNAP_TO_END = 1;
    
        /**
         * <p>Decides if the child should be snapped from start or end, depending on where it
         * currently is in relation to its parent.</p>
         * <p>For instance, if the view is virtually on the left of RecyclerView, using
         * {@code SNAP_TO_ANY} is the same as using {@code SNAP_TO_START}</p>
         *
         * @see #calculateDtToFit(int, int, int, int, int)
         * @see #calculateDxToMakeVisible(android.view.View, int)
         * @see #calculateDyToMakeVisible(android.view.View, int)
         */
        public static final int SNAP_TO_ANY = 0;
    

    一进入LinearSmoothScroller就被这三个常量所吸引,用我蹩脚的英语翻译了下,发现有点意思,首先看SNAP_TO_START:Align child view's left or top with parent view's left or top(将子视图的左视图或上视图与父视图的左视图或上视图对齐),这不就是我们想要的顶部对齐吗,然后就选中Ctrl+F,找寻它的踪影,下一个、下一个、下一...等会儿,好像发现了什么!!!

    /**
         * When scrolling towards a child view, this method defines whether we should align the left
         * or the right edge of the child with the parent RecyclerView.
         *
         * @return SNAP_TO_START, SNAP_TO_END or SNAP_TO_ANY; depending on the current target vector
         * @see #SNAP_TO_START
         * @see #SNAP_TO_END
         * @see #SNAP_TO_ANY
         */
        protected int getHorizontalSnapPreference() {
            return mTargetVector == null || mTargetVector.x == 0 ? SNAP_TO_ANY :
                    mTargetVector.x > 0 ? SNAP_TO_END : SNAP_TO_START;
        }
    
        /**
         * When scrolling towards a child view, this method defines whether we should align the top
         * or the bottom edge of the child with the parent RecyclerView.
         *
         * @return SNAP_TO_START, SNAP_TO_END or SNAP_TO_ANY; depending on the current target vector
         * @see #SNAP_TO_START
         * @see #SNAP_TO_END
         * @see #SNAP_TO_ANY
         */
        protected int getVerticalSnapPreference() {
            return mTargetVector == null || mTargetVector.y == 0 ? SNAP_TO_ANY :
                    mTargetVector.y > 0 ? SNAP_TO_END : SNAP_TO_START;
        }
    

    再次用蹩脚的英语和对代码的理解,果断确定了,这就是我们的解决办法!!!重写getVerticalSnapPreference()方法,将返回值设为SNAP_TO_START即可。
    解决方法是有了,不过还是得看看到底是怎么判断的吧,mTargetVector为一个PointF类,主要是根据y坐标的大小来返回值,那我们就看看这个mTargetVector是怎么来的吧。同样Ctrl+F,查找mTargetVector的来源。

     protected void updateActionForInterimTarget(Action action) {
            PointF scrollVector = computeScrollVectorForPosition(getTargetPosition());
           ...
            mTargetVector = scrollVector;
           ...
          }
    

    scrollVector赋值给mTargetVector,然后进入 computeScrollVectorForPosition方法。

    public PointF computeScrollVectorForPosition(int targetPosition) {
            RecyclerView.LayoutManager layoutManager = getLayoutManager();
            if (layoutManager instanceof ScrollVectorProvider) {
                return ((ScrollVectorProvider) layoutManager)
                        .computeScrollVectorForPosition(targetPosition);
            }
            Log.w(TAG, "You should override computeScrollVectorForPosition when the LayoutManager"
                    + " does not implement " + ScrollVectorProvider.class.getCanonicalName());
            return null;
        }
    

    发现返回的是layoutManager.computeScrollVectorForPosition(targetPosition),那就只有去LinearLayoutManager中找答案了。

    @Override
        public PointF computeScrollVectorForPosition(int targetPosition) {
            //当RecyclerView的item为0时
            if (getChildCount() == 0) {
                return null;
            }
          //获取第一个可见item在Adapter数据源中的位置
            final int firstChildPos = getPosition(getChildAt(0));
            final int direction = targetPosition < firstChildPos != mShouldReverseLayout ? -1 : 1;
            if (mOrientation == HORIZONTAL) {
                return new PointF(direction, 0);
            } else {
                return new PointF(0, direction);
            }
        }
    

    解释一波:getChildAt(0)获取第一个可见item,getPosition(View view)通过view来获取该view在Adapter数据源中的position,为什么要这么做,那是因为RecyclerView复用的原因,mShouldReverseLayout为是否反转,最后判断targetPosition和firstChildPos大小设置direction,direction的值便是我们上面看到的y值。

    OK,思路和解决问题都已经说清楚了,那么就just show the code!!!

    左边Recycle人View的实现

    左边主要代码是在Adapter中写一个设置选中状态的函数,holder中设置选中状态的item背景,item的布局一个TextView就Ok了。

    public class LeftAdapter extends RecyclerArrayAdapter<LeftBean> {
        private int prePosition = 0;
        public LeftAdapter(Context context) {
            super(context);
        }
        public LeftAdapter(Context context, LeftBean[] objects) {
            super(context, objects);
        }
        public LeftAdapter(Context context, List<LeftBean> objects) {
            super(context, objects);
        }
        @Override
        public BaseViewHolder OnCreateViewHolder(ViewGroup parent, int viewType) {
            return new LeftHolder(parent);
        }
        public void setSelectedPosition(int position){
            if (prePosition != position){
                //Tips:处理右边滑动时左边对应的item不在屏幕上的情况
                mRecyclerView.smoothScrollToPosition(position);
                mObjects.get(prePosition).setSelected(false);
                notifyItemChanged(prePosition);
                prePosition = position;
                mObjects.get(prePosition).setSelected(true);
                notifyItemChanged(prePosition);
            }
        }
    }
    ===============Holder代码=================
    public class LeftHolder extends BaseViewHolder<LeftBean> {
        private TextView textView;
        public LeftHolder(ViewGroup itemView) {
            this(itemView, R.layout.item_left);
            textView = $(R.id.text_menu);
        }
        public LeftHolder(ViewGroup parent, int res) {
            super(parent, res);
        }
        @Override
        public void setData(LeftBean data) {
            super.setData(data);
            textView.setText(data.getName());
            //设置选中状态的背景
            if (data.isSelected()){
                textView.setBackgroundColor(Color.WHITE);
            }
            else {
                textView.setBackgroundColor(Color.parseColor("#747373"));
            }
        }
    }
    

    右边RecyclerView实现

    右边界面实现就是两个RecyclerView的嵌套,很简单的,就不粘贴代码了。不过需要注意的是嵌套的item布局高度写成wrap_content,如果是match_parent可能会造成内容显示不完整

    RecyclerView联动效果实现

    在MainActivity中,左边RecyclerView设置点击事件,右边RecyclerView添加滑动监听事件。

           recyclerLeft.setLayoutManager(new LinearLayoutManager(this));recyclerLeft.setAdapter(leftAdapter);
           leftAdapter.setOnItemClickListener(new RecyclerArrayAdapter.OnItemClickListener() {
               @Override
               public void onItemClick(int position) {
                   //关联右边滑动
                   recyclerRight.smoothScrollToPosition(position);
                   leftAdapter.setSelectedPosition(position);
                   clicked = true;
               }
           });
           ================右边滑动监听===================
           //设置能够平滑到顶部的LayouManager
           recyclerRight.setLayoutManager(new ScrollTopLayoutManager(this));
           recyclerRight.setAdapter(rightAdapter);
           recyclerRight.addOnScrollListener(new RecyclerView.OnScrollListener() {
               @Override
               public void onScrollStateChanged(RecyclerView recyclerView, int newState) {
                   if (clicked && newState == RecyclerView.SCROLL_STATE_IDLE) {
                       clicked = false;
                   }
               }
               @Override
               public void onScrolled(RecyclerView recyclerView, int dx, int dy) {
                   super.onScrolled(recyclerView, dx, dy);
                   if (!clicked) {
                       LinearLayoutManager linearLayoutManager = (LinearLayoutManager) recyclerView.getLayoutManager();
                       int currentItem = linearLayoutManager.findFirstVisibleItemPosition();
                       View v = linearLayoutManager.findViewByPosition(currentItem);
                       //右边滑动超过当前item一半时左边切换到下一个
                       if (v.getBottom() < v.getHeight() / 2 && currentItem >= 0) {
                           if (currentItem < linearLayoutManager.getItemCount() - 1){
                               leftAdapter.setSelectedPosition(currentItem + 1);
                           }
                       } else {
                           leftAdapter.setSelectedPosition(currentItem);
                       }
                   }
               }
           });
    

    ScrollTopLayoutManager的实现

    ScrollTopLayoutManager继承自LinearLayoutManager,重写了smoothScrollToPosition方法,并设置滑动到顶部的SmoothScroller。

    public class ScrollTopLayoutManager extends LinearLayoutManager {
    
       public ScrollTopLayoutManager(Context context) {
           super(context);
       }
    
       public ScrollTopLayoutManager(Context context, int orientation, boolean reverseLayout) {
           super(context, orientation, reverseLayout);
       }
    
       public ScrollTopLayoutManager(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) {
           super(context, attrs, defStyleAttr, defStyleRes);
       }
    
       @Override
       public void smoothScrollToPosition(RecyclerView recyclerView, RecyclerView.State state, int position) {
           TopLinearSmoothScroller topLinearSmoothScroller = new TopLinearSmoothScroller(recyclerView.getContext());
           topLinearSmoothScroller.setTargetPosition(position);
           startSmoothScroll(topLinearSmoothScroller);
       }
    
       private static class TopLinearSmoothScroller extends LinearSmoothScroller {
    
           public TopLinearSmoothScroller(Context context) {
               super(context);
           }
    
           @Override
           protected int getVerticalSnapPreference() {
               //滑动到顶部
               return SNAP_TO_START;
           }
       }
    }
    

    最后在加入模拟数据,渲染在界面上就实现该效果了。

    private void initData() {
            if (null == leftAdapter){
                ArrayList<LeftBean> leftBeans = new ArrayList<>();
                for (int i = 0; i < 10; i++){
                    leftBeans.add(new LeftBean("我是左边第"+ i + "个"));
                    //默认第一个为选中状态
                    if (i == 0){
                        leftBeans.get(0).setSelected(true);
                    }
                }
                leftAdapter = new LeftAdapter(this,leftBeans);
            }
            if (null == rightAdapter){
                ArrayList<RightBean> rightBeans = new ArrayList<>();
                for (int i = 0; i < 10; i++){
                    ArrayList<InnerBean> innerBeans = new ArrayList<>();
                    for (int j = 0; j < 5; j++){
                        innerBeans.add(new InnerBean("我是里边第"+ j + "个"));
                    }
                    rightBeans.add(new RightBean("我是右边第"+ i + "个",innerBeans));
                }
                rightAdapter = new RightAdapter(this,rightBeans);
            }
        }
    

    Demo效果图:

    Demo效果图.gif
    该Demo的数据较为简单,并没有添加实用数据,此次Demo中所用的Adapter为EasyRecyclerView中的,具体可看链接https://github.com/Jude95/EasyRecyclerView

    所有的代码已经上传到GitHub了,代码地址:https://github.com/ysls/Demo

    相关文章

      网友评论

      • ShawnChu:最多两个就可以了
        lsys:至少需要左右两个吧,右边item里面需不需要嵌套RecyclerView看自身实际需要

      本文标题:RecycleView左右联动(仿拼多多搜索界面商品联动)

      本文链接:https://www.haomeiwen.com/subject/ogasuftx.html