1.MultiType簡單介紹
1.1.MultiType用於比較復雜的頁面。
如下圖,今日頭條用到了MultiType處理各種復雜的頁面。
這種還是比較簡單的類型。因為一個頁面也就這種類型。
下面看看這個頁面。
這個就比較復雜了,所以這時候MultiType的作用就體現出來了。
一個頁面用一個單獨的RecyclerView就可以實現。
再比如微博列表頁面:
有純文本的、代轉發原文的,帶圖片的、帶視頻的、帶文章的等等,甚至穿插一條可以橫向滑動的好友推薦條目。
不同的item類型眾多,而且隨着業務發展,還會更多。
如果我們使用傳統的開發方式,經常要做一些繁瑣的工作,代碼可能都堆積在一個Adapter中,我們需要復寫
RecyclerView.Adapter的getItemViewType方法,羅列一些type整型常量,並且ViewHolder轉型、綁定數據
也比較麻煩。
一旦產品需求有變,或者產品設計需要增加一種新的item類型,我們需要去代碼堆里找到我們原來的邏輯去修改,
或者找到正確的位置去增加代碼。非常繁瑣。
1.2.現在有了MultiType,簡單來說,MultiType就是一個多類型列表視圖的中間分發框架,它能幫助你快速並且清晰
地開發一些復雜的列表頁面。它本是為聊天頁面開發的,聊天頁面的消息類型也是有大量不同種類,並且新增頻繁
而MultiType能夠輕松勝任,代碼模塊化,隨時可擴展新的類型進入列表當中。它內建了類型-View的復用池系統,
支持RecyclerView,使用簡單靈活,令代碼清晰,適應需求變化。
1.3.MultiType也能輕松實現如下頁面。
在github中有相關頁面介紹。
2.基本使用方法
2.1.引入MultiType==>在build.gradle中加入:
dependencies { compile 'me.drakeet.multitype:multitype:3.3.0' }
注意:MultiType不支持低於23.0.0的RecyclerView,不過現在基本都沒有用那么低版本的RecyclerView了吧。
2.2.小貼士:
MultiType這個框架使用RecyclerView,但是不需要寫adapter,需要些ItemViewBinder,這是框架里面定義
的一個類。在多Type的情況下,每一種item對應一個數據模型(一個bean類)+一個ItemViewBinder。
2.3.新建一個數據模型bean
public class Title { public String title; public String url; }
2.4.新建一個類==>繼承ItemViewBinder的一個綁定類。
public class TitleViewBinder extends ItemViewBinder<Title, TitleViewBinder.TitleHolder> { @NonNull @Override protected TitleHolder onCreateViewHolder(@NonNull LayoutInflater inflater,
@NonNull ViewGroup parent) { View view = inflater.inflate(R.layout.layout_title, parent, false); return new TitleHolder(view); } @Override protected void onBindViewHolder(@NonNull TitleHolder holder, @NonNull Title item) { //填充數據渲染頁面,比如setText setImage等工作 } static class TitleHolder extends RecyclerView.ViewHolder { ImageView imageView; TextView textView; TitleHolder(View itemView) { super(itemView); imageView = (ImageView) itemView.findViewById(R.id.iv_title); textView = (TextView) itemView.findViewById(R.id.tv_title); } } }
這里R.layout.layout_title,就是定義布局資源,要加載這個布局。
這個布局中有R.id_iv_title+R.id_tv_title。
返回的是一個自定義的視圖持有者==>TitleHolder
這個視圖持有者是定義在這個視圖綁定類里面的類部類。
2.5.最后一步==>注冊綁定。
在Activity中將類和ItemViewBinder注冊綁定。
其余的工作就和普通的RecyclerView一樣。
因為MultiType使用了自己的adapter==>MultiTypeAdapter,在里面填充的數據列表應該是List<Object>
這里就根據自己的需求來設置這個Object類(就是自己定義Bean類)。
public class MainActivtiy extends AppCompatActivity { RecyclerView rv_design; List<Object> list; private MultiTypeAdapter adapter; @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_new_design); rv_design = (RecyclerView) findViewById(R.id.rv_design); rv_design.setLayoutManager(new LinearLayoutManager(this)); adapter = new MultiTypeAdapter(); adapter.register(Title.class, new TitleViewBinder()); rv_design.setAdapter(adapter); list = new ArrayList<>(); list.add(new Title());//模擬的初始化數據,偽代碼 adapter.setItems(list); adapter.notifyDataSetChanged(); }
活動的布局為==>R.layout.activity_new_design
活動里面的RecyclerView==>R.id.rv_design
RecyclerView設置布局管理器
新建一個MultiTypeAdapter()適配器
將MultiTypeAdapter注冊Bean+綁定類(Object+繼承ItemViewBinder類)
RecyclerView設置這個MultiTypeAdapter適配器。
新建一個ArrayList<>()
這個ArrayList用自定義的Bean類填充。
將MultiTypeAdapter適配器來設置數據,adapter.setItems(list)
最后調用MultiTypeAdapter的刷新方法。
3.以一個復雜頁面為例
3.1.例子來源於簡書:MultiType的基本使用和復雜頁面的寫法實例。
3.2.需要實現的頁面:
3.3.布局分析:
第一行是一個橫屏大圖。
第二行是5個分類的入口。
第三塊是2個入口。
.................
怎么寫這個布局呢?
第一行作為一個Item
第二行5個入口作為5個Item
第三行2個入口作為2個Item
如何將第一行的一個Item的排列轉接到第二行的5個item轉接到第三行的2個Item呢?
3.4.關鍵實現技術
這里使用了GridLayoutManager。
其實GridLayoutManager和LinearLayoutManager是差不多的,最大的區別就是其中的getSpanSize方法。
GridLayoutManager的構造方法是:
GridLayoutManager layoutManager = new GridLayoutManager(this, total);
其中total是一行的網格個數。
所以可以利用getSpanSize方法動態改變total的大小。如下面的代碼:
/**每一行占的個數不固定的例子 * 比如有一行1個,5個,2個 同時存在 * 全部相乘是10 * 第一種 SpanSize=10/1 * 第二種 SpanSize=10/5 * 第三種 SpanSize=10/2 * 為什么呢?因為要保證10/1 10/5 10/2 都是整數。 */ GridLayoutManager layoutManager = new GridLayoutManager(this, 10);//10=1*5*2 layoutManager.setSpanSizeLookup(new GridLayoutManager.SpanSizeLookup() { @Override public int getSpanSize(int position) { if (list.get(position) instanceof Title) { return 10/1;//一行占1個 } if (list.get(position) instanceof Ad) { return 10/5;//一行占5個 } if (list.get(position) instanceof EmptyValue) { return 10/2;//一行占2個 } return 10;//默認一行占1個 } });
如果拿到了list數據,然后判斷當前的list中的當前項的類型是什么(這個類型就是我們之前定義的Bean類)。
所以這里可以將所有的Bean冗雜在一起,然后這里判斷是哪個類型,再選擇不同入口就行了。
3.5.如果要求一對多,就是如果就是第一行有時候要標題或者分割線怎么辦?
這里假設我們的需求不確定,就是第一行有時候要標題,有時候要分割線。
處理方式==>參考這篇文章:一個類型對應多個ItemViewBinder。
在注冊Bean類的時候就只用一個空類,我們暫且叫做EmptyValue。它可以對應我們需要的多種類型。
從某個角度,有點像泛型了。
寫法如下:
adapter.register(EmptyValue.class) .to(new GoodTitleViewBinder(), new LineViewBinder()) .withClassLinker(new ClassLinker<EmptyValue>() { @NonNull @Override public Class<? extends ItemViewBinder<EmptyValue, ?>>
index(@NonNull EmptyValue emptyValue) { if (emptyValue.type == EmptyValue.TYPE_GOODTITLE) { return GoodTitleViewBinder.class; } if (emptyValue.type == EmptyValue.TYPE_LINE) { return LineViewBinder.class; } return LineViewBinder.class; } });
這里使用emptyVaule.type來應對不同的類型。
我們在一對多的時候需要區分這個Item是標題還是分割線。
/** * EmptyValue * 占一整行而無數據的類型,適用於分割線、寫死的標題等等。 */ public class EmptyValue { public static final int TYPE_GOODTITLE = 1; public static final int TYPE_LINE = 2; public int type; public EmptyValue(int type) { this.type = type; } }
3.6.效果預覽
3.7.分析代碼
加入的測試數據:
關鍵主活動代碼:

public class NewDesignActivity extends AppCompatActivity { RecyclerView rv_design; List<Object> list; private MultiTypeAdapter adapter; @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_new_design); rv_design = (RecyclerView) findViewById(R.id.rv_design); final int num_banner = 1; final int num_title = 5; final int num_ad = 2; final int num_good_title = 1; final int num_good = 4; final int num_hot = 1; final int num_main_game = 1; final int num_new_title = 1; final int num_new_game = 2; int[] types = new int[]{num_banner, num_title, num_ad, num_good_title, num_good, num_hot}; final int total = MockData.getTotal(types); /**每一行占的個數不固定的例子 * 比如有一行1個,5個,2個 同時存在 * 全部相乘是10 * 第一種 SpanSize=10/1 * 第二種 SpanSize=10/5 * 第三種 SpanSize=10/2 * 為什么呢?因為要保證10/1 10/5 10/2 都是整數。 */ GridLayoutManager layoutManager = new GridLayoutManager(this, total); layoutManager.setSpanSizeLookup(new GridLayoutManager.SpanSizeLookup() { @Override public int getSpanSize(int position) { if (list.get(position) instanceof Title) { return total / num_title; } if (list.get(position) instanceof Ad) { return total / num_ad; } if (list.get(position) instanceof EmptyValue) { return total / num_good_title; } if (list.get(position) instanceof Good) { return total / num_good; } if (list.get(position) instanceof HotList) { return total / num_hot; } if (list.get(position) instanceof MainGame) { return total / num_main_game; } return total; } }); rv_design.setLayoutManager(layoutManager); adapter = new MultiTypeAdapter(); adapter.register(Banner.class, new BannerViewBinder()); adapter.register(Title.class, new TitleViewBinder()); adapter.register(Ad.class, new ADViewBinder()); adapter.register(EmptyValue.class) .to(new GoodTitleViewBinder(), new LineViewBinder()) .withClassLinker(new ClassLinker<EmptyValue>() { @NonNull @Override public Class<? extends ItemViewBinder<EmptyValue, ?>> index(@NonNull EmptyValue emptyValue) { if (emptyValue.type == EmptyValue.TYPE_GOODTITLE) { return GoodTitleViewBinder.class; } if (emptyValue.type == EmptyValue.TYPE_LINE) { return LineViewBinder.class; } return LineViewBinder.class; } }); adapter.register(Good.class, new GoodViewBinder()); adapter.register(HotList.class, new HotViewBinder()); adapter.register(MainGame.class, new MainGameViewBinder()); rv_design.setAdapter(adapter); list = new ArrayList<>(); MockData.init(list); adapter.setItems(list); adapter.notifyDataSetChanged(); } }
圖解:
4.今日頭條真實案例
4.1.今日頭條的段子頁面布局為:R.item_joke_content.

