当前版本 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顶端.