RecyclerView双列表联动
来源:     阅读:477
云上智慧
发布于 2020-04-24 14:54
查看主页

详情

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

Demo地址

LovMin/Linkage

免责声明:本文为用户发表,不代表网站立场,仅供参考,不构成引导等用途。 系统环境 服务器应用
相关推荐
一文带你理解Web前台发展历程
Redis如何分析慢查询操作?
小白指南:eclipse + maven + spring-boot + hello world
遇见Python(二):面向对象
Linux中Apache(httpd)配置反向代理商二级域名
首页
搜索
订单
购物车
我的