<?xml version="1.0" encoding="utf-8"?> <android.support.v7.widget.CardView xmlns:android="http://schemas.android.com/apk/res/android" xmlns:app="http://schemas.android.com/apk/res-auto" xmlns:tools="http://schemas.android.com/tools" android:layout_width="match_parent" android:layout_height="wrap_content" android:layout_marginBottom="4dp" android:layout_marginTop="4dp" android:background="@color/viewBackground" app:cardElevation="1dp"> <RelativeLayout android:layout_width="match_parent" android:layout_height="match_parent" android:background="?attr/selectableItemBackground" android:foreground="?attr/selectableItemBackground" android:orientation="vertical" android:padding="16dp"> <LinearLayout android:id="@+id/header" android:layout_width="match_parent" android:layout_height="wrap_content" android:orientation="horizontal"> <com.jasonjan.headnews.widget.CircleImageView android:id="@+id/iv_avatar" android:layout_width="22dp" android:layout_height="22dp" android:layout_gravity="center"/> <TextView android:id="@+id/tv_username" android:layout_width="wrap_content" android:layout_height="wrap_content" android:layout_gravity="center" android:layout_marginLeft="8dp" android:layout_marginStart="8dp" android:ellipsize="end" android:maxLength="30" android:maxLines="1" android:textAppearance="@style/TextAppearance.AppCompat.Caption" tools:text="小恢恢的帽子"/> <RelativeLayout android:layout_width="match_parent" android:layout_height="wrap_content"> <ImageView android:id="@+id/iv_dots" android:layout_width="22dp" android:layout_height="22dp" android:layout_alignParentEnd="true" android:layout_alignParentRight="true" android:layout_centerVertical="true" android:padding="4dp" android:scaleType="center" app:srcCompat="@drawable/ic_dots_horizontal_grey500_24dp" tools:ignore="ContentDescription"/> </RelativeLayout> </LinearLayout> <LinearLayout android:layout_width="match_parent" android:layout_height="wrap_content" android:layout_below="@+id/header" android:layout_marginTop="4dp" android:orientation="vertical"> <TextView android:id="@+id/tv_text" android:layout_width="wrap_content" android:layout_height="wrap_content" tools:text="昨天和閨蜜出去逛街,閨密問她老公要錢,她老公坐在沙發上,翹着二郎腿抽着煙問:“20行嗎?”閨密想了想,溫柔的點點頭,我正驚訝她老公能把她管制的服服貼貼,只見她老公從錢包里掏出20,然后把錢包遞給了媳婦……"/> <LinearLayout android:layout_width="match_parent" android:layout_height="20dp" android:layout_gravity="bottom" android:layout_marginTop="6dp" android:gravity="bottom" android:orientation="horizontal"> <TextView android:id="@+id/tv_digg_count" android:layout_width="wrap_content" android:layout_height="wrap_content" tools:text="53"/> <ImageView android:layout_width="16dp" android:layout_height="16dp" android:layout_marginLeft="4dp" android:layout_marginStart="4dp" app:srcCompat="@drawable/ic_like_gray_24dp" tools:ignore="ContentDescription"/> <TextView android:id="@+id/tv_bury_count" android:layout_width="wrap_content" android:layout_height="wrap_content" android:layout_marginLeft="16dp" android:layout_marginStart="16dp" tools:text="11"/> <ImageView android:layout_width="16dp" android:layout_height="16dp" android:layout_marginLeft="4dp" android:layout_marginStart="4dp" app:srcCompat="@drawable/ic_dislike_gray_24dp" tools:ignore="ContentDescription"/> <TextView android:id="@+id/tv_comment_count" android:layout_width="0dp" android:layout_height="wrap_content" android:layout_weight="1" android:gravity="end" tools:text="48評論"/> </LinearLayout> </LinearLayout> </RelativeLayout> </android.support.v7.widget.CardView>
預覽圖+實際預覽圖
4.2.段子視圖綁定==>JokeContentViewBinder
邏輯處理都在這里面進行。
包括什么點擊事件啊,設置真實數據的顯示情況啊。
這里就有可能用到很多圖片加載的第三方庫等,圓形圖片等,不過都是一些顯示問題了。
4.3.加載完畢所有視圖綁定==>LoadingEndViewBinder
這里的布局很簡單。也可以很復雜,有些做的花里胡哨的,都是這里做一些手腳的。
R.item_loading_end.xml==>
<?xml version="1.0" encoding="utf-8"?> <LinearLayout xmlns:android="http://schemas.android.com/apk/res/android" android:layout_width="match_parent" android:layout_height="48dp" android:layout_marginBottom="1dp" android:gravity="center_horizontal" android:orientation="horizontal"> <TextView android:layout_width="wrap_content" android:layout_height="match_parent" android:gravity="center_vertical" android:text="@string/no_more_content" android:textSize="16sp"/> </LinearLayout>
實圖預覽:
4.4.正在加載視圖綁定==>LoadingViewBinder
這里正在加載的話,應該有一個進度條加載框(andorid自帶的是PregressBar)
這里的布局文件為:R.layout.item_loading==>
<?xml version="1.0" encoding="utf-8"?> <LinearLayout xmlns:android="http://schemas.android.com/apk/res/android" android:layout_width="match_parent" android:layout_height="48dp" android:layout_marginBottom="1dp" android:gravity="center_horizontal" android:orientation="horizontal"> <ProgressBar android:id="@+id/progress_footer" android:layout_width="wrap_content" android:layout_height="wrap_content" android:layout_gravity="center_horizontal" android:visibility="visible"/> <TextView android:layout_width="wrap_content" android:layout_height="match_parent" android:layout_marginLeft="16dp" android:layout_marginStart="8dp" android:gravity="center_vertical" android:text="@string/loading"/> </LinearLayout>
預覽圖片為:
4.5.在段子頁面==>JokeContentView執行如下代碼:
@Override protected void initView(View view) { super.initView(view); adapter = new MultiTypeAdapter(oldItems); Register.registerJokeContentItem(adapter); recyclerView.setAdapter(adapter); recyclerView.addOnScrollListener(new OnLoadMoreListener() { @Override public void onLoadMore() { if(NetWorkUtil.isNetworkConnected(InitApp.AppContext)){ if (canLoadMore) { canLoadMore = false; presenter.doLoadMoreData(); } }else{ presenter.doShowNetError(); } } }); }
這里監聽了recyclerView是否滑動最低端了,用處理器的doLoadMoreData方法來加載下一頁。
這里注冊的類型,自己封裝了一個類Register。
public class Register { public static void registerJokeContentItem(@NonNull MultiTypeAdapter adapter) { adapter.register(JokeContentBean.DataBean.GroupBean.class, new JokeContentViewBinder()); adapter.register(LoadingBean.class, new LoadingViewBinder()); adapter.register(LoadingEndBean.class, new LoadingEndViewBinder()); } }
如果當前頁面可見的話,將會執行fetchData方法,這個是懶加載碎片中定義的一個抽象方法。
執行了onLoadData()方法來
@Override public void onLoadData() { onShowLoading(); presenter.doLoadData(); }
先展示加載框。
然后用處理器的一個doLoadData()方法來加載數據。
所以這個處理器中一定有一個適配器,而且就是這里面定義的適配器。
所以進入JokeContentPresenter處理器中,發現這里面並沒有前面定義adapter啊!
但是這里數據在處理器中加載完畢,相當於執行了一個回調,到JokeContentView中的onSetAdapter中添加。
@Override public void doLoadData(){ if(NetWorkUtil.isNetworkConnected(InitApp.AppContext)) { //這里需要更改!!! Map<String, String> map = ToutiaoUtil.getAsCp(); RetrofitFactory.getRetrofit().create(IJokeApi.class).getJokeContent(time, map.get(Constant.AS), map.get(Constant.CP)) .subscribeOn(Schedulers.io()) .map(new Function<JokeContentBean, List<JokeContentBean.DataBean.GroupBean>>() { @Override public List<JokeContentBean.DataBean.GroupBean> apply(@NonNull JokeContentBean jokeContentBean) throws Exception { List<JokeContentBean.DataBean> data = jokeContentBean.getData(); for (JokeContentBean.DataBean dataBean : data) { groupList.add(dataBean.getGroup()); } time = jokeContentBean.getNext().getMax_behot_tim() + ""; return groupList; } }) .compose(view.<List<JokeContentBean.DataBean.GroupBean>>bindToLife()) .observeOn(AndroidSchedulers.mainThread()) .subscribe(new Consumer<List<JokeContentBean.DataBean.GroupBean>>() { @Override public void accept(@NonNull List<JokeContentBean.DataBean.GroupBean> groupBeen) throws Exception { if (groupBeen.size() > 0) { doSetAdapter(); } else { doShowNoMore(); } } }, ErrorAction.error()); }else{ doShowNetError(); } }
這是主要加載數據的方法。將所有數據放到一個groupList中。
然后執行doSetAdapter回調方法。
@Override public void doSetAdapter(){ view.onSetAdapter(groupList); view.onHideLoading(); }
這里調用了一個接口的方法,所以必然要在JokeContentView中實現這個接口,才有這個回調。
@Override public void onSetAdapter(List<?> list) { Items newItems = new Items(list); newItems.add(new LoadingBean()); DiffCallback.notifyDataSetChanged(oldItems, newItems, DiffCallback.JOKE, adapter); oldItems.clear(); oldItems.addAll(newItems); canLoadMore = true; }
這里先將 list保存到一個newItems,就是從API請求到的數據。
這里將新增的Items動態添加在oldItems中,執行了一個DiffCallback自定義類來添加。
最后將oldItems重寫。
設置canLoadMore為true。
這個oldItems定義在BaseListFragment中。
protected Items oldItems = new Items();
@Override public void onShowNoMore() { getActivity().runOnUiThread(new Runnable() { @Override public void run() { if (oldItems.size() > 0) { Items newItems = new Items(oldItems); newItems.remove(newItems.size() - 1); newItems.add(new LoadingEndBean()); adapter.setItems(newItems); adapter.notifyDataSetChanged(); } else if (oldItems.size() == 0) { oldItems.add(new LoadingEndBean()); adapter.setItems(oldItems); adapter.notifyDataSetChanged(); } canLoadMore = false; } }); }
如果沒有更多了,要先判斷一下。
如果之前沒有數據,直接在尾巴加一個LoadingEndBean。
如果之前有數據,先移除最后一個數據,將一個LoadingEndBean加到尾巴。
最后將canLoadMore為false。
其實這里是處理沒有更多的情況。
5.自定義DiffUtil簡單用法
5.1..如何利用DiffCallback將adapter填充了Items數據的?
參考文章:Android詳解7.0帶來的新工具類DiffUtil。
用處:在RecyclerView刷新時,不再無腦adapter.notifyDataSetChanged()。
RecyclerVIew刷新不會觸發RecyclerView的動畫(刪除、新增、位移、change動畫)
性能較低,畢竟是無腦的刷新了一遍整個RecyclerView,極端情況下,新老數據集一樣,效率最低。
它會自動計算新老數據集的差異,並根據差異情況,自動調用以下四個方法。
adapter.notifyItemRangeInserted(position, count);
adapter.notifyItemRangeRemoved(position, count);
adapter.notifyItemMoved(fromPosition, toPosition);
adapter.notifyItemRangeChanged(position, count, payload);
實現增量更新效果。
5.2.僅僅需要額外編寫一個Callback回調類。
它的意義是:定義了一些用來比較新老Item是否相等的契約(Contract)、規則(Rule)的類。
DiffUtil.Callback抽象類如下:
public abstract static class Callback { public abstract int getOldListSize();//老數據集size public abstract int getNewListSize();//新數據集size //新老數據集在同一個postion的Item是否是一個對象?(可能內容不同,如果這里返回true,會調用下面的方法) public abstract boolean areItemsTheSame(int oldItemPosition, int newItemPosition); //這個方法僅僅是上面方法返回ture才會調用,我的理解是只有notifyItemRangeChanged()才會調用,判斷item的內容是否有變化 public abstract boolean areContentsTheSame(int oldItemPosition, int newItemPosition); //該方法在DiffUtil高級用法中用到 ,暫且不提 @Nullable public Object getChangePayload(int oldItemPosition, int newItemPosition) { return null; } }
5.3.以某個項目的DiffCallback為例。

public class DiffCallback extends DiffUtil.Callback { public static final int JOKE = 1; private List oldList, newList; private int type; public DiffCallback(List oldList, List newList, int type) { this.oldList = oldList; this.newList = newList; this.type = type; } public static void notifyDataSetChanged(List oldList, List newList, int type, RecyclerView.Adapter adapter) { DiffCallback diffCallback = new DiffCallback(oldList, newList, type); DiffUtil.DiffResult result = DiffUtil.calculateDiff(diffCallback, true); result.dispatchUpdatesTo(adapter); } @Override public int getOldListSize() { return oldList != null ? oldList.size() : 0; } @Override public int getNewListSize() { return newList != null ? newList.size() : 0; } @Override public boolean areItemsTheSame(int oldItemPosition,int newItemPosition){ try{ switch(type){ case JOKE: return ((JokeContentBean.DataBean.GroupBean) oldList.get(oldItemPosition)).getContent().equals( ((JokeContentBean.DataBean.GroupBean) newList.get(newItemPosition)).getContent()); } }catch(Exception e){ } return false; } @Override public boolean areContentsTheSame(int oldItemPosition, int newItemPosition) { try { switch (type) { case JOKE: return ((JokeContentBean.DataBean.GroupBean) oldList.get(oldItemPosition)).getShare_url().equals( ((JokeContentBean.DataBean.GroupBean) newList.get(newItemPosition)).getShare_url()); } } catch (Exception e) { } return false; } }
具體分析:
構造函數不用多說,一個老的List,一個新的List,一個類型。
public DiffCallback(List oldList, List newList, int type) { this.oldList = oldList; this.newList = newList; this.type = type; }
為什么要傳入一個類型呢?==>因為這里可能數據很多,類型也很多,所以先設置type鋪墊一下。
然后是一個靜態函數,供外部調用。
public static void notifyDataSetChanged(List oldList, List newList, int type,
RecyclerView.Adapter adapter) { DiffCallback diffCallback = new DiffCallback(oldList, newList, type); DiffUtil.DiffResult result = DiffUtil.calculateDiff(diffCallback, true); result.dispatchUpdatesTo(adapter); }
耦合度非常低,外部只需要傳入兩個List,一個自己自願加的type,一個關鍵的adapter。
所以外部調用這個方法后,會執行這個抽象函數DiffCallback(oldList,newList,type)。
DiffUtil.DiffResult result=DiffUtil.calculateDiff(diffCallback,true)怎么理解呢?==>
在將newDatas 設置給Adapter之前,先調用DiffUtil.calculateDiff()方法,
計算出新老數據集轉化的最小更新集,就是DiffUtil.DiffResult對象。
DiffUtil.calculateDiff()方法定義如下:
第一個參數是DiffUtil.Callback對象,
第二個參數代表是否檢測Item的移動,改為false算法效率更高,按需設置,我們這里是true。
result.dispatchUpdatesTo(adapter)怎么理解呢?
利用DiffUtil.DiffResult對象的dispatchUpdatesTo()方法,傳入RecyclerView的Adapter,
替代普通青年才用的mAdapter.notifyDataSetChanged()方法。
查看源碼可知,該方法內部,就是根據情況調用了adapter的下面的四大定向刷新方法。
public void dispatchUpdatesTo(final RecyclerView.Adapter adapter) { dispatchUpdatesTo(new ListUpdateCallback() { @Override public void onInserted(int position, int count) { adapter.notifyItemRangeInserted(position, count); } @Override public void onRemoved(int position, int count) { adapter.notifyItemRangeRemoved(position, count); } @Override public void onMoved(int fromPosition, int toPosition) { adapter.notifyItemMoved(fromPosition, toPosition); } @Override public void onChanged(int position, int count, Object payload) { adapter.notifyItemRangeChanged(position, count, payload); } }); }
關於重寫的areItemsTheSame(int oldItemPosition,int newItemPosition)方法的理解。
/** * 被DiffUtil調用,用來判斷 兩個對象是否是相同的Item。 * 例如,如果你的Item有唯一的id字段,這個方法就 判斷id是否相等。 * 本例判斷name字段是否一致,不一定全部是這樣!!!! */ @Override public boolean areItemsTheSame(int oldItemPosition, int newItemPosition) { return mOldDatas.get(oldItemPosition).getName().equals
(mNewDatas.get(newItemPosition).getName()); }
關於重寫的areContentsTheSame(int oldItemPositon,int newItemPosition)方法的理解。
/** * 被DiffUtil調用,用來檢查 兩個item是否含有相同的數據 * DiffUtil用返回的信息(true false)來檢測當前item的內容是否發生了變化 * DiffUtil 用這個方法替代equals方法去檢查是否相等。 * 所以你可以根據你的UI去改變它的返回值 * 例如,如果你用RecyclerView.Adapter 配合DiffUtil使用,你需要返回Item的視覺表現是否相同。 * 這個方法僅僅在areItemsTheSame()返回true時,才調用。 */ @Override public boolean areContentsTheSame(int oldItemPosition, int newItemPosition) { TestBean beanOld = mOldDatas.get(oldItemPosition); TestBean beanNew = mNewDatas.get(newItemPosition); if (!beanOld.getDesc().equals(beanNew.getDesc())) { return false;//如果有內容不同,就返回false } if (beanOld.getPic() != beanNew.getPic()) { return false;//如果有內容不同,就返回false } return true; //默認兩個data內容是相同的 }
小貼士:
我一開始想找adapter在哪里設置數據的,因為DiffCallback只是比較差異,並沒有自己將數據加載進去。
所以我找了半天都不知道這個方法在哪里。
后來,我發現,根本不用setDatas。
因為我用了MultiType第三方庫,所以它自己有一個方法,率先將數據綁定到適配器了。
adapter = new MultiTypeAdapter(oldItems);
就是這個方法,oldItems變化的時候,adapter也隨之添加了這個數據。