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.songma.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); } } });
https://blog.csdn.net/pengbo6665631/article/details/80400146
LovMin/Linkage