Adapter分組封裝

YCGroupAdapter

  • 01.前沿說明
    • 1.1 案例展示效果
    • 1.2 該庫(kù)功能和優(yōu)勢(shì)
    • 1.3 相關(guān)類介紹說明
  • 02.如何使用
    • 2.1 如何引入
    • 2.2 最簡(jiǎn)單使用
    • 2.3 使用建議
  • 03.常用api
    • 3.1 自定義adapter
    • 3.2 notify相關(guān)
    • 3.3 點(diǎn)擊事件listener
  • 04.實(shí)現(xiàn)步驟
    • 4.1 業(yè)務(wù)需求分析
    • 4.2 adapter實(shí)現(xiàn)多type
    • 4.3 這樣寫的弊端
    • 4.4 分組實(shí)體bean
    • 4.5 構(gòu)建封裝adapter
  • 05.優(yōu)化相關(guān)
  • 06.關(guān)于參考
  • 07.其他說明介紹

01.前沿說明

1.1 案例展示效果

  • demo中的效果圖
  • image

    image

    image

    image

    image

    image

    image
  • 實(shí)際項(xiàng)目中的效果圖
  • image

    image

1.2 該庫(kù)功能和優(yōu)勢(shì)

  • 按組劃分的自定義adapter適配器,一個(gè)recyclerView可以完成強(qiáng)大的group+children類型的業(yè)務(wù)需求。
  • 每組支持添加header,footer,children,且每一個(gè)都支持設(shè)置多類型type的view視圖。
  • 支持局部插入刷新,局部移除刷新,也就是說可以按組插入或者移除數(shù)據(jù),或者按組中child的某個(gè)未知插入或者移除數(shù)據(jù)。
  • 支持組中header,footer,child的各個(gè)視圖view的自定義點(diǎn)擊事件。且返回具體的索引!
  • 常見使用場(chǎng)景:仿懂車帝,汽車之家分組圖片查看器;仿QQ聯(lián)系人分組,可以折疊和伸展;以及復(fù)雜分組頁(yè)面……
  • 添加了object同步鎖處理adapter中data添加,獲取和移除等方法,有效避免多線程或者其他操作導(dǎo)致數(shù)據(jù)錯(cuò)位或者偶發(fā)性fast-fail。

02.如何使用

2.1 如何引入

  • 如下所示
    implementation 'cn.yc:GroupAdapterLib:1.0.3'
    

2.2 最簡(jiǎn)單使用

  • 必須的三個(gè)步驟代碼,如下所示
    mRecyclerView.setLayoutManager(new LinearLayoutManager(this));
    mAdapter = new GroupedSecondAdapter(this, list);
    mRecyclerView.setAdapter(mAdapter);
    
  • 關(guān)于如何實(shí)現(xiàn)仿照QQ分組的功能
    /**
     * 判斷當(dāng)前組是否展開
     *
     * @param groupPosition
     * @return
     */
    public boolean isExpand(int groupPosition) {
        GroupEntity entity = mGroups.get(groupPosition);
        return entity.isExpand();
    }
    
    /**
     * 展開一個(gè)組
     *
     * @param groupPosition
     */
    public void expandGroup(int groupPosition) {
        expandGroup(groupPosition, false);
    }
    
    /**
     * 展開一個(gè)組
     *
     * @param groupPosition
     * @param animate
     */
    public void expandGroup(int groupPosition, boolean animate) {
        GroupEntity entity = mGroups.get(groupPosition);
        entity.setExpand(true);
        if (animate) {
            notifyChildrenInserted(groupPosition);
        } else {
            notifyDataChanged();
        }
    }
    
    /**
     * 收起一個(gè)組
     *
     * @param groupPosition
     */
    public void collapseGroup(int groupPosition) {
        collapseGroup(groupPosition, false);
    }
    
    /**
     * 收起一個(gè)組
     *
     * @param groupPosition
     * @param animate
     */
    public void collapseGroup(int groupPosition, boolean animate) {
        GroupEntity entity = mGroups.get(groupPosition);
        entity.setExpand(false);
        if (animate) {
            notifyChildrenRemoved(groupPosition);
        } else {
            notifyDataChanged();
        }
    }
    
    /**
     * 收起所有的組
     */
    public void collapseGroup() {
        for (int i=0 ; i<mGroups.size() ; i++){
            GroupEntity entity = mGroups.get(i);
            entity.setExpand(false);
        }
        notifyDataChanged();
    }
    

03.常用api

