Android Linkage-RecyclerView源码阅读

欧阳衡
2023-12-01

当前版本 1.9.2
项目地址

概述
  • 自定义LinkageRecyclerView控件,该控件布局中含有两个RecyclerView控件,左边为主Rv,右边为次Rv.
  • 次Rv顶部有一个悬挂头View,该View专门用来展示次Rv中每个分组的组名称.
  • 监听次Rv的滑动事件,根据屏幕中展示的次Rv中的第一条目索引的改变来判断当前组名称时候有更改,如果有更改组名称就拿到该组名称在原始数据中的索引位置,进而拿到主Rv中该组名称对应索引,使得主Rv滑动到该索引位置.
  • 监听主Rv中item被点击的事件,通过ViewHolder拿到该条目的索引,进而获取该条目在原始数据集合中的索引,然后通知次Rv滑动到该索引对应的item位置,这些滑动都是将item滑动到Rv的顶端.
    这个控件的更多功能请查看该项目的README,这里仅仅只分析了一部分功能.
示例代码

这里以项目中Demo里面的RxMagicSampleFragment使用LinkageRecyclerView为例来分析

Gson gson = new Gson();
// 将字符串格式化
List<DefaultGroupedItem> items = gson.fromJson(getString(R.string.operators_json),new TypeToken<List<DefaultGroupedItem>>() {}.getType());
// 对LinkageRecyclerView初始化
linkage.init(items);
// 设置一些回调
linkage.setDefaultOnItemBindListener(...);
LinkageRecyclerView初始化
public class LinkageRecyclerView<T extends BaseGroupedItem.ItemInfo> extends RelativeLayout {
    // 次Rv悬挂头View,专门展示组名用
    private TextView mTvHeader;
    // 组名称集合
    private List<String> mInitGroupNames;
    // 原始数据集合
    private List<BaseGroupedItem<T>> mInitItems;
    // 头部元素对应的索引
    private List<Integer> mHeaderPositions = new ArrayList<>();
    // 次Rv悬挂头高度
    private int mTitleHeight;
    // 屏幕中次Rv屏幕中第一个可见条目在数据源中的索引.
    private int mFirstVisiblePosition;
    // 上一次在悬挂头View中的名称
    private String mLastGroupName;
    // 构造
    public LinkageRecyclerView(Context context, @Nullable AttributeSet attrs) {
        super(context, attrs);
        initView(context, attrs);
    }
    /**
     * LinkageRecyclerView控件被初始化的时候,就会将R.layout.layout_linkage_view布局中的主RecyclerView与次RecyclerView等都通过findViewById找到
     *
     * @param context
     * @param attrs
     */
    private void initView(Context context, @Nullable AttributeSet attrs) {
        this.mContext = context;
        View view = LayoutInflater.from(context).inflate(R.layout.layout_linkage_view, this);
        // 主Rv
        mRvPrimary = (RecyclerView) view.findViewById(R.id.rv_primary);
        // 次要Rv
        mRvSecondary = (RecyclerView) view.findViewById(R.id.rv_secondary);
        // 次Rv中,在次Rv上方的专门展示组名称用的容器
        mHeaderContainer = (FrameLayout) view.findViewById(R.id.header_container);
        // 外部父容器
        mLinkageLayout = (LinearLayout) view.findViewById(R.id.linkage_layout);
    }
    // 初始化数据
    public void init(List<BaseGroupedItem<T>> linkageItems) {
        init(linkageItems, new DefaultLinkagePrimaryAdapterConfig(), new DefaultLinkageSecondaryAdapterConfig());
    }
    /**
     * @param linkageItems           原始数据源
     * @param primaryAdapterConfig   主要适配器配置
     * @param secondaryAdapterConfig 次要适配器配置
     */
    public void init(List<BaseGroupedItem<T>> linkageItems, ILinkagePrimaryAdapterConfig primaryAdapterConfig, ILinkageSecondaryAdapterConfig secondaryAdapterConfig) {
        // 主要适配器与次要适配器的初始化,以及为Rv设置适配器的工作
        initRecyclerView(primaryAdapterConfig, secondaryAdapterConfig);
        // 原始数据集合
        this.mInitItems = linkageItems;
        String lastGroupName = null;
        List<String> groupNames = new ArrayList<>();
        // 遍历原始数据集合
        if (mInitItems != null && mInitItems.size() > 0) {
            for (BaseGroupedItem<T> item1 : mInitItems) {
                if (item1.isHeader) {
                    // 获取原始数据集合中每个组的名称,并存入集合groupNames中
                    groupNames.add(item1.header);
                    // 获取原始数据集合中最后一个组的名称,并用变量lastGroupName接收
                    lastGroupName = item1.header;
                }
            }
        }
        // 获取头部元素的索引存入集合中
        if (mInitItems != null) {
            for (int i = 0; i < mInitItems.size(); i++) {
                if (mInitItems.get(i).isHeader) {
                    // 如果原始数据中的某个元素是头部元素,就将该头部元素对应的索引存入mHeaderPositions集合中
                    mHeaderPositions.add(i);
                }
            }
        }
        DefaultGroupedItem.ItemInfo info = new DefaultGroupedItem.ItemInfo(null, lastGroupName);
        // 创建一个DefaultGroupedItem对象,该对象中有用的变量就是DefaultGroupedItem.ItemInfo对象中的group(这个值表示的是组名称)
        BaseGroupedItem<T> footerItem = (BaseGroupedItem<T>) new DefaultGroupedItem(info);
        // 将表示最后组信息的对象存入原始数据集合中.
        mInitItems.add(footerItem);
        // 将组名称集合交由mInitGroupNames变量保存
        this.mInitGroupNames = groupNames;
        // 经过上面的那些初始化适配器,处理数据,设置适配器等完成之后就开始正式为两个Rv设置新的数据了.
        mPrimaryAdapter.initData(mInitGroupNames);
        mSecondaryAdapter.initData(mInitItems);
        //  次Rv悬挂滑动,并且关联上主Rv滑动到相应的组名条目
        initLinkageSecondary();
    }
    
