Adapter分組封裝

YCGroupAdapter

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

01.前沿說明

1.1 案例展示效果

  • demo中的效果圖

  • image
    image
    image
    image
    image
    image
    image

  • 實際項目中的效果圖

  • image
    image

1.2 該庫功能和優勢

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

02.如何使用

2.1 如何引入

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

2.2 最簡單使用

  • 必須的三個步驟代碼,如下所示
    mRecyclerView.setLayoutManager(new LinearLayoutManager(this));
    mAdapter = new GroupedSecondAdapter(this, list);
    mRecyclerView.setAdapter(mAdapter);
    
  • 關於如何實現仿照QQ分組的功能
    /**
     * 判斷當前組是否展開
     *
     * @param groupPosition
     * @return
     */
    public boolean isExpand(int groupPosition) {
        GroupEntity entity = mGroups.get(groupPosition);
        return entity.isExpand();
    }
    
    /**
     * 展開一個組
     *
     * @param groupPosition
     */
    public void expandGroup(int groupPosition) {
        expandGroup(groupPosition, false);
    }
    
    /**
     * 展開一個組
     *
     * @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();
        }
    }
    
    /**
     * 收起一個組
     *
     * @param groupPosition
     */
    public void collapseGroup(int groupPosition) {
        collapseGroup(groupPosition, false);
    }
    
    /**
     * 收起一個組
     *
     * @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表示不顯示……就是這麼簡單
    @Override
    public boolean hasHeader(int groupPosition) {
        return true;
    }
    
    @Override
    public boolean hasFooter(int groupPosition) {
        return true;
    }
    

3.2 notify相關

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

3.3 點擊事件listener

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

04.實現步驟

4.1 業務需求分析

  • 比如在app開發中,產品說實現一個QQ分組的功能,要求有收疊功能。同時在app中,圖片相冊,仿照懂車帝實現分組圖片。看到這樣一個需求,思考能否用一個recyclerView實現,使用type來區分不同類型佈局。
  • RecyclerView 可以用ViewType來區分不同的item,也可以滿足需求,但還是存在一些問題,比如:
    • 1,在item過多邏輯複雜列表界面,Adapter裏面的代碼量龐大,邏輯複雜,後期難以維護。
    • 2,每次增加一個列表都需要增加一個Adapter,重複搬磚,效率低下。
    • 3,無法複用adapter,假如有多個頁面有多個type,那麼就要寫多個adapter。
    • 4,要是有局部刷新,那麼就比較麻煩了,比如廣告區也是一個九宮格的RecyclerView,點擊局部刷新當前數據,比較麻煩。

4.2 adapter實現多個type

  • 通常寫一個多Item列表的方法
    • 根據不同的ViewType 處理不同的item,如果邏輯複雜,這個類的代碼量是很龐大的。如果版本迭代添加新的需求,修改代碼很麻煩,後期維護困難。
  • 主要操作步驟
    • 在onCreateViewHolder中根據viewType參數,也就是getItemViewType的返回值來判斷需要創建的ViewHolder類型
    • 在onBindViewHolder方法中對ViewHolder的具體類型進行判斷,分別爲不同類型的ViewHolder進行綁定數據與邏輯處理
  • 代碼如下所示
    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,等其中一個
        }
    
        @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 這樣寫的弊端

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

4.4 分組實體bean

  • 通過GroupStructure記錄每個組是否有頭部,是否有尾部和子項的數量。從而能方便的計算列表的長度和每個組的組頭、組尾和子項在列表中的位置。

4.5 構建封裝adapter

  • 核心目的就是三個
    • 避免類的類型檢查與類型轉型
    • 增強Adapter的擴展性
    • 增強Adapter的可維護性
  • 當列表中類型增加或減少時Adapter中主要改動的就是getItemViewType、onCreateViewHolder、onBindViewHolder這三個方法,因此,我們就從這三個方法中開始着手。
  • 在getItemViewType方法中。
    • if之類的邏輯判斷簡化代碼,可以簡單粗暴的用作爲TYPE_HEADER,TYPE_FOOTER,TYPE_CHILD增加type標識。
    • 既然是分組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 頭部 尾部 和 子項
     *
     * @param position
     * @return
     */
    public int judgeType(int position) {
        int itemCount = 0;
        //獲取組的數量
        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;
                }
            }
    
            //獲取孩子的數量
            itemCount += structure.getChildrenCount();
            if (position < itemCount) {
                return TYPE_CHILD;
            }
    
            //判斷是否有footer數量
            if (structure.hasFooter()) {
                itemCount += 1;
                if (position < itemCount) {
                    return TYPE_FOOTER;
                }
            }
        }
    
        //以防萬一,爲了避免在插入刷新,移除刷新時,避免索引越界異常,不要throw異常
        //即使當 position == getItemCount() 爲true時,可以用空頁面替代
        return TYPE_NO;
        //throw new IndexOutOfBoundsException("can't determine the item type of the position." +
        //        "position = " + position + ",item count = " + getItemCount());
    }
    //省略部分代碼,具體可以看lib中源代碼
    
  • 在onCreateViewHolder方法中
    • 創建viewHolder,主要作用是創建Item視圖,並返回相應的ViewHolder。這個地方,需要注意一下,在分組控件中,能否把組的header,footer,children等佈局暴露給外部開發者創建?
    • 因此,這裏需要區分類型,然後返回對應的佈局,這裏返回對應的佈局幾個方法,可以弄成抽象的方法,子類必須實現。讓子類返回具體的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方法中
    • 這個方法中主要做兩個事情,第一個是設置組中的header,footer,還有children的點擊事件,並且需要返回具體的索引,包括組索引,和組中孩子的索引。
    • 第二個是綁定viewHolder,主要作用是綁定數據到正確的Item視圖上,這個可以把方法抽象,讓子類去實現。
    @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,且每一個都支持設置多類型type的view視圖。而且支持局部插入刷新,局部移除刷新,也就是說可以按組插入或者移除數據,或者按組中child的某個未知插入或者移除數據。
    • 可維護性——不同的列表類型由adapter添加header,footer,children類型處理,相互之間互不干擾,代碼簡潔,維護成本低。還可以靈活控制header,footer類型的佈局是否可見,特別靈活!

參考案例說明

  • https://github.com/msdx/group-recycler-adapter
  • https://www.jianshu.com/p/7b5607a7fbe1
  • https://www.jianshu.com/p/26b0911f396f
  • https://github.com/donkingliang/GroupedRecyclerViewAdapter
  • https://www.jianshu.com/p/1558cbd20d17

其他推薦說明

關於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.

開源庫地址:https://github.com/yangchong211/YCGroupAdapter

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章