3.1 自定義adapter

  • 代碼如下所示
    public class GroupedSecondAdapter extends AbsGroupedAdapter {
    
        private List<GroupEntity> mGroups;
    
        public GroupedSecondAdapter(Context context, List<GroupEntity> groups) {
            super(context);
            mGroups = groups;
        }
    
        @Override
        public int getGroupCount() {
            return mGroups == null ? 0 : mGroups.size();
        }
    
        @Override
        public int getChildrenCount(int groupPosition) {
            if (mGroups!=null){
                ArrayList<ChildEntity> children = mGroups.get(groupPosition).getChildren();
                return children == null ? 0 : children.size();
            }
            return 0;
        }
    
        @Override
        public boolean hasHeader(int groupPosition) {
            return true;
        }
    
        @Override
        public boolean hasFooter(int groupPosition) {
            return true;
        }
    
        @Override
        public int getHeaderLayout(int viewType) {
            return R.layout.item_text_header;
        }
    
        @Override
        public int getFooterLayout(int viewType) {
            return R.layout.item_text_footer;
        }
    
        @Override
        public int getChildLayout(int viewType) {
            return R.layout.item_content_view;
        }
    
        @Override
        public void onBindHeaderViewHolder(GroupViewHolder holder, int groupPosition) {
            
        }
    
        @Override
        public void onBindFooterViewHolder(GroupViewHolder holder, int groupPosition) {
            
        }
    
        @Override
        public void onBindChildViewHolder(GroupViewHolder holder, int groupPosition, int childPosition) {
            
        }
    
    }
    
  • 那么如何控制組中的header或者footer是否顯示呢?
    • 返回true表示顯示,返回false表示不顯示……就是這么簡(jiǎn)單
    @Override
    public boolean hasHeader(int groupPosition) {
        return true;
    }
    
    @Override
    public boolean hasFooter(int groupPosition) {
        return true;
    }
    

3.2 notify相關(guān)

  • 插入數(shù)據(jù)
    //通知一組數(shù)據(jù)插入
    mAdapter.notifyGroupInserted(1);
    //通知一個(gè)子項(xiàng)到組里插入
    mAdapter.notifyChildInserted(1,3);
    //通知一組里的多個(gè)子項(xiàng)插入
    mAdapter.notifyChildRangeInserted(1,2,10);
    //通知一組里的所有子項(xiàng)插入
    mAdapter.notifyChildrenInserted(1);
    //通知多組數(shù)據(jù)插入
    mAdapter.notifyGroupRangeInserted(1,3);
    //通知組頭插入
    mAdapter.notifyHeaderInserted(1);
    //通知組尾插入
    mAdapter.notifyFooterInserted(1);
    
  • 移除數(shù)據(jù)
    //通知所有數(shù)據(jù)刪除
    mAdapter.notifyDataRemoved();
    //通知一組數(shù)據(jù)刪除,包括組頭,組尾和子項(xiàng)
    mAdapter.notifyGroupRemoved(1);
    //通知多組數(shù)據(jù)刪除,包括組頭,組尾和子項(xiàng)
    mAdapter.notifyGroupRangeRemoved(1,3);
    //通知組頭刪除
    mAdapter.notifyHeaderRemoved(1);
    //通知組尾刪除
    mAdapter.notifyFooterRemoved(1);
    //通知一組里的某個(gè)子項(xiàng)刪除
    mAdapter.notifyChildRemoved(1,3);
    //通知一組里的多個(gè)子項(xiàng)刪除
    mAdapter.notifyChildRangeRemoved(1,3,4);
    //通知一組里的所有子項(xiàng)刪除
    mAdapter.notifyChildrenRemoved(1);
    

3.3 點(diǎn)擊事件listener

  • 設(shè)置組header點(diǎn)擊事件
    mAdapter.setOnHeaderClickListener(new OnHeaderClickListener() {
        @Override
        public void onHeaderClick(AbsGroupedAdapter adapter, GroupViewHolder holder,
                                  int groupPosition) {
            Toast.makeText(SecondActivity.this,
                    "組頭:groupPosition = " + groupPosition,Toast.LENGTH_LONG).show();
        }
    });
    
  • 設(shè)置組footer點(diǎn)擊事件
    mAdapter.setOnFooterClickListener(new OnFooterClickListener() {
        @Override
        public void onFooterClick(AbsGroupedAdapter adapter, GroupViewHolder holder,
                                  int groupPosition) {
            Toast.makeText(SecondActivity.this,
                    "組尾:groupPosition = " + groupPosition,Toast.LENGTH_LONG).show();
        }
    });
    
  • 設(shè)置組中children點(diǎn)擊事件
    mAdapter.setOnChildClickListener(new OnChildClickListener() {
        @Override
        public void onChildClick(AbsGroupedAdapter adapter, GroupViewHolder holder,
                                 int groupPosition, int childPosition) {
            Toast.makeText(SecondActivity.this,"子項(xiàng):groupPosition = " + groupPosition
                    + ", childPosition = " + childPosition,Toast.LENGTH_LONG).show();
        }
    });
    

