Android7.0 工具類:DiffUtil詳解
一 概述
DiffUtil是support-v7:24.2.0中的新工具類,它用來比較兩個(gè)數(shù)據(jù)集,尋找出舊數(shù)據(jù)集-》新數(shù)據(jù)集的最小變化量。
說到數(shù)據(jù)集,相信大家知道它是和誰相關(guān)的了,就是我的最愛,RecyclerView。
就我使用的這幾天來看,它最大的用處就是在RecyclerView刷新時(shí),不再無腦mAdapter.notifyDataSetChanged()。
以前無腦mAdapter.notifyDataSetChanged()有兩個(gè)缺點(diǎn):
1.不會(huì)觸發(fā)RecyclerView的動(dòng)畫(刪除、新增、位移、change動(dòng)畫)
2.性能較低,畢竟是無腦的刷新了一遍整個(gè)RecyclerView , 極端情況下:新老數(shù)據(jù)集一模一樣,效率是最低的。
使用DiffUtil后,改為如下代碼:
DiffUtil.DiffResult diffResult = DiffUtil.calculateDiff(new DiffCallBack(mDatas, newDatas), true);
diffResult.dispatchUpdatesTo(mAdapter);
它會(huì)自動(dòng)計(jì)算新老數(shù)據(jù)集的差異,并根據(jù)差異情況,自動(dòng)調(diào)用以下四個(gè)方法
adapter.notifyItemRangeInserted(position, count);
adapter.notifyItemRangeRemoved(position, count);
adapter.notifyItemMoved(fromPosition, toPosition);
adapter.notifyItemRangeChanged(position, count, payload);
顯然,這個(gè)四個(gè)方法在執(zhí)行時(shí)都是伴有RecyclerView的動(dòng)畫的,且都是定向刷新方法,刷新效率蹭蹭的上升了。
老規(guī)矩,先上圖,
圖一是無腦mAdapter.notifyDataSetChanged()的效果圖,可以看到刷新交互很生硬,Item突然的出現(xiàn)在某個(gè)位置:
圖二是使用DiffUtils的效果圖,最明顯的是有插入、移動(dòng)Item的動(dòng)畫:
轉(zhuǎn)成GIF有些渣,下載文末Demo運(yùn)行效果更佳哦。
本文將包含且不僅包含以下內(nèi)容:
1 先介紹DiffUtil的簡(jiǎn)單用法,實(shí)現(xiàn)刷新時(shí)的“增量更新”效果。(“增量更新”是我自己的叫法)
2 DiffUtil的高級(jí)用法,在某項(xiàng)Item只有內(nèi)容(data)變化,位置(position)未變化時(shí),完成部分更新(官方稱之為Partial bind,部分綁定)。
3 了解到 RecyclerView.Adapter還有public void onBindViewHolder(VH holder, int position, List<Object> payloads)方法,并掌握它。
4 在子線程中計(jì)算DiffResult,在主線程中刷新RecyclerView。
5 少部分人不喜歡的notifyItemChanged()導(dǎo)致Item白光一閃的動(dòng)畫 如何去除。
6 DiffUtil部分類、方法 官方注釋的漢化
二 DiffUtil的簡(jiǎn)單用法
前文也提到,DiffUtil是幫助我們?cè)谒⑿翿ecyclerView時(shí),計(jì)算新老數(shù)據(jù)集的差異,并自動(dòng)調(diào)用RecyclerView.Adapter的刷新方法,以完成高效刷新并伴有Item動(dòng)畫的效果。
那么我們?cè)趯W(xué)習(xí)它之前要先做一些準(zhǔn)備工作,先寫一個(gè)普通青年版,無腦notifyDataSetChanged()刷新的Demo。
1 一個(gè)普通的JavaBean,但是實(shí)現(xiàn)了clone方法,僅用于寫Demo模擬刷新用,實(shí)際項(xiàng)目不需要,因?yàn)樗⑿聲r(shí),數(shù)據(jù)都是從網(wǎng)絡(luò)拉取的。:
class TestBean implements Cloneable { private String name; private String desc; ....//get set方法省略 //僅寫DEMO 用 實(shí)現(xiàn)克隆方法 @Override public TestBean clone() throws CloneNotSupportedException { TestBean bean = null; try { bean = (TestBean) super.clone(); } catch (CloneNotSupportedException e) { e.printStackTrace(); } return bean; }
2 實(shí)現(xiàn)一個(gè)普普通通的RecyclerView.Adapter。
public class DiffAdapter extends RecyclerView.Adapter<DiffAdapter.DiffVH> { private final static String TAG = "zxt"; private List<TestBean> mDatas; private Context mContext; private LayoutInflater mInflater; public DiffAdapter(Context mContext, List<TestBean> mDatas) { this.mContext = mContext; this.mDatas = mDatas; mInflater = LayoutInflater.from(mContext); } public void setDatas(List<TestBean> mDatas) { this.mDatas = mDatas; } @Override public DiffVH onCreateViewHolder(ViewGroup parent, int viewType) { return new DiffVH(mInflater.inflate(R.layout.item_diff, parent, false)); } @Override public void onBindViewHolder(final DiffVH holder, final int position) { TestBean bean = mDatas.get(position); holder.tv1.setText(bean.getName()); holder.tv2.setText(bean.getDesc()); holder.iv.setImageResource(bean.getPic()); } @Override public int getItemCount() { return mDatas != null ? mDatas.size() : 0; } class DiffVH extends RecyclerView.ViewHolder { TextView tv1, tv2; ImageView iv; public DiffVH(View itemView) { super(itemView); tv1 = (TextView) itemView.findViewById(R.id.tv1); tv2 = (TextView) itemView.findViewById(R.id.tv2); iv = (ImageView) itemView.findViewById(R.id.iv); } } }
3 Activity代碼:
public class MainActivity extends AppCompatActivity { private List<TestBean> mDatas; private RecyclerView mRv; private DiffAdapter mAdapter; @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_main); initData(); mRv = (RecyclerView) findViewById(R.id.rv); mRv.setLayoutManager(new LinearLayoutManager(this)); mAdapter = new DiffAdapter(this, mDatas); mRv.setAdapter(mAdapter); } private void initData() { mDatas = new ArrayList<>(); mDatas.add(new TestBean("張旭童1", "Android", R.drawable.pic1)); mDatas.add(new TestBean("張旭童2", "Java", R.drawable.pic2)); mDatas.add(new TestBean("張旭童3", "背鍋", R.drawable.pic3)); mDatas.add(new TestBean("張旭童4", "手撕產(chǎn)品", R.drawable.pic4)); mDatas.add(new TestBean("張旭童5", "手撕測(cè)試", R.drawable.pic5)); } /** * 模擬刷新操作 * * @param view */ public void onRefresh(View view) { try { List<TestBean> newDatas = new ArrayList<>(); for (TestBean bean : mDatas) { newDatas.add(bean.clone());//clone一遍舊數(shù)據(jù) ,模擬刷新操作 } newDatas.add(new TestBean("趙子龍", "帥", R.drawable.pic6));//模擬新增數(shù)據(jù) newDatas.get(0).setDesc("Android+"); newDatas.get(0).setPic(R.drawable.pic7);//模擬修改數(shù)據(jù) TestBean testBean = newDatas.get(1);//模擬數(shù)據(jù)位移 newDatas.remove(testBean); newDatas.add(testBean); //別忘了將新數(shù)據(jù)給Adapter mDatas = newDatas; mAdapter.setDatas(mDatas); mAdapter.notifyDataSetChanged();//以前我們大多數(shù)情況下只能這樣 } catch (CloneNotSupportedException e) { e.printStackTrace(); } } }
很簡(jiǎn)單,只不過在構(gòu)建新數(shù)據(jù)源newDatas時(shí),是遍歷老數(shù)據(jù)源mDatas,調(diào)用每個(gè)data的clone()方法,確保新老數(shù)據(jù)源雖然數(shù)據(jù)一致,但是內(nèi)存地址(指針不一致),這樣在后面修改newDatas里的值時(shí),不會(huì)牽連mDatas里的值被一起改了。
4 activity_main.xml 刪掉了一些寬高代碼,就是一個(gè)RecyclerView和一個(gè)Button用于模擬刷新。:
<?xml version="1.0" encoding="utf-8"?> <RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android" > <android.support.v7.widget.RecyclerView android:id="@+id/rv" /> <Button android:id="@+id/btnRefresh" android:layout_alignParentRight="true" android:onClick="onRefresh" android:text="模擬刷新" /> </RelativeLayout>
以上是一個(gè)普通青年很容易寫出的,無腦notifyDataSetChanged()的demo,運(yùn)行效果如第一節(jié)圖一。
但是我們都要爭(zhēng)做文藝青年,so
下面開始進(jìn)入正題,簡(jiǎn)單使用DiffUtil,我們需要且僅需要額外編寫一個(gè)類。
想成為文藝青年,我們需要實(shí)現(xiàn)一個(gè)繼承自DiffUtil.Callback的類,實(shí)現(xiàn)它的四個(gè)abstract方法。
雖然這個(gè)類叫Callback,但是把它理解成:定義了一些用來比較新老Item是否相等的契約(Contract)、規(guī)則(Rule)的類, 更合適。
DiffUtil.Callback抽象類如下:
public abstract static class Callback { public abstract int getOldListSize();//老數(shù)據(jù)集size public abstract int getNewListSize();//新數(shù)據(jù)集size public abstract boolean areItemsTheSame(int oldItemPosition, int newItemPosition);//新老數(shù)據(jù)集在同一個(gè)postion的Item是否是一個(gè)對(duì)象?(可能內(nèi)容不同,如果這里返回true,會(huì)調(diào)用下面的方法) public abstract boolean areContentsTheSame(int oldItemPosition, int newItemPosition);//這個(gè)方法僅僅是上面方法返回ture才會(huì)調(diào)用,我的理解是只有notifyItemRangeChanged()才會(huì)調(diào)用,判斷item的內(nèi)容是否有變化 //該方法在DiffUtil高級(jí)用法中用到 ,暫且不提 @Nullable public Object getChangePayload(int oldItemPosition, int newItemPosition) { return null; } }
本Demo如下實(shí)現(xiàn)DiffUtil.Callback,核心方法配有中英雙語注釋(說人話就是,翻譯了官方的英文注釋,方便大家更好理解)。
/** * 介紹:核心類 用來判斷 新舊Item是否相等 * 作者:zhangxutong * 郵箱:zhangxutong@imcoming.com * 時(shí)間: 2016/9/12. */ public class DiffCallBack extends DiffUtil.Callback { private List<TestBean> mOldDatas, mNewDatas;//看名字 public DiffCallBack(List<TestBean> mOldDatas, List<TestBean> mNewDatas) { this.mOldDatas = mOldDatas; this.mNewDatas = mNewDatas; } //老數(shù)據(jù)集size @Override public int getOldListSize() { return mOldDatas != null ? mOldDatas.size() : 0; } //新數(shù)據(jù)集size @Override public int getNewListSize() { return mNewDatas != null ? mNewDatas.size() : 0; } /** * Called by the DiffUtil to decide whether two object represent the same Item. * 被DiffUtil調(diào)用,用來判斷 兩個(gè)對(duì)象是否是相同的Item。 * For example, if your items have unique ids, this method should check their id equality. * 例如,如果你的Item有唯一的id字段,這個(gè)方法就 判斷id是否相等。 * 本例判斷name字段是否一致 * * @param oldItemPosition The position of the item in the old list * @param newItemPosition The position of the item in the new list * @return True if the two items represent the same object or false if they are different. */ @Override public boolean areItemsTheSame(int oldItemPosition, int newItemPosition) { return mOldDatas.get(oldItemPosition).getName().equals(mNewDatas.get(newItemPosition).getName()); } /** * Called by the DiffUtil when it wants to check whether two items have the same data. * 被DiffUtil調(diào)用,用來檢查 兩個(gè)item是否含有相同的數(shù)據(jù) * DiffUtil uses this information to detect if the contents of an item has changed. * DiffUtil用返回的信息(true false)來檢測(cè)當(dāng)前item的內(nèi)容是否發(fā)生了變化 * DiffUtil uses this method to check equality instead of {@link Object#equals(Object)} * DiffUtil 用這個(gè)方法替代equals方法去檢查是否相等。 * so that you can change its behavior depending on your UI. * 所以你可以根據(jù)你的UI去改變它的返回值 * For example, if you are using DiffUtil with a * {@link android.support.v7.widget.RecyclerView.Adapter RecyclerView.Adapter}, you should * return whether the items‘ visual representations are the same. * 例如,如果你用RecyclerView.Adapter 配合DiffUtil使用,你需要返回Item的視覺表現(xiàn)是否相同。 * This method is called only if {@link #areItemsTheSame(int, int)} returns * {@code true} for these items. * 這個(gè)方法僅僅在areItemsTheSame()返回true時(shí),才調(diào)用。 * @param oldItemPosition The position of the item in the old list * @param newItemPosition The position of the item in the new list which replaces the * oldItem * @return True if the contents of the items are the same or false if they are different. */ @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;//如果有內(nèi)容不同,就返回false } if (beanOld.getPic() != beanNew.getPic()) { return false;//如果有內(nèi)容不同,就返回false } return true; //默認(rèn)兩個(gè)data內(nèi)容是相同的 }
注釋張寫了這么詳細(xì)的注釋+簡(jiǎn)單的代碼,相信一眼可懂。
然后在使用時(shí),注釋掉你以前寫的notifyDatasetChanged()方法吧,替換成以下代碼:
//文藝青年新寵 //利用DiffUtil.calculateDiff()方法,傳入一個(gè)規(guī)則DiffUtil.Callback對(duì)象,和是否檢測(cè)移動(dòng)item的 boolean變量,得到DiffUtil.DiffResult 的對(duì)象 DiffUtil.DiffResult diffResult = DiffUtil.calculateDiff(new DiffCallBack(mDatas, newDatas), true); //利用DiffUtil.DiffResult對(duì)象的dispatchUpdatesTo()方法,傳入RecyclerView的Adapter,輕松成為文藝青年 diffResult.dispatchUpdatesTo(mAdapter); //別忘了將新數(shù)據(jù)給Adapter mDatas = newDatas; mAdapter.setDatas(mDatas);
講解:
步驟一
在將newDatas 設(shè)置給Adapter之前,先調(diào)用DiffUtil.calculateDiff()方法,計(jì)算出新老數(shù)據(jù)集轉(zhuǎn)化的最小更新集,就是
DiffUtil.DiffResult對(duì)象。
DiffUtil.calculateDiff()方法定義如下:
第一個(gè)參數(shù)是DiffUtil.Callback對(duì)象,
第二個(gè)參數(shù)代表是否檢測(cè)Item的移動(dòng),改為false算法效率更高,按需設(shè)置,我們這里是true。
public static DiffResult calculateDiff(Callback cb, boolean detectMoves)
步驟二
然后利用DiffUtil.DiffResult對(duì)象的dispatchUpdatesTo()方法,傳入RecyclerView的Adapter,替代普通青年才用的mAdapter.notifyDataSetChanged()方法。
查看源碼可知,該方法內(nèi)部,就是根據(jù)情況調(diào)用了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); } }); }
小結(jié):
所以說,DiffUtil不僅僅只能和RecyclerView配合,我們也可以自己實(shí)現(xiàn)ListUpdateCallback接口的四個(gè)方法去做一些事情。(我暫時(shí)不負(fù)責(zé)任隨便一項(xiàng)想,想到可以配合自己項(xiàng)目里的九宮格控件?或者優(yōu)化我上篇文章寫的NestFullListView?小安利,見 ListView、RecyclerView、ScrollView里嵌套ListView 相對(duì)優(yōu)雅的解決方案:http://blog.csdn.net/zxt0601/article/details/52494665)
至此,我們已進(jìn)化成文藝青年,運(yùn)行效果和第一節(jié)圖二基本一致,
唯一不同的是此時(shí)adapter.notifyItemRangeChanged()會(huì)有Item白光一閃的更新動(dòng)畫 (本文Demo的postion為0的item)。 這個(gè)Item一閃的動(dòng)畫有人喜歡有人恨,不過都不重要了,
因?yàn)楫?dāng)我們學(xué)會(huì)了第三節(jié)的DiffUtil搞基用法,你愛不愛這個(gè)ItemChange動(dòng)畫,它都將隨風(fēng)而去。(不知道是不是官方bug)
效果就是第一節(jié)的圖二,我們的item0其實(shí)圖片和文字都變化了,但是這個(gè)改變并沒有伴隨任何動(dòng)畫。
讓我們邁向 文藝青年中的文藝青年 之路。
三 DiffUtil的高級(jí)用法
理論:
高級(jí)用法只涉及到兩個(gè)方法,
我們需要分別實(shí)現(xiàn)DiffUtil.Callback的
public Object getChangePayload(int oldItemPosition, int newItemPosition)方法,
返回的Object就是表示Item改變了哪些內(nèi)容。
再配合RecyclerView.Adapter的
public void onBindViewHolder(VH holder, int position, List<Object> payloads)方法,
完成定向刷新。(成為文青中的文青,文青青。)
敲黑板,這是一個(gè)新方法,注意它有三個(gè)參數(shù),前兩個(gè)我們熟,第三個(gè)參數(shù)就包含了我們?cè)趃etChangePayload()返回的Object。
好吧,那我們就先看看這個(gè)方法是何方神圣:
在v7-24.2.0的源碼里,它長(zhǎng)這個(gè)樣子:
/** * Called by RecyclerView to display the data at the specified position. This method * should update the contents of the {@link ViewHolder#itemView} to reflect the item at * the given position. * <p> * Note that unlike {@link android.widget.ListView}, RecyclerView will not call this method * again if the position of the item changes in the data set unless the item itself is * invalidated or the new position cannot be determined. For this reason, you should only * use the <code>position</code> parameter while acquiring the related data item inside * this method and should not keep a copy of it. If you need the position of an item later * on (e.g. in a click listener), use {@link ViewHolder#getAdapterPosition()} which will * have the updated adapter position. * <p> * Partial bind vs full bind: * <p> * The payloads parameter is a merge list from {@link #notifyItemChanged(int, Object)} or * {@link #notifyItemRangeChanged(int, int, Object)}. If the payloads list is not empty, * the ViewHolder is currently bound to old data and Adapter may run an efficient partial * update using the payload info. If the payload is empty, Adapter must run a full bind. * Adapter should not assume that the payload passed in notify methods will be received by * onBindViewHolder(). For example when the view is not attached to the screen, the * payload in notifyItemChange() will be simply dropped. * * @param holder The ViewHolder which should be updated to represent the contents of the * item at the given position in the data set. * @param position The position of the item within the adapter‘s data set. * @param payloads A non-null list of merged payloads. Can be empty list if requires full * update. */ public void onBindViewHolder(VH holder, int position, List<Object> payloads) { onBindViewHolder(holder, position); }
原來它內(nèi)部就僅僅調(diào)用了兩個(gè)參數(shù)的onBindViewHolder(holder, position) ,(題外話,哎喲喂,我的NestFullListView 的Adapter也有幾分神似這種寫法,看來我離Google大神又近了一步)
看到這我才明白,其實(shí)onBind的入口,就是這個(gè)方法,它才是和onCreateViewHolder對(duì)應(yīng)的方法,
源碼往下翻幾行可以看到有個(gè)public final void bindViewHolder(VH holder, int position),它內(nèi)部調(diào)用了三參的onBindViewHolder。
關(guān)于RecyclerView.Adapter 也不是三言兩句說的清楚的。(其實(shí)我只掌握到這里)
好了不再跑題,回到我們的三參數(shù)的onBindViewHolder(VH holder, int position, List<Object> payloads),這個(gè)方法頭部有一大堆英文注釋,我一直覺得閱讀這些英文注釋對(duì)理解方法很有用處,于是我翻譯了一下,
翻譯:
由RecyclerView調(diào)用 用來在在指定的位置顯示數(shù)據(jù)。
這個(gè)方法應(yīng)該更新ViewHolder里的ItemView的內(nèi)容,以反映在給定的位置 Item(的變化)。
請(qǐng)注意,不像ListView,如果給定位置的item的數(shù)據(jù)集變化了,RecyclerView不會(huì)再次調(diào)用這個(gè)方法,除非item本身失效了(invalidated ) 或者新的位置不能確定。
出于這個(gè)原因,在這個(gè)方法里,你應(yīng)該只使用 postion參數(shù) 去獲取相關(guān)的數(shù)據(jù)item,而且不應(yīng)該去保持 這個(gè)數(shù)據(jù)item的副本。
如果你稍后需要這個(gè)item的position,例如設(shè)置clickListener。應(yīng)該使用 ViewHolder.getAdapterPosition(),它能提供 更新后的位置。
(二筆的我看到這里發(fā)現(xiàn) 這是在講解兩參的onbindViewHolder方法
下面是這個(gè)三參方法的獨(dú)特部分:)
**部分(partial)綁定**vs完整(full)綁定
payloads 參數(shù) 是一個(gè)從(notifyItemChanged(int, Object)或notifyItemRangeChanged(int, int, Object))里得到的合并list。
如果payloads list 不為空,那么當(dāng)前綁定了舊數(shù)據(jù)的ViewHolder 和Adapter, 可以使用 payload的數(shù)據(jù)進(jìn)行一次 高效的部分更新。
如果payload 是空的,Adapter必須進(jìn)行一次完整綁定(調(diào)用兩參方法)。
Adapter不應(yīng)該假定(想當(dāng)然的認(rèn)為) 在那些notifyxxxx通知方法傳遞過來的payload, 一定會(huì)在 onBindViewHolder()方法里收到。(這一句翻譯不好 QAQ 看舉例就好)
舉例來說,當(dāng)View沒有attached 在屏幕上時(shí),這個(gè)來自notifyItemChange()的payload 就簡(jiǎn)單的丟掉好了。
payloads對(duì)象不會(huì)為null,但是它可能是空(empty),這時(shí)候需要完整綁定(所以我們?cè)诜椒ɡ镏灰袛鄆sEmpty就好,不用重復(fù)判空)。
作者語:這方法是一個(gè)高效的方法。 我是個(gè)低效的翻譯者,我看了40+分鐘。才終于明白,重要的部分已經(jīng)加粗顯示。
實(shí)戰(zhàn):
說了這么多話,其實(shí)用起來超級(jí)簡(jiǎn)單:
先看如何使用getChangePayload()方法,又附帶了中英雙語注釋
/** * When {@link #areItemsTheSame(int, int)} returns {@code true} for two items and * {@link #areContentsTheSame(int, int)} returns false for them, DiffUtil * calls this method to get a payload about the change. * * 當(dāng){@link #areItemsTheSame(int, int)} 返回true,且{@link #areContentsTheSame(int, int)} 返回false時(shí),DiffUtils會(huì)回調(diào)此方法, * 去得到這個(gè)Item(有哪些)改變的payload。 * * For example, if you are using DiffUtil with {@link RecyclerView}, you can return the * particular field that changed in the item and your * {@link android.support.v7.widget.RecyclerView.ItemAnimator ItemAnimator} can use that * information to run the correct animation. * * 例如,如果你用RecyclerView配合DiffUtils,你可以返回 這個(gè)Item改變的那些字段, * {@link android.support.v7.widget.RecyclerView.ItemAnimator ItemAnimator} 可以用那些信息去執(zhí)行正確的動(dòng)畫 * * Default implementation returns {@code null}. * 默認(rèn)的實(shí)現(xiàn)是返回null * * @param oldItemPosition The position of the item in the old list * @param newItemPosition The position of the item in the new list * @return A payload object that represents the change between the two items. * 返回 一個(gè) 代表著新老item的改變內(nèi)容的 payload對(duì)象, */ @Nullable @Override public Object getChangePayload(int oldItemPosition, int newItemPosition) { //實(shí)現(xiàn)這個(gè)方法 就能成為文藝青年中的文藝青年 // 定向刷新中的部分更新 // 效率最高 //只是沒有了ItemChange的白光一閃動(dòng)畫,(反正我也覺得不太重要) TestBean oldBean = mOldDatas.get(oldItemPosition); TestBean newBean = mNewDatas.get(newItemPosition); //這里就不用比較核心字段了,一定相等 Bundle payload = new Bundle(); if (!oldBean.getDesc().equals(newBean.getDesc())) { payload.putString("KEY_DESC", newBean.getDesc()); } if (oldBean.getPic() != newBean.getPic()) { payload.putInt("KEY_PIC", newBean.getPic()); } if (payload.size() == 0)//如果沒有變化 就傳空 return null; return payload;// }
簡(jiǎn)單的說,這個(gè)方法返回一個(gè)Object類型的payload,它包含了某個(gè)item的變化了的那些內(nèi)容。
我們這里使用Bundle保存這些變化。
在Adapter里如下重寫三參的onBindViewHolder:
@Override public void onBindViewHolder(DiffVH holder, int position, List<Object> payloads) { if (payloads.isEmpty()) { onBindViewHolder(holder, position); } else { //文藝青年中的文青 Bundle payload = (Bundle) payloads.get(0); TestBean bean = mDatas.get(position); for (String key : payload.keySet()) { switch (key) { case "KEY_DESC": //這里可以用payload里的數(shù)據(jù),不過data也是新的 也可以用 holder.tv2.setText(bean.getDesc()); break; case "KEY_PIC": holder.iv.setImageResource(payload.getInt(key)); break; default: break; } } } }
這里傳遞過來的payloads是一個(gè)List,由注釋可知,一定不為null,所以我們判斷是否是empty,
如果是empty,就調(diào)用兩參的函數(shù),進(jìn)行一次Full Bind。
如果不是empty,就進(jìn)行partial bind,
通過下標(biāo)0取出我們?cè)趃etChangePayload方法里返回的payload,然后遍歷payload的key,根據(jù)key檢索,如果payload里攜帶有相應(yīng)的改變,就取出來 然后更新在ItemView上。
(這里,通過mDatas獲得的也是最新數(shù)據(jù)源的數(shù)據(jù),所以用payload的數(shù)據(jù)或者新數(shù)據(jù)的數(shù)據(jù) 進(jìn)行更新都可以)
至此,我們已經(jīng)掌握了刷新RecyclerView,文藝青年中最文藝的那種寫法。
四 在子線程中使用DiffUtil
在DiffUtil的源碼頭部注釋中介紹了DiffUtil的相關(guān)信息,
DiffUtil內(nèi)部采用的Eugene W. Myers's difference 算法,但該算法不能檢測(cè)移動(dòng)的item,所以Google在其基礎(chǔ)上改進(jìn)支持檢測(cè)移動(dòng)項(xiàng)目,但是檢測(cè)移動(dòng)項(xiàng)目,會(huì)更耗性能。
在有1000項(xiàng)數(shù)據(jù),200處改動(dòng)時(shí),這個(gè)算法的耗時(shí):
打開了移動(dòng)檢測(cè)時(shí):平均值:27.07ms,中位數(shù):26.92ms。
關(guān)閉了移動(dòng)檢測(cè)時(shí):平均值:13.54ms,中位數(shù):13.36ms。
有興趣可以自行去源碼頭部閱讀注釋,對(duì)我們比較有用的是其中一段提到,
如果我們的list過大,這個(gè)計(jì)算出DiffResult的時(shí)間還是蠻久的,所以我們應(yīng)該將獲取DiffResult的過程放到子線程中,并在主線程中更新RecyclerView。
這里我采用Handler配合DiffUtil使用:
代碼如下:
private static final int H_CODE_UPDATE = 1; private List<TestBean> mNewDatas;//增加一個(gè)變量暫存newList private Handler mHandler = new Handler() { @Override public void handleMessage(Message msg) { switch (msg.what) { case H_CODE_UPDATE: //取出Result DiffUtil.DiffResult diffResult = (DiffUtil.DiffResult) msg.obj; diffResult.dispatchUpdatesTo(mAdapter); //別忘了將新數(shù)據(jù)給Adapter mDatas = mNewDatas; mAdapter.setDatas(mDatas); break; } } }; new Thread(new Runnable() { @Override public void run() { //放在子線程中計(jì)算DiffResult DiffUtil.DiffResult diffResult = DiffUtil.calculateDiff(new DiffCallBack(mDatas, mNewDatas), true); Message message = mHandler.obtainMessage(H_CODE_UPDATE); message.obj = diffResult;//obj存放DiffResult message.sendToTarget(); } }).start();
就是簡(jiǎn)單的Handler使用,不再贅述。
五總結(jié)和其他
1 其實(shí)本文代碼量很少,可下載Demo查看,一共就四個(gè)類。
但是不知不覺又被我寫的這么長(zhǎng),主要涉及到了一些源碼的注釋的翻譯,方便大家更好的理解。
2 DiffUtil很適合下拉刷新這種場(chǎng)景,
更新的效率提高了,而且?guī)?dòng)畫,而且~還不用你動(dòng)腦子算了。
不過若是就做個(gè)刪除 點(diǎn)贊這種,完全不用DiffUtils。自己記好postion,判斷一下postion在不在屏幕里,調(diào)用那幾個(gè)定向刷新的方法即可。
3 其實(shí)DiffUtil不是只能和RecyclerView.Adapter配合使用,
我們可以自己實(shí)現(xiàn) ListUpdateCallback接口,利用DIffUtil幫我們找到新舊數(shù)據(jù)集的最小差異集 來做更多的事情。
4 注意 寫DEMO的時(shí)候,用于比較的新老數(shù)據(jù)集,不僅ArrayList不同,里面每個(gè)data也要不同。 否則changed 無法觸發(fā)。
實(shí)際項(xiàng)目中遇不到,因?yàn)樾聰?shù)據(jù)往往是網(wǎng)絡(luò)來的。
5 今天是中秋節(jié)的最后一天,我們公司居然就開始上班了!?。鈶嵵?,我怒碼一篇DiffUtil,我都不需要用DiffUtil,也能輕易比較出我們公司和其他公司的差異。QAQ,而且今天狀態(tài)不佳,居然寫了8個(gè)小時(shí)才完工。本以為這篇文章是可以入選微作文集的,沒想到也是蠻長(zhǎng)的。沒有耐心的其實(shí)可以下載DEMO看看,代碼量沒多少,使用起來還是很輕松的。
github傳送門:
https://github.com/mcxtzhang/DiffUtils
以上就是對(duì)Android7.0 工具類DiffUtil 的資料整理,后續(xù)繼續(xù)補(bǔ)充相關(guān)資料,謝謝大家對(duì)本站的支持!
相關(guān)文章
Android自定義控件之圓形進(jìn)度條動(dòng)畫
這篇文章主要為大家詳細(xì)介紹了Android自定義控件之圓形進(jìn)度條動(dòng)畫,文中示例代碼介紹的非常詳細(xì),具有一定的參考價(jià)值,感興趣的小伙伴們可以參考一下2020-07-07Android實(shí)現(xiàn)桌面懸浮窗、蒙板效果實(shí)例代碼
這篇文章主要介紹了Android實(shí)現(xiàn)桌面懸浮窗、蒙板效果實(shí)例代碼的相關(guān)資料,需要的朋友可以參考下2016-05-05Android開發(fā)Input系統(tǒng)觸摸事件分發(fā)
這篇文章主要為大家介紹了Android開發(fā)Input系統(tǒng)觸摸事件分發(fā)示例解析,有需要的朋友可以借鑒參考下,希望能夠有所幫助,祝大家多多進(jìn)步,早日升職加薪2023-03-03關(guān)于Android bitmap你不知道的一些事
這篇文章主要為大家詳細(xì)介紹了關(guān)于Android bitmap你不知道的一些事,使用bitmap需要注意的一些細(xì)節(jié),具有一定的參考價(jià)值,感興趣的小伙伴們可以參考一下2016-11-11一文詳解?Compose?Navigation?的實(shí)現(xiàn)原理
這篇文章主要介紹了一文詳解?Compose?Navigation的實(shí)現(xiàn)原理,文章通告圍繞主題展開詳細(xì)的相關(guān)內(nèi)容介紹,具有一定的參考價(jià)值,需要的小伙伴可以參考一下2022-08-08詳解Flutter自定義應(yīng)用程序內(nèi)鍵盤的實(shí)現(xiàn)方法
本文將展示如何利用Flutter創(chuàng)建自定義鍵盤小部件,用于在自己的應(yīng)用程序中的Flutter TextField中輸入文本,感興趣的小伙伴可以了解一下2022-06-06Android中查看USB連接的外接設(shè)備信息的代碼實(shí)例
這篇文章主要介紹了Android中查看USB連接的外接設(shè)備信息的代碼實(shí)例,需要的朋友可以參考下2014-04-04