當前版本 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頂端.