天天看點

Adapter分組封裝YCGroupAdapter

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.其他說明介紹

  • demo中的效果圖
  • Adapter分組封裝YCGroupAdapter
    Adapter分組封裝YCGroupAdapter
Adapter分組封裝YCGroupAdapter
Adapter分組封裝YCGroupAdapter
Adapter分組封裝YCGroupAdapter
Adapter分組封裝YCGroupAdapter
Adapter分組封裝YCGroupAdapter
  • 實際項目中的效果圖
  • Adapter分組封裝YCGroupAdapter
    Adapter分組封裝YCGroupAdapter

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

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

  • 必須的三個步驟代碼,如下所示
    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();
    }           

  • 代碼如下所示
    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;
    }           

  • 插入資料
    //通知一組資料插入
    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);           

  • 設定組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();
    }
    });           

  • 比如在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);
                //綁定控件
            }
        }
        //省略部分代碼
    }           

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

  • 通過GroupStructure記錄每個組是否有頭部,是否有尾部和子項的數量。進而能友善的計算清單的長度和每個組的組頭、組尾和子項在清單中的位置。

  • 核心目的就是三個
    • 避免類的類型檢查與類型轉型
    • 增強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類型的布局是否可見,特别靈活!

參考案例說明

其他推薦說明

關于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