04.實(shí)現(xiàn)步驟

4.1 業(yè)務(wù)需求分析

  • 比如在app開發(fā)中,產(chǎn)品說實(shí)現(xiàn)一個(gè)QQ分組的功能,要求有收疊功能。同時(shí)在app中,圖片相冊(cè),仿照懂車帝實(shí)現(xiàn)分組圖片??吹竭@樣一個(gè)需求,思考能否用一個(gè)recyclerView實(shí)現(xiàn),使用type來區(qū)分不同類型布局。
  • RecyclerView 可以用ViewType來區(qū)分不同的item,也可以滿足需求,但還是存在一些問題,比如:
    • 1,在item過多邏輯復(fù)雜列表界面,Adapter里面的代碼量龐大,邏輯復(fù)雜,后期難以維護(hù)。
    • 2,每次增加一個(gè)列表都需要增加一個(gè)Adapter,重復(fù)搬磚,效率低下。
    • 3,無法復(fù)用adapter,假如有多個(gè)頁(yè)面有多個(gè)type,那么就要寫多個(gè)adapter。
    • 4,要是有局部刷新,那么就比較麻煩了,比如廣告區(qū)也是一個(gè)九宮格的RecyclerView,點(diǎn)擊局部刷新當(dāng)前數(shù)據(jù),比較麻煩。

4.2 adapter實(shí)現(xiàn)多個(gè)type

  • 通常寫一個(gè)多Item列表的方法
    • 根據(jù)不同的ViewType 處理不同的item,如果邏輯復(fù)雜,這個(gè)類的代碼量是很龐大的。如果版本迭代添加新的需求,修改代碼很麻煩,后期維護(hù)困難。
  • 主要操作步驟
    • 在onCreateViewHolder中根據(jù)viewType參數(shù),也就是getItemViewType的返回值來判斷需要?jiǎng)?chuàng)建的ViewHolder類型
    • 在onBindViewHolder方法中對(duì)ViewHolder的具體類型進(jìn)行判斷,分別為不同類型的ViewHolder進(jìn)行綁定數(shù)據(jù)與邏輯處理
  • 代碼如下所示
    public class HomePageAdapter extends RecyclerView.Adapter {
        public static final int TYPE_HEADER = 1;
        public static final int TYPE_FOOTER = 2;
        public static final int TYPE_IMAGE = 3;
        private List<HomePageEntry> mData;
    
        public void setData(List<HomePageEntry> data) {
            mData = data;
        }
    
        @Override
        public RecyclerView.ViewHolder onCreateViewHolder(ViewGroup parent, int viewType) {
            switch (viewType){
                case TYPE_HEADER:
                    return new HeaderViewHolder(LayoutInflater.from(parent.getContext()).inflate(R.layout.home_ad_item_layout,null));
                case TYPE_FOOTER:
                    return new FooterViewHolder(LayoutInflater.from(parent.getContext()).inflate(R.layout.home_text_item_layout,null));
                case TYPE_CHILD:
                    return new ChildViewHolder(LayoutInflater.from(parent.getContext()).inflate(R.layout.home_image_item_layout,null));
            }
            return null;
        }
    
        @Override
        public void onBindViewHolder(RecyclerView.ViewHolder holder, int position) {
            int type = getItemViewType(position);
            switch (type){
                case TYPE_HEADER:
                    // TYPE_HEADER 邏輯處理
                    break;
                case TYPE_FOOTER:
                    // TYPE_FOOTER 邏輯處理
                    break;
                case TYPE_CHILD:
                    // TYPE_CHILD 邏輯處理
                    break;
            }
        }
    
        @Override
        public int getItemViewType(int position) {
             return mData.get(position).type;//type 的值為TYPE_HEADER,TYPE_FOOTER,TYPE_AD,等其中一個(gè)
        }
    
        @Override
        public int getItemCount() {
            return mData == null ? 0:mData.size();
        }
    
        public static class HeaderViewHolder extends RecyclerView.ViewHolder{
            public HeaderViewHolder(View itemView) {
                super(itemView);
                //綁定控件
            }
        }
        //省略部分代碼
    }
    

