最近在一个交流群里看到有个朋友问两个RecycleView左右联动效果怎么实现,看了他的效果图就感觉很熟悉,发现是自己去年9月份写过的一个界面,于是乎回顾了当初所写的代码,并写了一个Demo。
拼多多效果图:
拼多多效果图.gif看图分析需求:
- 左边item点击选中时,显示状态改变,右边将相应的分类内容滑动到最顶部显示。
- 右边滑动时,左边item跟随着滑动改变选中状态,具体为右边所看见第一个item内容列表的显示高度小于自身一半时,左边自动跳转到下一个item。
实现思路:
图片分析.png- 如上图所示,整体界面可用三个RecyclerView实现,1,2关联滑动,3为嵌套内容显示。
- 左边item点击实现为在数据源中添加一个isSelected布尔值,在holder中通过判断isSelected是否为true来改变背景颜色和字体颜色,每次点击后改变isSelected值,然后调用notifyItemChanged(position)方法刷新界面。
- 接下来就考虑左边点击时右边跟随着滑动,RecyclerView中滑动到指定position的方法有scrollToPosition(position)和smoothScrollToPosition(position)两种,两者区别为前者类似于直接跳到position位置,后者有一个滑动过程,故此我们选择后者来实现滑动效果。
- 右边监听滑动,设置addOnScrollListener在onScrolled()方法中获取第一个可见item,判断item的显示高度来改变左边item所选中的position。
所遇到的问题
- 在点击后,右边滑动时,会再次刷新左边选中状态,例:当前选中为第一项,当点击第五项时,第五项设为选中状态,右边会将第五个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:手指拖动后刚抬起手指的状态; - 调用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
网友评论