Android Linkage-RecyclerView源碼閱讀

當前版本 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頂端.
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章