4.3 這樣寫的弊端

  • 上面那樣寫的弊端
    • 類型檢查與類型轉(zhuǎn)型,由于在onCreateViewHolder根據(jù)不同類型創(chuàng)建了不同的ViewHolder,所以在onBindViewHolder需要針對(duì)不同類型的ViewHolder進(jìn)行數(shù)據(jù)綁定與邏輯處理,這導(dǎo)致需要通過instanceof對(duì)ViewHolder進(jìn)行類型檢查與類型轉(zhuǎn)型。
    • 不利于維護(hù),這點(diǎn)應(yīng)該是上一點(diǎn)的延伸,隨著列表中布局類型的增加與變更,getItemViewType、onCreateViewHolder、onBindViewHolder中的代碼都需要變更或增加,Adapter 中的代碼會(huì)變得臃腫與混亂,增加了代碼的維護(hù)成本。
    • 比如,在分組控件中,類似QQ分組那樣,點(diǎn)擊組中的header,可以切換關(guān)閉和伸展該組中children的自選項(xiàng)item,那么如果不封裝,adapter對(duì)數(shù)據(jù)處理也比較麻煩。
    • 有時(shí)候,在分組控件中,有的組不想顯示header,有的組不想顯示footer,那么這個(gè)時(shí)候就不太靈活。能否使用一個(gè)開關(guān)方法來控制header和footer的顯示和隱藏呢?

4.4 分組實(shí)體bean

  • 通過GroupStructure記錄每個(gè)組是否有頭部,是否有尾部和子項(xiàng)的數(shù)量。從而能方便的計(jì)算列表的長(zhǎng)度和每個(gè)組的組頭、組尾和子項(xiàng)在列表中的位置。

