version:2.8.5
更多分享請(qǐng)看:http://cherylgood.cn
今天我們來(lái)學(xué)習(xí)BaseRecyclerViewAdapterHelper中有關(guān)實(shí)現(xiàn)可展開(kāi)和折疊二級(jí)Item或多級(jí)Item的源碼。在開(kāi)始學(xué)習(xí)之前,我想先分析下實(shí)現(xiàn)的思路,這樣對(duì)于進(jìn)行源碼的理解效果比較好。
實(shí)現(xiàn)伸展and折疊,很多控件都有,網(wǎng)上也有用linearlayout實(shí)現(xiàn)的功能很強(qiáng)大、很炫酷的開(kāi)源項(xiàng)目,平時(shí)要實(shí)現(xiàn)一些伸縮性的自定義控件,我們也可以是用屬性動(dòng)畫,或者動(dòng)態(tài)控制控件的Layout屬性等都可以實(shí)現(xiàn)。那么現(xiàn)在我們來(lái)想象一下,如果在recyclerview中實(shí)現(xiàn)該功能,相對(duì)來(lái)說(shuō)能想到的比較合適的方式是什么呢?
其實(shí)我們可以很好的利用RecyclerView.Adapter給我們提供的如下一些通知數(shù)據(jù)源更新的方法來(lái)實(shí)現(xiàn)我們的動(dòng)態(tài)伸展and折疊功能。當(dāng)要伸展時(shí),我們動(dòng)態(tài)將下一級(jí)item的數(shù)據(jù)添加在與adapter綁定的數(shù)據(jù)集合中,然后通知layoutManger更新數(shù)據(jù)源。當(dāng)要收縮時(shí),同理,將下一級(jí)的item的數(shù)據(jù)源從與adapter綁定的數(shù)據(jù)集合中移除,然后通知更新。
* @see #notifyItemChanged(int)
* @see #notifyItemInserted(int)
* @see #notifyItemRemoved(int)
* @see #notifyItemRangeChanged(int, int)
* @see #notifyItemRangeInserted(int, int)
* @see #notifyItemRangeRemoved(int, int)
思路:
數(shù)據(jù)bean應(yīng)該有存儲(chǔ)自己數(shù)據(jù)的字段
數(shù)據(jù)bean應(yīng)該有存儲(chǔ)下一級(jí)item列表的集合類型的字段
數(shù)據(jù)bean應(yīng)該有一個(gè)字段標(biāo)識(shí)當(dāng)前item的狀態(tài)(伸展or收縮)
初始化adapter時(shí)只渲染頂級(jí)的item
點(diǎn)擊item是檢測(cè)該item是否支持伸縮
支持伸縮:當(dāng)前狀態(tài)展開(kāi)->折疊(將次級(jí)list插入adapter綁定的data集合中,刷新數(shù)據(jù));當(dāng)前狀態(tài)折疊->展開(kāi)(將次級(jí)的list從與adapter綁定的data集合中移除,刷新數(shù)據(jù))
插入或移除的位置根據(jù)點(diǎn)擊的item確定,插入量與移除量根據(jù)下一級(jí)item數(shù)量確定
插入移除過(guò)程中可以使用動(dòng)畫效果
思路理清之后我們接下來(lái)開(kāi)始學(xué)習(xí)源代碼:
實(shí)現(xiàn)Expandable?And collapse 效果我們?nèi)匀皇鞘褂肂aseMultiItemQuickAdapter實(shí)現(xiàn)即可
然后我們需要先看兩個(gè)相關(guān)的類:IExpandable接口;AbstractExpandableItem: 對(duì)數(shù)據(jù)bean的再次封裝,某個(gè)bean如果有次級(jí)的list 可以實(shí)現(xiàn)該抽象類。
package com.chad.library.adapter.base.entity;
import java.util.List;
/**
* implement the interface if the item is expandable
* Created by luoxw on 2016/8/8.
*/
public interface IExpandable {
boolean isExpanded();
void setExpanded(boolean expanded);
List getSubItems();
/**
* Get the level of this item. The level start from 0.
* If you don't care about the level, just return a negative.
*/
int getLevel();
}
可以看到,IExpandable 里面定義了四個(gè)接口方法:
isExpanded判斷當(dāng)前的bean是否已展開(kāi)
setExoanded更新bean的當(dāng)前狀態(tài)
getSubItems返回下一級(jí)的數(shù)據(jù)集合
getLevel 返回當(dāng)前item屬于第幾個(gè)層級(jí), 第一級(jí)from 0
package com.chad.library.adapter.base.entity;
import java.util.ArrayList;
import java.util.List;
/**
*
A helper to implement expandable item.
*
if you don't want to extent a class, you can also implement the interface IExpandable
* Created by luoxw on 2016/8/9.
*/
public abstract class AbstractExpandableItem implements IExpandable {
protected boolean mExpandable = false;
protected List mSubItems;
@Override
public boolean isExpanded() {
return mExpandable;
}
@Override
public void setExpanded(boolean expanded) {
mExpandable = expanded;
}
@Override
public List getSubItems() {
return mSubItems;
}
public boolean hasSubItem() {
return mSubItems != null && mSubItems.size() > 0;
}
public void setSubItems(List list) {
mSubItems = list;
}
public T getSubItem(int position) {
if (hasSubItem() && position < mSubItems.size()) {
return mSubItems.get(position);
} else {
return null;
}
}
public int getSubItemPosition(T subItem) {
return mSubItems != null ? mSubItems.indexOf(subItem) : -1;
}
public void addSubItem(T subItem) {
if (mSubItems == null) {
mSubItems = new ArrayList<>();
}
mSubItems.add(subItem);
}
public void addSubItem(int position, T subItem) {
if (mSubItems != null && position >= 0 && position < mSubItems.size()) {
mSubItems.add(position, subItem);
} else {
addSubItem(subItem);
}
}
public boolean contains(T subItem) {
return mSubItems != null && mSubItems.contains(subItem);
}
public boolean removeSubItem(T subItem) {
return mSubItems != null && mSubItems.remove(subItem);
}
public boolean removeSubItem(int position) {
if (mSubItems != null && position >= 0 && position < mSubItems.size()) {
mSubItems.remove(position);
return true;
}
return false;
}
}
字段方法解析:
mExpandable 保存當(dāng)前的狀態(tài)值,默認(rèn)為false
mSubItems 存儲(chǔ)數(shù)據(jù)bean集合
里面還包裝了一些常用的方法,這里就不一一解析了。
接下來(lái)我們以一個(gè)使用demo的實(shí)現(xiàn)來(lái)進(jìn)行分析:
我們可以看群主demo中的ExpandableUseActivity :
private ArrayList generateData() {
int lv0Count = 9;
int lv1Count = 3;
int personCount = 5;
String[] nameList = {"Bob", "Andy", "Lily", "Brown", "Bruce"};
Random random = new Random();
ArrayList res = new ArrayList<>();
for (int i = 0; i < lv0Count; i++) {
Level0Item lv0 = new Level0Item("This is " + i + "th item in Level 0", "subtitle of " + i);
for (int j = 0; j < lv1Count; j++) {
Level1Item lv1 = new Level1Item("Level 1 item: " + j, "(no animation)");
for (int k = 0; k < personCount; k++) {
lv1.addSubItem(new Person(nameList[k], random.nextInt(40)));
}
lv0.addSubItem(lv1);
}
res.add(lv0);
}
return res;
}
這段代碼的作用是生成一個(gè)支持Expandable and collapse 的數(shù)據(jù)集合,創(chuàng)建一個(gè)0級(jí)的LevelOItem 然后將下一級(jí)的Level1Item添加到Level0Item中。
public class Level0Item extends AbstractExpandableItem implements MultiItemEntity {
public String title;
public String subTitle;
public Level0Item( String title, String subTitle) {
this.subTitle = subTitle;
this.title = title;
}
@Override
public int getItemType() {
return ExpandableItemAdapter.TYPE_LEVEL_0;
}
@Override
public int getLevel() {
return 0;
}
}
可以看到Level0Item繼承了AbstractExpandableItem 并實(shí)現(xiàn)MultiItemEntity接口。里面根據(jù)實(shí)際需求定義相應(yīng)的字段即可。
Level1Item 與Level0Item一樣,只是返回的Level =1:
public class Level1Item extends AbstractExpandableItem implements MultiItemEntity{
public String title;
public String subTitle;
public Level1Item(String title, String subTitle) {
this.subTitle = subTitle;
this.title = title;
}
@Override
public int getItemType() {
return ExpandableItemAdapter.TYPE_LEVEL_1;
}
@Override
public int getLevel() {
return 1;
}
}
當(dāng)如過(guò)某一級(jí)的item沒(méi)有下一級(jí)的list時(shí),就不需要在實(shí)現(xiàn)AbstractExpandableItem了
然后我們的切入點(diǎn)時(shí)adapter,因?yàn)槟J(rèn)是折疊狀態(tài),當(dāng)我們點(diǎn)擊具備展開(kāi)折疊能力的item時(shí)才會(huì)觸發(fā)該功能,所以邏輯的控制是在adapter中的。
package com.chad.baserecyclerviewadapterhelper.adapter;
import android.util.Log;
import android.view.View;
import android.view.ViewGroup;
import com.chad.baserecyclerviewadapterhelper.R;
import com.chad.baserecyclerviewadapterhelper.entity.Level0Item;
import com.chad.baserecyclerviewadapterhelper.entity.Level1Item;
import com.chad.baserecyclerviewadapterhelper.entity.Person;
import com.chad.library.adapter.base.BaseMultiItemQuickAdapter;
import com.chad.library.adapter.base.BaseViewHolder;
import com.chad.library.adapter.base.entity.MultiItemEntity;
import java.util.List;
/**
* Created by luoxw on 2016/8/9.
*/
public class ExpandableItemAdapter extends BaseMultiItemQuickAdapter {
private static final String TAG = ExpandableItemAdapter.class.getSimpleName();
public static final int TYPE_LEVEL_0 = 0;
public static final int TYPE_LEVEL_1 = 1;
public static final int TYPE_PERSON = 2;
/**
* Same as QuickAdapter#QuickAdapter(Context,int) but with
* some initialization data.
*
* @param data A new list is created out of this one to avoid mutable list
*/
public ExpandableItemAdapter(List data) {
super(data);
addItemType(TYPE_LEVEL_0, R.layout.item_expandable_lv0);
addItemType(TYPE_LEVEL_1, R.layout.item_expandable_lv1);
addItemType(TYPE_PERSON, R.layout.item_expandable_lv2);
}
@Override
protected void convert(final BaseViewHolder holder, final MultiItemEntity item) {
switch (holder.getItemViewType()) {
case TYPE_LEVEL_0:
switch (holder.getLayoutPosition() %
3) {
case 0:
holder.setImageResource(R.id.iv_head, R.mipmap.head_img0);
break;
case 1:
holder.setImageResource(R.id.iv_head, R.mipmap.head_img1);
break;
case 2:
holder.setImageResource(R.id.iv_head, R.mipmap.head_img2);
break;
}
final Level0Item lv0 = (Level0Item)item;
holder.setText(R.id.title, lv0.title)
.setText(R.id.sub_title, lv0.subTitle)
.setImageResource(R.id.iv, lv0.isExpanded() ? R.mipmap.arrow_b : R.mipmap.arrow_r);
holder.itemView.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
int pos = holder.getAdapterPosition();
Log.d(TAG, "Level 0 item pos: " + pos);
if (lv0.isExpanded()) {
collapse(pos);
} else {
//? ? ? ? ? ? ? ? ? ? ? ? ? ? if (pos % 3 == 0) {
//? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? expandAll(pos, false);
//? ? ? ? ? ? ? ? ? ? ? ? ? ? } else {
expand(pos);
//? ? ? ? ? ? ? ? ? ? ? ? ? ? }
}
}
});
break;
case TYPE_LEVEL_1:
final Level1Item lv1 = (Level1Item)item;
holder.setText(R.id.title, lv1.title)
.setText(R.id.sub_title, lv1.subTitle)
.setImageResource(R.id.iv, lv1.isExpanded() ? R.mipmap.arrow_b : R.mipmap.arrow_r);
holder.itemView.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
int pos = holder.getAdapterPosition();
Log.d(TAG, "Level 1 item pos: " + pos);
if (lv1.isExpanded()) {
collapse(pos, false);
} else {
expand(pos, false);
}
}
});
break;
case TYPE_PERSON:
final Person person = (Person)item;
holder.setText(R.id.tv, person.name + " parent pos: " + getParentPosition(person));
break;
}
}
}
可以看到里面我們先添加3個(gè)level的布局資源文件。重點(diǎn)在convert回調(diào)方法;
最外層進(jìn)行viewholder 的類型判斷進(jìn)行數(shù)據(jù)綁定
添加點(diǎn)擊事件的監(jiān)聽(tīng)
當(dāng)被點(diǎn)擊時(shí),判斷當(dāng)前的levelitem是不是展開(kāi)的或折疊的,然后根據(jù)你的需要調(diào)用collapse或者expand進(jìn)行折疊或展開(kāi)操作。
重點(diǎn)來(lái)的,最終實(shí)現(xiàn)展開(kāi)、折疊功能其實(shí)是依賴collapse和expand這些api;那我們來(lái)看下這些api到底內(nèi)部是怎么實(shí)現(xiàn)的,我們從expand開(kāi)始。代碼中expand(pos);傳了一個(gè)pos進(jìn)來(lái),而這個(gè)pos就是被點(diǎn)擊的item在adapter數(shù)據(jù)集合中的index。
/**
* Expand an expandable item
*
* @param position? ? position of the item
* @param animate? ? ? expand items with animation
* @param shouldNotify notify the RecyclerView to rebind items, false if you want to do it
*? ? ? ? ? ? ? ? ? ? yourself.
* @return the number of items that have been added.
*/
public int expand(@IntRange(from = 0) int position, boolean animate, boolean shouldNotify) {
position -= getHeaderLayoutCount();
IExpandable expandable = getExpandableItem(position);
if (expandable == null) {
return 0;
}
if (!hasSubItems(expandable)) {
expandable.setExpanded(false);
return 0;
}
int subItemCount = 0;
if (!expandable.isExpanded()) {
List list = expandable.getSubItems();
mData.addAll(position + 1, list);
subItemCount += recursiveExpand(position + 1, list);
expandable.setExpanded(true);
subItemCount += list.size();
}
int parentPos = position + getHeaderLayoutCount();
if (shouldNotify) {
if (animate) {
notifyItemChanged(parentPos);
notifyItemRangeInserted(parentPos + 1, subItemCount);
} else {
notifyDataSetChanged();
}
}
return subItemCount;
}
/**
* Expand an expandable item
*
* @param position position of the item, which includes the header layout count.
* @param animate? expand items with animation
* @return the number of items that have been added.
*/
public int expand(@IntRange(from = 0) int position, boolean animate) {
return expand(position, animate, true);
}
/**
* Expand an expandable item with animation.
*
* @param position position of the item, which includes the header layout count.
* @return the number of items that have been added.
*/
public int expand(@IntRange(from = 0) int position) {
return expand(position, true, true);
}
可以看到expand是一個(gè)方法多態(tài),提供了三種參數(shù)類型的調(diào)用。支持是否需要?jiǎng)赢?,是否更新?shù)據(jù)源。
排除headerview的干擾,獲得實(shí)際的位置position
position -= getHeaderLayoutCount();
判斷其是否支持展開(kāi)折疊,是否有下一級(jí)items需要展開(kāi),沒(méi)有就直接返回0
IExpandable expandable = getExpandableItem(position);
if (expandable == null) {
return 0;
}
if (!hasSubItems(expandable)) {
expandable.setExpanded(false);
return 0;
}
下面代碼作用:如果處于折疊狀態(tài)且需要展開(kāi),則執(zhí)行到下面代碼,通過(guò)getSubItems獲得要展開(kāi)的list,將其添加到mdata中,通過(guò)recursiveExpand獲得要展開(kāi)的items的數(shù)量
int subItemCount = 0;
if (!expandable.isExpanded()) {
List list = expandable.getSubItems();
mData.addAll(position + 1, list);
subItemCount += recursiveExpand(position + 1, list);
expandable.setExpanded(true);
subItemCount += list.size();
}
我們可以看到recursiveExpand的源碼如下:下面是一個(gè)遞歸調(diào)用,一直遍歷到最后一層不支持展開(kāi)折疊的item才會(huì)回溯回來(lái),遍歷過(guò)程中可以看到一個(gè)判斷,if(item.isExpanded) 就是如果下一級(jí)的items原來(lái)已經(jīng)是處于展開(kāi)狀態(tài)的,此時(shí)我們也需要展開(kāi)他。最終返回的是所需展開(kāi)的items的數(shù)量。
private int recursiveExpand(int position, @NonNull List list) {
int count = 0;
int pos = position + list.size() - 1;
for (int i = list.size() - 1; i >= 0; i--, pos--) {
if (list.get(i) instanceof IExpandable) {
IExpandable item = (IExpandable) list.get(i);
if (item.isExpanded() && hasSubItems(item)) {
List subList = item.getSubItems();
mData.addAll(pos + 1, subList);
int subItemCount = recursiveExpand(pos + 1, subList);
count += subItemCount;
}
}
}
return count;
}
獲得需要展開(kāi)的items的數(shù)量值,也將數(shù)據(jù)集合添加到了mData中,此時(shí)我們通知layoutManager刷新數(shù)據(jù)即可
int parentPos = position + getHeaderLayoutCount();
if (shouldNotify) {
if (animate) {
notifyItemChanged(parentPos);
notifyItemRangeInserted(parentPos + 1, subItemCount);
} else {
notifyDataSetChanged();
}
}
刷新的時(shí)候我們要先確定開(kāi)始刷新位置,所以需要加上headerview的數(shù)量
然后調(diào)用如上代碼即可。折疊是反向進(jìn)行的,根據(jù)這個(gè)思路看就可以了。
總結(jié):折疊->展開(kāi):mData添加需展開(kāi)的數(shù)據(jù)集,更新數(shù)據(jù)源;展開(kāi)->折疊:mData移除需折疊的數(shù)據(jù)集,更新數(shù)據(jù)源。
后面會(huì)繼續(xù)分析其他功能的實(shí)現(xiàn)源碼,歡迎一起學(xué)習(xí)!