美文网首页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

相关文章

网友评论

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

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

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