4.5 構(gòu)建封裝adapter

  • 核心目的就是三個(gè)
    • 避免類的類型檢查與類型轉(zhuǎn)型
    • 增強(qiáng)Adapter的擴(kuò)展性
    • 增強(qiáng)Adapter的可維護(hù)性
  • 當(dāng)列表中類型增加或減少時(shí)Adapter中主要改動(dòng)的就是getItemViewType、onCreateViewHolder、onBindViewHolder這三個(gè)方法,因此,我們就從這三個(gè)方法中開始著手。
  • 在getItemViewType方法中。
    • if之類的邏輯判斷簡(jiǎn)化代碼,可以簡(jiǎn)單粗暴的用作為TYPE_HEADER,TYPE_FOOTER,TYPE_CHILD增加type標(biāo)識(shí)。
    • 既然是分組adapter,首先是獲取組的索引,然后通過組的索引來判斷type的類型,最后在返回具體的itemType類型。
    @Override
    public int getItemViewType(int position) {
        itemType = position;
        int groupPosition = getGroupPositionForPosition(position);
        int type = judgeType(position);
        if (type == TYPE_HEADER) {
            return getHeaderViewType(groupPosition);
        } else if (type == TYPE_FOOTER) {
            return getFooterViewType(groupPosition);
        } else if (type == TYPE_CHILD) {
            int childPosition = getChildPositionForPosition(groupPosition, position);
            return getChildViewType(groupPosition, childPosition);
        }
        return super.getItemViewType(position);
    }
    
    /**
     * 判斷item的type 頭部 尾部 和 子項(xiàng)
     *
     * @param position
     * @return
     */
    public int judgeType(int position) {
        int itemCount = 0;
        //獲取組的數(shù)量
        int groupCount = mStructures.size();
    
        for (int i = 0; i < groupCount; i++) {
            GroupStructure structure = mStructures.get(i);
    
            //判斷是否有header頭部view
            if (structure.hasHeader()) {
                itemCount += 1;
                if (position < itemCount) {
                    return TYPE_HEADER;
                }
            }
    
            //獲取孩子的數(shù)量
            itemCount += structure.getChildrenCount();
            if (position < itemCount) {
                return TYPE_CHILD;
            }
    
            //判斷是否有footer數(shù)量
            if (structure.hasFooter()) {
                itemCount += 1;
                if (position < itemCount) {
                    return TYPE_FOOTER;
                }
            }
        }
    
        //以防萬一,為了避免在插入刷新,移除刷新時(shí),避免索引越界異常,不要throw異常
        //即使當(dāng) position == getItemCount() 為true時(shí),可以用空頁(yè)面替代
        return TYPE_NO;
        //throw new IndexOutOfBoundsException("can't determine the item type of the position." +
        //        "position = " + position + ",item count = " + getItemCount());
    }
    //省略部分代碼,具體可以看lib中源代碼
    
  • 在onCreateViewHolder方法中
    • 創(chuàng)建viewHolder,主要作用是創(chuàng)建Item視圖,并返回相應(yīng)的ViewHolder。這個(gè)地方,需要注意一下,在分組控件中,能否把組的header,footer,children等布局暴露給外部開發(fā)者創(chuàng)建?
    • 因此,這里需要區(qū)分類型,然后返回對(duì)應(yīng)的布局,這里返回對(duì)應(yīng)的布局幾個(gè)方法,可以弄成抽象的方法,子類必須實(shí)現(xiàn)。讓子類返回具體的header,footer,children布局。
    @NonNull
    @Override
    public RecyclerView.ViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) {
        View view;
        if (viewType != TYPE_NO){
            int layoutId = getLayoutId(itemType, viewType);
            if (inflater==null){
                inflater = LayoutInflater.from(mContext);
            }
            view = inflater.inflate(layoutId, parent, false);
        } else {
            //使用空布局
            //未知類型可以使用空布局代替
            view = new View(parent.getContext());
        }
        return new GroupViewHolder(view);
    }
    
    private int getLayoutId(int position, int viewType) {
        int type = judgeType(position);
        if (type == TYPE_HEADER) {
            return getHeaderLayout(viewType);
        } else if (type == TYPE_FOOTER) {
            return getFooterLayout(viewType);
        } else if (type == TYPE_CHILD) {
            return getChildLayout(viewType);
        }
        return 0;
    }
    
  • 在onBindViewHolder方法中
    • 這個(gè)方法中主要做兩個(gè)事情,第一個(gè)是設(shè)置組中的header,footer,還有children的點(diǎn)擊事件,并且需要返回具體的索引,包括組索引,和組中孩子的索引。
    • 第二個(gè)是綁定viewHolder,主要作用是綁定數(shù)據(jù)到正確的Item視圖上,這個(gè)可以把方法抽象,讓子類去實(shí)現(xiàn)。
    @Override
    public void onBindViewHolder(@NonNull final RecyclerView.ViewHolder holder, int position) {
        int type = judgeType(position);
        final int groupPosition = getGroupPositionForPosition(position);
        if (type == TYPE_HEADER) {
            if (mOnHeaderClickListener != null) {
                holder.itemView.setOnClickListener(new View.OnClickListener() {
                    @Override
                    public void onClick(View v) {
                        if (mOnHeaderClickListener != null) {
                            mOnHeaderClickListener.onHeaderClick(AbsGroupAdapter.this,
                                    (GroupViewHolder) holder, groupPosition);
                        }
                    }
                });
            }
            onBindHeaderViewHolder((GroupViewHolder) holder, groupPosition);
        } else if (type == TYPE_FOOTER) {
            if (mOnFooterClickListener != null) {
                holder.itemView.setOnClickListener(new View.OnClickListener() {
                    @Override
                    public void onClick(View v) {
                        if (mOnFooterClickListener != null) {
                            mOnFooterClickListener.onFooterClick(AbsGroupAdapter.this,
                                    (GroupViewHolder) holder, groupPosition);
                        }
                    }
                });
            }
            onBindFooterViewHolder((GroupViewHolder) holder, groupPosition);
        } else if (type == TYPE_CHILD) {
            final int childPosition = getChildPositionForPosition(groupPosition, position);
            if (mOnChildClickListener != null) {
                holder.itemView.setOnClickListener(new View.OnClickListener() {
                    @Override
                    public void onClick(View v) {
                        if (mOnChildClickListener != null) {
                            mOnChildClickListener.onChildClick(AbsGroupAdapter.this,
                                    (GroupViewHolder) holder, groupPosition, childPosition);
                        }
                    }
                });
            }
            onBindChildViewHolder((GroupViewHolder) holder, groupPosition, childPosition);
        }
    }
    
  • 封裝后好處
    • 拓展性——每組支持添加header,footer,children,且每一個(gè)都支持設(shè)置多類型type的view視圖。而且支持局部插入刷新,局部移除刷新,也就是說可以按組插入或者移除數(shù)據(jù),或者按組中child的某個(gè)未知插入或者移除數(shù)據(jù)。
    • 可維護(hù)性——不同的列表類型由adapter添加header,footer,children類型處理,相互之間互不干擾,代碼簡(jiǎn)潔,維護(hù)成本低。還可以靈活控制header,footer類型的布局是否可見,特別靈活!

參考案例說明

其他推薦說明

關(guān)于LICENSE

Copyright 2017 yangchong211(github.com/yangchong211)

Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at

http://www.apache.org/licenses/LICENSE-2.0

Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.

開源庫(kù)地址:https://github.com/yangchong211/YCGroupAdapter

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請(qǐng)結(jié)合常識(shí)與多方信息審慎甄別。
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡(jiǎn)書系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容