    /**
     * 主要适配器与次要适配器的初始化
     * @param primaryAdapterConfig   主要适配器配置
     * @param secondaryAdapterConfig 次要适配器配置
     */
    private void initRecyclerView(ILinkagePrimaryAdapterConfig primaryAdapterConfig, ILinkageSecondaryAdapterConfig secondaryAdapterConfig) {
        // mInitGroupNames: 表示组名称集合
        // 创建主要适配器
        mPrimaryAdapter = new LinkagePrimaryAdapter(mInitGroupNames, primaryAdapterConfig,
                new LinkagePrimaryAdapter.OnLinkageListener() {
                    @Override
                    public void onLinkageClick(LinkagePrimaryViewHolder holder, String title) {
                        if (isScrollSmoothly()) {
                            // 是平滑滚动
                            // mRvSecondary:次Rv
                            // LinearSmoothScroller.SNAP_TO_START:平滑滚动置顶
                            // mHeaderPositions.get(holder.getAdapterPosition()): holder.getAdapterPosition()获取的是组名对应的组名集合中的索引,
                            // 然后mHeaderPositions.get(index)获取的是组名称在原始数据集合中索引值,这样其实就拿到了次Rv中组名的索引了,然后再调用
                            // RecyclerViewScrollHelper.smoothScrollToPosition()方法将该组名对应的item滑动到次Rv的顶部.
                            RecyclerViewScrollHelper.smoothScrollToPosition(mRvSecondary,
                                    LinearSmoothScroller.SNAP_TO_START,
                                    mHeaderPositions.get(holder.getAdapterPosition()));
                        } else {
                            mSecondaryLayoutManager.scrollToPositionWithOffset(
                                    mHeaderPositions.get(holder.getAdapterPosition()), SCROLL_OFFSET);
                        }
                    }
                });
        mPrimaryLayoutManager = new LinearLayoutManager(mContext);
        mRvPrimary.setLayoutManager(mPrimaryLayoutManager);
        // 为主Rv设置主适配器
        mRvPrimary.setAdapter(mPrimaryAdapter);
        // 创建次要适配器
        // mInitItems:原始数据集合
        mSecondaryAdapter = new LinkageSecondaryAdapter(mInitItems, secondaryAdapterConfig);
        // 该方法是用来设置次Rv中的布局格式
        setLevel2LayoutManager();
        // 为次Rv设置适配器
        mRvSecondary.setAdapter(mSecondaryAdapter);
    }
    
