Android开发RecyclerView双列表联动

释放双眼,带上耳机,听听看~!

介绍

RecyclerView双列表联动效果在很多App中都有,一般用于分类页面,是一个比较常见的效果,好了,废话不多说,先上Demo最终效果图

 

需求分析

从图中可以清楚的看到
1、当点击左侧Item,显示选中状态,同时右侧对应该分类滑动到顶部显示;
2、当滑动右侧时,左侧跟随滑动并相应选中显示;
3、当点击左侧Item或右侧滑动停止时,左侧该Item会平滑地滚动到列表中间显示(可以滚动的话)。

实现

一、数据解析

对于这种联动效果,后端可能会返回这样的Json数据结构,但不管怎么返回,数据结构都是死的,靠的是我们怎么去重新组合数据,这里本地写死,放在assets目录下

[
......
{
"id": 8,
"title": "专场推荐",
"content": [
{
"id": 9,
"title": "1元抢好物",
"image": "https://graph.baidu.com/resource/116c0f069cc1772ffd2f901572577758.jpg"
},
{
"id": 10,
"title": "300享9折",
"image": "https://graph.baidu.com/resource/116c0f069cc1772ffd2f901572577758.jpg"
},
{
"id": 11,
"title": "抢1111神券",
"image": "https://graph.baidu.com/resource/116c0f069cc1772ffd2f901572577758.jpg"
}
]
}
......
]

为了方便管理,这里建立两个实体对象,左侧对象就很简单了,解析id、title字段即可,同时再增加一个布尔值字段isSelected用作判断当前选中的Item是哪个,方便改变选中状态;

 public class LeftVo {
private int id;
private String title;
private boolean isSelected;   //是否选中
public LeftVo(int id, String title) {
this.id = id;
this.title = title;
isSelected = false;
}
public LeftVo(int id, String title, boolean isSelected) {
this.id = id;
this.title = title;
this.isSelected = isSelected;
}
public int getId() {
return id;
}
public void setId(int id) {
this.id = id;
}
public String getTitle() {
return title;
}
public void setTitle(String title) {
this.title = title;
}
public boolean isSelected() {
return isSelected;
}
public void setSelected(boolean selected) {
isSelected = selected;
}
}

而对于右侧对象,除了解析id,title,image字段还要一个字段itemType来区分,哪个Item属于Title部分,哪个Item属于Content部分,都是为了写多布局结构,同时用一个字段fakePosition来记录当前右侧Item位置对应的左侧位置

 

   public class RightVo {
private int id;
private String title;
private String image;
private int itemType;       //多布局标识
private int fakePosition;   //记录当前右侧Item位置对应的左侧位置
public int getId() {
return id;
}
public void setId(int id) {
this.id = id;
}
public String getTitle() {
return title;
}
public void setTitle(String title) {
this.title = title;
}
public String getImage() {
return image;
}
public void setImage(String image) {
this.image = image;
}
public int getItemType() {
return itemType;
}
public void setItemType(int itemType) {
this.itemType = itemType;
}
public int getFakePosition() {
return fakePosition;
}
public void setFakePosition(int fakePosition) {
this.fakePosition = fakePosition;
}
}

开始解析左侧数据

 /**
* 获取左边数据
*/
public static List<LeftVo> getLeftData(Context context) throws JSONException {
String json = getAssets(context, "datas.json");
if (json != null) {
JSONArray ja = new JSONArray(json);
List<LeftVo> datas = new ArrayList<>();
for (int i = 0; i < ja.length(); i++) {
JSONObject jo = ja.getJSONObject(i);
int id = jo.getInt("id");
String title = jo.getString("title");
if (i == 0) {
datas.add(new LeftVo(id, title, true));
} else {
datas.add(new LeftVo(id, title));
}
}
return datas;
}
return null;
}

开始解析右侧数据

    /**
* 记录右侧Title真实索引位置
*/
private static SparseIntArray sTitlePosSa = new SparseIntArray();
/**
* 获取右测数据
*/
public static List<RightVo> getRightData(Context context) throws JSONException {
String json = getAssets(context, "datas.json");
if (json != null) {
JSONArray ja = new JSONArray(json);
List<RightVo> datas = new ArrayList<>();
for (int i = 0; i < ja.length(); i++) {
JSONObject jo = ja.getJSONObject(i);
int id = jo.getInt("id");
String title = jo.getString("title");
datas.add(createTitle(id, title, i));
JSONArray contentJa = jo.getJSONArray("content");
for (int j = 0; j < contentJa.length(); j++) {
JSONObject contentJo = contentJa.getJSONObject(j);
int contentId = contentJo.getInt("id");
String contentTitle = contentJo.getString("title");
String contentImage = contentJo.getString("image");
datas.add(createContent(contentId, contentTitle, contentImage, i));
}
}
saveRightTitleRealPosition(datas);
return datas;
}
return null;
}
/**
* 记录右侧Title真实位置
*/
private static void saveRightTitleRealPosition(List<RightVo> datas) {
if (datas != null) {
int key = 0;    //左侧索引
for (int i = 0; i < datas.size(); i++) {
if (datas.get(i).getItemType() == RightAdapter.TITLE) {
sTitlePosSa.put(key, i);
key++;
}
}
}
}
/**
* 创建一个右测标题数据
*/
private static RightVo createTitle(int id, String title, int fakePosition) {
RightVo rightVo = new RightVo();
rightVo.setId(id);
rightVo.setTitle("-- " + title + " --");
rightVo.setItemType(RightAdapter.TITLE);
rightVo.setFakePosition(fakePosition);
return rightVo;
}
/**
* 创建一个右测内容数据
*/
private static RightVo createContent(int id, String title, String image, int fakePosition) {
RightVo rightVo = new RightVo();
rightVo.setId(id);
rightVo.setTitle(title);
rightVo.setImage(image);
rightVo.setItemType(RightAdapter.CONTENT);
rightVo.setFakePosition(fakePosition);      //这里加上是为了扩大响应范围
return rightVo;
}
二、写适配器

1、左侧适配器,这里就很简单了,都是些常规用法

public class LeftAdapter extends RecyclerView.Adapter<LeftAdapter.ViewHolder> implements View.OnClickListener {
private Context mContext;
private LayoutInflater mInflater;
private List<LeftVo> mDatas;
public LeftAdapter(Context context) {
mContext = context;
mInflater = LayoutInflater.from(context);
}
/**
* 初始化添加数据
*/
public void addData(List<LeftVo> datas) {
mDatas = datas;
notifyDataSetChanged();
}
/**
* 全局刷新
*/
public void notifyGlobal(int position){
for (int i = 0; i < mDatas.size(); i++) {
if (i == position) {
mDatas.get(i).setSelected(true);
} else {
mDatas.get(i).setSelected(false);
}
}
notifyDataSetChanged();
}
@NonNull
@Override
public LeftAdapter.ViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) {
return new ViewHolder(mInflater.inflate(R.layout.item_left, parent, false));
}
@Override
public void onBindViewHolder(@NonNull LeftAdapter.ViewHolder holder, int position) {
LeftVo leftVo = mDatas.get(position);
if (leftVo.isSelected()) {
Drawable indicator = ContextCompat.getDrawable(mContext, R.drawable.shape_indicator);
if (indicator != null) {
indicator.setBounds(0, 0, indicator.getIntrinsicWidth(), indicator.getIntrinsicHeight());
holder.tvLeft.setCompoundDrawables(indicator, null, null, null);
}
holder.tvLeft.setBackgroundColor(Color.parseColor("#FFF9F9F9"));
} else {
holder.tvLeft.setCompoundDrawables(null, null, null, null);
holder.tvLeft.setBackgroundColor(Color.parseColor("#FFFFFFFF"));
}
holder.tvLeft.setText(leftVo.getTitle());
holder.tvLeft.setTag(position);
holder.tvLeft.setOnClickListener(this);
}
@Override
public int getItemCount() {
if (mDatas == null) {
mDatas = new ArrayList<>();
}
return mDatas.size();
}
@Override
public void onClick(View v) {
if (mOnItemClickListener != null) {
mOnItemClickListener.onItemClick(v, (Integer) v.getTag());
}
}
static class ViewHolder extends RecyclerView.ViewHolder {
TextView tvLeft;
ViewHolder(@NonNull View itemView) {
super(itemView);
tvLeft = itemView.findViewById(R.id.tv_left);
}
}
public OnItemClickListener mOnItemClickListener;
public void setOnItemClickListener(OnItemClickListener onItemClickListener) {
mOnItemClickListener = onItemClickListener;
}
public interface OnItemClickListener {
void onItemClick(View v, int position);
}
}

接下来,我们看下需求分析中的第三点,点击左侧Item或右侧滑动停止,左侧Item会平滑滚动到中间(可以滚动的话),由平滑滚动,我们想到是用smoothScrollToPosition(),但发现并不能达到理想中的效果(请看下图),不管怎么点击左侧,该Item都不能滚动,更别说滚动到中间了。哈哈,后来发现,其实它是可以滚动的,只是在可见范围内,它是不会滚动的,其原理请参考https://www.jianshu.com/p/a5cd3cff2f1b

这时候有小伙伴可能急了,既然这个方法不行,那换其他方法实现鸭,我觉得莫急,我们来简单看一下这个方法的源码

    @VisibleForTesting LayoutManager mLayout;
/**
* Starts a smooth scroll to an adapter position.
* <p>
* To support smooth scrolling, you must override
* {@link LayoutManager#smoothScrollToPosition(RecyclerView, State, int)} and create a
* {@link SmoothScroller}.
* <p>
* {@link LayoutManager} is responsible for creating the actual scroll action. If you want to
* provide a custom smooth scroll logic, override
* {@link LayoutManager#smoothScrollToPosition(RecyclerView, State, int)} in your
* LayoutManager.
*
* @param position The adapter position to scroll to
* @see LayoutManager#smoothScrollToPosition(RecyclerView, State, int)
*/
public void smoothScrollToPosition(int position) {
if (mLayoutSuppressed) {
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);
}

很明显,它调用了LayoutManager中的smoothScrollToPosition方法,而LayoutManager是RecyclerView中的抽象静态内部类,继续追踪

   /**
* <p>Smooth scroll to the specified adapter position.</p>
* <p>To support smooth scrolling, override this method, create your {@link SmoothScroller}
* instance and call {@link #startSmoothScroll(SmoothScroller)}.
* </p>
* @param recyclerView The RecyclerView to which this layout manager is attached
* @param state    Current State of RecyclerView
* @param position Scroll to this adapter position.
*/
public void smoothScrollToPosition(RecyclerView recyclerView, State state,
int position) {
Log.e(TAG, "You must override smoothScrollToPosition to support smooth scrolling");
}

该方法里有一句Log,说你必须要实现这个方法来支持平滑滚动,妈蛋,我哪会哟,好吧,其实我们设置适配器的时候都会有这么一句代码,是用来设置布局管理器,这里设置了线性布局管理器LinearLayoutManager

mRvLeft.setLayoutManager(new LinearLayoutManager(this));

点击进去看它实现的smoothScrollToPosition方法,发现有个LinearSmoothScroller,然后它的实例设置给了startSmoothScroll,很明显能猜到这个就是用来控制滚动的类

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

既然LinearLayoutManager的smoothScrollToPosition实现不了我们想要的效果,那我们就自己来重写一波。模仿LinearLayoutManager自定义一个LayoutManager,叫CenterLayoutManager

public class CenterLayoutManager extends LinearLayoutManager {
public CenterLayoutManager(Context context) {
super(context);
}
public CenterLayoutManager(Context context, int orientation, boolean reverseLayout) {
super(context, orientation, reverseLayout);
}
public CenterLayoutManager(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) {
super(context, attrs, defStyleAttr, defStyleRes);
}
@Override
public void smoothScrollToPosition(RecyclerView recyclerView, RecyclerView.State state, int position) {
CenterSmoothScroller centerSmoothScroller = new CenterSmoothScroller(recyclerView.getContext());
centerSmoothScroller.setTargetPosition(position);
startSmoothScroll(centerSmoothScroller);
}
}

模仿LinearSmoothScroller自定义一个CenterSmoothScroller,重写相关两个重要的方法

public class CenterSmoothScroller extends LinearSmoothScroller {
private static final float MILLISECONDS_PER_INCH = 80f;
public CenterSmoothScroller(Context context) {
super(context);
}
/**
* 计算滚动速度
*
* @param displayMetrics
* @return 滑过1px所需时间ms
*/
@Override
protected float calculateSpeedPerPixel(DisplayMetrics displayMetrics) {
return MILLISECONDS_PER_INCH / displayMetrics.densityDpi;
}
/**
* RecyclerView的中心点和item的中心点的相差
* @param viewStart
* @param viewEnd
* @param boxStart
* @param boxEnd
* @param snapPreference
* @return item需要移动的距离
*/
@Override
public int calculateDtToFit(int viewStart, int viewEnd, int boxStart, int boxEnd, int snapPreference) {
return (boxStart + (boxEnd - boxStart) / 2) - (viewStart + (viewEnd - viewStart) / 2);
}
}

写完了,给左侧适配器换上我们的布局管理器试试,咦,没错了,效果达到了

mRvLeft.setLayoutManager(new CenterLayoutManager(this));

2、右侧适配器,重写关键方法onAttachedToRecyclerView(),这里用了GridLayoutManager来实现右侧多布局。

public class RightAdapter extends RecyclerView.Adapter<RecyclerView.ViewHolder> {
/**
* Item类型
*/
public static final int TITLE = 1;
public static final int CONTENT = 2;
private Context mContext;
private LayoutInflater mInflater;
private List<RightVo> mDatas;
public RightAdapter(Context context) {
mContext = context;
mInflater = LayoutInflater.from(context);
}
public void addData(List<RightVo> datas) {
mDatas = datas;
notifyDataSetChanged();
}
public List<RightVo> getDatas() {
return mDatas;
}
@Override
public int getItemViewType(int position) {
RightVo rightVo = mDatas.get(position);
return rightVo.getItemType();
}
@NonNull
@Override
public RecyclerView.ViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) {
switch (viewType) {
case RightAdapter.TITLE:
return new TitleViewHolder(mInflater.inflate(R.layout.item_right_title, parent, false));
case RightAdapter.CONTENT:
return new ContentViewHolder(mInflater.inflate(R.layout.item_right_content, parent, false));
}
return null;
}
@Override
public void onBindViewHolder(@NonNull RecyclerView.ViewHolder holder, int position) {
RightVo rightVo = mDatas.get(position);
switch (getItemViewType(position)) {
case RightAdapter.TITLE:
TitleViewHolder titleViewHolder = (TitleViewHolder) holder;
titleViewHolder.tvRightTitle.setText(rightVo.getTitle());
break;
case RightAdapter.CONTENT:
ContentViewHolder contentViewHolder = (ContentViewHolder) holder;
contentViewHolder.tvRightContent.setText(rightVo.getTitle());
Glide.with(mContext)
.load(rightVo.getImage())
.into(contentViewHolder.ivRightContent);
break;
}
}
@Override
public int getItemCount() {
if (mDatas == null) {
mDatas = new ArrayList<>();
}
return mDatas.size();
}
@Override
public void onAttachedToRecyclerView(@NonNull RecyclerView recyclerView) {
super.onAttachedToRecyclerView(recyclerView);
RecyclerView.LayoutManager layoutManager = recyclerView.getLayoutManager();
if (layoutManager instanceof GridLayoutManager) {
GridLayoutManager gridLayoutManager = (GridLayoutManager) layoutManager;
gridLayoutManager.setSpanSizeLookup(new GridLayoutManager.SpanSizeLookup() {
@Override
public int getSpanSize(int position) {
switch (mDatas.get(position).getItemType()) {
case RightAdapter.TITLE:
return 3;                //Title占3份
case RightAdapter.CONTENT:
return 1;                //Content占1份
}
return 1;
}
});
}
}
static class TitleViewHolder extends RecyclerView.ViewHolder {
TextView tvRightTitle;
TitleViewHolder(@NonNull View itemView) {
super(itemView);
tvRightTitle = itemView.findViewById(R.id.tv_right_title);
}
}
static class ContentViewHolder extends RecyclerView.ViewHolder {
TextView tvRightContent;
ImageView ivRightContent;
ContentViewHolder(@NonNull View itemView) {
super(itemView);
tvRightContent = itemView.findViewById(R.id.tv_right_content);
ivRightContent = itemView.findViewById(R.id.iv_right_content);
}
}
}
三、联动实现

1、左侧联动右侧

mLeftAdapter.setOnItemClickListener(new LeftAdapter.OnItemClickListener() {
@Override
public void onItemClick(View v, int position) {
//左侧滑动到中间
mRvLeft.smoothScrollToPosition(position);
//左侧刷新状态
mLeftAdapter.notifyGlobal(position);
//右侧滑动到相应位置
GridLayoutManager layoutManager = (GridLayoutManager) mRvRight.getLayoutManager();
if (layoutManager != null) {
layoutManager.scrollToPositionWithOffset(DataUtil.getTitlePosSa().get(position), 0);
}
}
});

2、右侧联动左侧

mRvRight.addOnScrollListener(new RecyclerView.OnScrollListener() {
int firstVisibleItemPosition;
@Override
public void onScrollStateChanged(@NonNull RecyclerView recyclerView, int newState) {
super.onScrollStateChanged(recyclerView, newState);
if (newState == RecyclerView.SCROLL_STATE_IDLE) {
//左侧滑动到中间,等滑动停止再操作,防止卡顿
int position = mRightAdapter.getDatas().get(firstVisibleItemPosition).getFakePosition();
mRvLeft.smoothScrollToPosition(position);
}
}
@Override
public void onScrolled(@NonNull RecyclerView recyclerView, int dx, int dy) {
super.onScrolled(recyclerView, dx, dy);
GridLayoutManager layoutManager = (GridLayoutManager) mRvRight.getLayoutManager();
if (layoutManager != null) {
//左侧刷新状态
firstVisibleItemPosition = layoutManager.findFirstVisibleItemPosition();
int position = mRightAdapter.getDatas().get(firstVisibleItemPosition).getFakePosition();
mLeftAdapter.notifyGlobal(position);
}
}
});
参考

http://www.jishudog.com/15928/html

Demo地址

https://github.com/LovMin/Linkage

人已赞赏
Android文章

Android开发仿华为LoadingView

2020-2-1 16:38:58

Android文章

Android开发获取重力加速度和磁场强度的方法

2020-2-1 16:45:06

个人中心
购物车
优惠劵
今日签到
有新私信 私信列表
搜索