    /**
     * 次Rv悬挂滑动,并且关联上主Rv滑动到相应的组名条目
     */
    private void initLinkageSecondary() {
        if (mTvHeader == null && mSecondaryAdapter.getConfig() != null) {
            // 获取次要适配器DefaultLinkageSecondaryAdapterConfig对象
            ILinkageSecondaryAdapterConfig config = mSecondaryAdapter.getConfig();
            // 获取View,这个View就是次要适配器中的悬挂头布局
            int layout = config.getHeaderLayoutId();
            View view = LayoutInflater.from(mContext).inflate(layout, null);
            // 将次Rv悬挂头View添加到展示次Rv组名的容器中
            mHeaderContainer.addView(view);
            // 获取次Rv悬挂头View,专门展示组名用
            mTvHeader = view.findViewById(config.getHeaderTextViewId());
        }
        // mFirstVisiblePosition:屏幕中次Rv屏幕中第一个可见条目在数据源中的索引.
        // 获取数据源中第一个可见条目对应的数据是否有头信息.
        if (mInitItems.get(mFirstVisiblePosition).isHeader) {
            // 如果该条目是有头信息的,那么次Rv悬挂头View就展示该条目对应的组信息.
            mTvHeader.setText(mInitItems.get(mFirstVisiblePosition).header);
        }
        // 监听次Rv滚动
        mRvSecondary.addOnScrollListener(new RecyclerView.OnScrollListener() {
            @Override
            public void onScrollStateChanged(@NonNull RecyclerView recyclerView, int newState) {
                super.onScrollStateChanged(recyclerView, newState);
                // 次Rv悬挂头高度
                mTitleHeight = mTvHeader.getMeasuredHeight();
            }
            @Override
            public void onScrolled(@NonNull RecyclerView recyclerView, int dx, int dy) {
                super.onScrolled(recyclerView, dx, dy);
                // 次Rv在屏幕中显示的第一个Item所对应的条目
                int firstPosition = mSecondaryLayoutManager.findFirstVisibleItemPosition();
                // 次Rv在屏幕中完全显示的第一个Item所对应的条目
                int firstCompletePosition = mSecondaryLayoutManager.findFirstCompletelyVisibleItemPosition();
                List<BaseGroupedItem<T>> items = mSecondaryAdapter.getItems();
                // 假如屏幕中第一个完整显示的item条目距离屏幕顶端的距离比次Rv悬挂头的高度还要小,
                // 那么随着第一个完整显示的item条目向上的移动时,悬挂头也要向上移动.
                // 效果就是第一个完整显示的item条目将悬挂头顶出屏幕,或者item下滑时,悬挂头展示出来.
                if (firstCompletePosition > 0 && (firstCompletePosition) < items.size() && items.get(firstCompletePosition).isHeader) {
                    View view = mSecondaryLayoutManager.findViewByPosition(firstCompletePosition);
                    if (view != null && view.getTop() <= mTitleHeight) {
                        mTvHeader.setY(view.getTop() - mTitleHeight);
                    }
                }
                // Here is the logic of group title changes and linkage:
                boolean groupNameChanged = false;
                if (mFirstVisiblePosition != firstPosition && firstPosition >= 0) {
                    // 假设屏幕中第一个显示的item索引与上次屏幕中第一个显示item索引不同的话,
                    // 那么就更新mFirstVisiblePosition值
                    mFirstVisiblePosition = firstPosition;
                    // 将次Rv的悬挂头显示出来
                    mTvHeader.setY(0);
                    // 取得该条目对应的数据
                    // 判断该条目是头还是内容条目,最终获取当前条目对应的组名称
                    String currentGroupName = items.get(mFirstVisiblePosition).isHeader
                            ? items.get(mFirstVisiblePosition).header
                            : items.get(mFirstVisiblePosition).info.getGroup();
                    if (TextUtils.isEmpty(mLastGroupName) || !mLastGroupName.equals(currentGroupName)) {
                        // 如果当前item对应的组名称为空或者当前屏幕中显示的第一个item的组名称与上一次item对应的组名称不同.
                        // 1.更新mLastGroupName组名称
                        // 2.标记Rx悬挂头中的组名称已经改了
                        // 3.更改次Rx悬挂头的组名称
                        mLastGroupName = currentGroupName;
                        groupNameChanged = true;
                        mTvHeader.setText(mLastGroupName);
                    }
                }
                // 假如当前次Rx悬挂头的中组名称已经更改了
                if (groupNameChanged) {
                    // 获取组名称集合
                    List<String> groupNames = mPrimaryAdapter.getStrings();
                    // 对该组名称集合进行遍历
                    for (int i = 0; i < groupNames.size(); i++) {
                        // 如果次Rv中悬挂头中的组名称与组名称集合中的某个元素相等,获取该元素的索引.设置主Rv该索引对应的条目被选中.
                        if (groupNames.get(i).equals(mLastGroupName)) {
                            // 设置条目被选中
                            mPrimaryAdapter.setSelectedPosition(i);
                            // 平滑的滑动某条目,并将该条目置顶.
                            RecyclerViewScrollHelper.smoothScrollToPosition(mRvPrimary, LinearSmoothScroller.SNAP_TO_END, i);
                        }
                    }
                }
            }
        });
    }
}
LinkagePrimaryAdapter主适配器中逻辑
public class LinkagePrimaryAdapter extends RecyclerView.Adapter<LinkagePrimaryViewHolder> {
    // 组名称集合
    private List<String> mStrings;
    // DefaultLinkagePrimaryAdapterConfig对象适配器配置信息
    private ILinkagePrimaryAdapterConfig mConfig;
    // 组名控件被点击之后的回调
    private OnLinkageListener mLinkageListener;
    public LinkagePrimaryAdapter(List<String> strings, ILinkagePrimaryAdapterConfig config, OnLinkageListener linkageListener) {
        mStrings = strings;// 组名称集合
        if (mStrings == null) {
            mStrings = new ArrayList<>();
        }
        mConfig = config;// DefaultLinkagePrimaryAdapterConfig对象适配器配置信息
        mLinkageListener = linkageListener;// 组名控件被点击之后的回调
    }
    /**
     * 更新列表数据
     * @param list
     */
    public void initData(List<String> list) {
        mStrings.clear();
        if (list != null) {
            mStrings.addAll(list);
        }
        notifyDataSetChanged();
    }
    /**
     * 更新选中item
     * @param selectedPosition
     */
    public void setSelectedPosition(int selectedPosition) {
        mSelectedPosition = selectedPosition;
        notifyDataSetChanged();
    }
    @NonNull
    @Override
    public LinkagePrimaryViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) {
        mContext = parent.getContext();
        // 为DefaultLinkagePrimaryAdapterConfig对象设置mContext.
        mConfig.setContext(mContext);
        // mConfig.getLayoutId(): 返回DefaultLinkagePrimaryAdapterConfig对象中的R.layout.default_adapter_linkage_primary布局ID
        // 获取R.layout.default_adapter_linkage_primary布局对应的View
        mView = LayoutInflater.from(mContext).inflate(mConfig.getLayoutId(), parent, false);
        // LinkagePrimaryViewHolder对象中持有组名View引用以及DefaultLinkagePrimaryAdapterConfig适配器配置对象引用
        return new LinkagePrimaryViewHolder(mView, mConfig);
    }
    @Override
    public void onBindViewHolder(@NonNull final LinkagePrimaryViewHolder holder, int position) {
        // 改变组名View背景为选中状态
        holder.mLayout.setSelected(true);
        // 获取当前组名View对应的索引
        final int adapterPosition = holder.getAdapterPosition();
        // 获取组名称
        final String title = mStrings.get(adapterPosition);
        // 对组名View控件中的内容或者样式进行一些设置.
        mConfig.onBindViewHolder(holder, adapterPosition == mSelectedPosition, title);
        // holder.itemView:代表的是组名View的父控件,可以看作是组名View,也就是设置点击组名View时候的回调.
        holder.itemView.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View v) {
                // 如果初始化主适配器的时候传递了回调对象,那么就执行回调对象中的回调方法.
                if (mLinkageListener != null) {
                    // 在该回调方法中,次Rv相应组的第一条item将被移动到屏幕顶端.
                    mLinkageListener.onLinkageClick(holder, title);
                }
                // DefaultLinkagePrimaryAdapterConfig中的如果有回调也可以被调用.
                mConfig.onItemClick(holder, v, title);
            }
        });
    }
}
DefaultLinkagePrimaryAdapterConfig主要更新item的方法
public class DefaultLinkagePrimaryAdapterConfig implements ILinkagePrimaryAdapterConfig {
    // 返回主Rv中的item布局
    @Override
    public int getLayoutId() {
        return R.layout.default_adapter_linkage_primary;
    }
    // 主主Rv中item的显示组名称的TextViewid
    @Override
    public int getGroupTitleViewId() {
        return R.id.tv_group;
    }
    // 主Rv中item最外层布局id
    @Override
    public int getRootViewId() {
        return R.id.layout_group;
    }
    /***
     * 该方法主要是对组名View进行一些列的设置
     * @param holder   LinkagePrimaryViewHolder 用来获取组名View
     * @param selected selected of this position 当前组名View是否被选中
     * @param title    title of this position 组的名称
     */
    @Override
    public void onBindViewHolder(LinkagePrimaryViewHolder holder, boolean selected, String title) {
        // 获取组名View
        TextView tvTitle = ((TextView) holder.mGroupTitle);
        // 为组名View设置组的名称
        tvTitle.setText(title);

        // 设置组名View是否选中的相应背景
        tvTitle.setBackgroundColor(mContext.getResources().getColor(selected ? R.color.colorPurple : R.color.colorWhite));
        // 设置组名View是否选中的相应字体颜色
        tvTitle.setTextColor(ContextCompat.getColor(mContext, selected ? R.color.colorWhite : R.color.colorGray));
        // 设置组名View如果没有被选中则组名称文字末尾省略号,如果被选中了就跑马灯展示
        tvTitle.setEllipsize(selected ? TextUtils.TruncateAt.MARQUEE : TextUtils.TruncateAt.END);
        // 设置视图是否可以获取焦点
        tvTitle.setFocusable(selected);
        // 设置视图可否获取焦点并保持焦点
        tvTitle.setFocusableInTouchMode(selected);
        // 设置组名View被选中了可以重复动画选框,如果没有被选中则不能有动画.
        tvTitle.setMarqueeRepeatLimit(selected ? MARQUEE_REPEAT_LOOP_MODE : MARQUEE_REPEAT_NONE_MODE);

        if (mListener != null) {
            mListener.onBindViewHolder(holder, title);
        }
    }
}
RecyclerViewScrollHelper使Rv滑动的工具类
public class RecyclerViewScrollHelper {
    /**
     *
     * @param recyclerView
     * @param snapMode 条目置顶还是置底
     * @param position 某条目置顶或置底
     */
    public static void smoothScrollToPosition(RecyclerView recyclerView, int snapMode, int position) {
        RecyclerView.LayoutManager layoutManager = recyclerView.getLayoutManager();
        if (layoutManager instanceof LinearLayoutManager) {
            LinearLayoutManager manager = (LinearLayoutManager) layoutManager;
            LinearSmoothScroller mScroller = null;
            if (snapMode == LinearSmoothScroller.SNAP_TO_START) {
                // 滑动、跳转到某个Position并置顶
                mScroller = new TopSmoothScroller(recyclerView.getContext());
            } else if (snapMode == LinearSmoothScroller.SNAP_TO_END) {
                // 滑动、跳转到某个Position并置底
                mScroller = new BottomSmoothScroller(recyclerView.getContext());
            } else {
                // 平滑滑动、跳转到某个Position
                mScroller = new LinearSmoothScroller(recyclerView.getContext());
            }
            mScroller.setTargetPosition(position);
            // 平滑滑动开始
            manager.startSmoothScroll(mScroller);
        }
    }
    // 让item滑动到Rv顶部
    public static class TopSmoothScroller extends LinearSmoothScroller {
        TopSmoothScroller(Context context) {
            super(context);
        }

        @Override
        protected int getHorizontalSnapPreference() {
            return SNAP_TO_START;
        }

        @Override
        protected int getVerticalSnapPreference() {
            return SNAP_TO_START;
        }
    }
    // 让item滑动到Rv底部
    public static class BottomSmoothScroller extends LinearSmoothScroller {
        BottomSmoothScroller(Context context) {
            super(context);
        }

        @Override
        protected int getHorizontalSnapPreference() {
            return SNAP_TO_END;
        }

        @Override
        protected int getVerticalSnapPreference() {
            return SNAP_TO_END;
        }
    }
}
原始数据源
  • 原始数据源集合中存放着组头信息和组成员信息,他们都是按照顺序排列组头,组成员…组头,组成员…,
  • 处理原始数据的时候会将组名称取出来存入一个单独集合,然后再使用一个集合存储组名称在原始数据集合中对应的索引值,这两个集合拥有相同的长度,同一索引对应组名称与组名称在原始数据集合中的索引.这样在点击主Rv中组名称item时候就可以动态的寻找到次Rv中组名称对应item索引然后将该item移动到次Rv顶端.
 类似资料: