Android開發(fā)之DiffUtil的使用詳解
寫在前面的話
DiffUtil是一個查找集合變化的工具類,是搭配RecyclerView一起使用的,如果你還不了解RecyclerView,可以閱讀一些資料,這里就不介紹了。
先放效果圖:

可以看到,當(dāng)我們點(diǎn)擊按鈕的時候,這個RecyclerView所顯示的集合發(fā)生了改變,有的元素被增加了(8.Jason),也有的元素被移動了(3.Rose),甚至是被修改了(2.Fndroid)。
RecyclerView對于每個Item的動畫是以不同方式刷新的:
notifyItemInserted
notifyItemChanged
notifyItemMoved
notifyItemRemoved
而對于連續(xù)的幾個Item的刷新,可以調(diào)用:
notifyItemRangeChanged
notifyItemRangeInserted
notifyItemRangeRemoved
而由于集合發(fā)生變化的時候,只可以調(diào)用notifyDataSetChanged方法進(jìn)行整個界面的刷新,并不能根據(jù)集合的變化為每一個變化的元素添加動畫。所以這里就有了DiffUtil來解決這個問題。
DiffUtil的作用,就是找出集合中每一個Item發(fā)生的變化,然后對每個變化給予對應(yīng)的刷新。
這個DiffUtil使用的是Eugene Myers的差別算法,這個算法本身不能檢查到元素的移動,也就是移動只能被算作先刪除、再增加,而DiffUtil是在算法的結(jié)果后再進(jìn)行一次移動檢查。假設(shè)在不檢測元素移動的情況下,算法的時間復(fù)雜度為O(N + D2),而檢測元素移動則復(fù)雜度為O(N2)。所以,如果集合本身就已經(jīng)排好序,可以不進(jìn)行移動的檢測提升效率。
下面我們一起來看看這個工具怎么用。
首先對于每個Item,數(shù)據(jù)是一個Student對象:
class Student {
private String name;
private int num;
public Student(String name, int num) {
this.name = name;
this.num = num;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public int getNum() {
return num;
}
public void setNum(int num) {
this.num = num;
}
}
接著我們定義布局(省略)和適配器:
class MyAdapter extends RecyclerView.Adapter {
private ArrayList<Student> data;
ArrayList<Student> getData() {
return data;
}
void setData(ArrayList<Student> data) {
this.data = new ArrayList<>(data);
}
@Override
public RecyclerView.ViewHolder onCreateViewHolder(ViewGroup parent, int viewType) {
View itemView = LayoutInflater.from(RecyclerViewActivity.this).inflate(R.layout.itemview, null);
return new MyViewHolder(itemView);
}
@Override
public void onBindViewHolder(RecyclerView.ViewHolder holder, int position) {
MyViewHolder myViewHolder = (MyViewHolder) holder;
Student student = data.get(position);
myViewHolder.tv.setText(student.getNum() + "." + student.getName());
}
@Override
public int getItemCount() {
return data.size();
}
class MyViewHolder extends RecyclerView.ViewHolder {
TextView tv;
MyViewHolder(View itemView) {
super(itemView);
tv = (TextView) itemView.findViewById(R.id.item_tv);
}
}
}
初始化數(shù)據(jù)集合:
private void initData() {
students = new ArrayList<>();
Student s1 = new Student("John", 1);
Student s2 = new Student("Curry", 2);
Student s3 = new Student("Rose", 3);
Student s4 = new Student("Dante", 4);
Student s5 = new Student("Lunar", 5);
students.add(s1);
students.add(s2);
students.add(s3);
students.add(s4);
students.add(s5);
}
接著實(shí)例化Adapter并設(shè)置給RecyclerView:
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_recycler_view);
initData();
recyclerView = (RecyclerView) findViewById(R.id.rv);
recyclerView.setLayoutManager(new LinearLayoutManager(this));
adapter = new MyAdapter();
adapter.setData(students);
recyclerView.setAdapter(adapter);
}
這些內(nèi)容都不是本篇的內(nèi)容,但是,需要注意到的一個地方是Adapter的定義:
class MyAdapter extends RecyclerView.Adapter {
private ArrayList<Student> data;
ArrayList<Student> getData() {
return data;
}
void setData(ArrayList<Student> data) {
this.data = new ArrayList<>(data);
}
// 省略部分代碼
......
}
這里的setData方法并不是直接將ArrayList的引用保存,而是重新的建立一個ArrayList,先記著,后面會解釋為什么要這樣做。
DiffUtil的使用方法:
當(dāng)鼠標(biāo)按下時,修改ArrayList的內(nèi)容:
public void change(View view) {
students.set(1, new Student("Fndroid", 2));
students.add(new Student("Jason", 8));
Student s2 = students.get(2);
students.remove(2);
students.add(s2);
ArrayList<Student> old_students = adapter.getData();
DiffUtil.DiffResult result = DiffUtil.calculateDiff(new MyCallback(old_students, students), true);
adapter.setData(students);
result.dispatchUpdatesTo(adapter);
}
2-6行是對集合進(jìn)行修改,第8行先獲取到adapter中的集合為舊的數(shù)據(jù)。
重點(diǎn)看第9行調(diào)用DiffUtil.calculateDiff方法來計(jì)算集合的差別,這里要傳入一個CallBack接口的實(shí)現(xiàn)類(用于指定計(jì)算的規(guī)則)并且把新舊數(shù)據(jù)都傳遞給這個接口的實(shí)現(xiàn)類,最后還有一個boolean類型的參數(shù),這個參數(shù)指定是否需要進(jìn)行Move的檢測,如果不需要,如果有Item移動了,會被認(rèn)為是先remove,然后insert。這里指定為true,所以就有了動圖顯示的移動效果。
第10行重新將新的數(shù)據(jù)設(shè)置給Adapter。
第11行調(diào)用第9行得到的DiffResult對象的dispatchUpdatesTo方法通知RecyclerView刷新對應(yīng)發(fā)生變化的Item。
這里回到上面說的setData方法,因?yàn)槲覀冊谶@里要區(qū)分兩個集合,如果在setData方法中直接保存引用,那么在2-6行的修改就直接修改了Adapter中的集合了(Java知識)。
如果設(shè)置不檢查Item的移動,效果如下:

接著我們看看CallBack接口的實(shí)現(xiàn)類如何定義:
private class MyCallback extends DiffUtil.Callback {
private ArrayList<Student> old_students, new_students;
MyCallback(ArrayList<Student> data, ArrayList<Student> students) {
this.old_students = data;
this.new_students = students;
}
@Override
public int getOldListSize() {
return old_students.size();
}
@Override
public int getNewListSize() {
return new_students.size();
}
// 判斷Item是否已經(jīng)存在
@Override
public boolean areItemsTheSame(int oldItemPosition, int newItemPosition) {
return old_students.get(oldItemPosition).getNum() == new_students.get(newItemPosition).getNum();
}
// 如果Item已經(jīng)存在則會調(diào)用此方法,判斷Item的內(nèi)容是否一致
@Override
public boolean areContentsTheSame(int oldItemPosition, int newItemPosition) {
return old_students.get(oldItemPosition).getName().equals(new_students.get(newItemPosition).getName());
}
}
這里根據(jù)學(xué)號判斷是否同一個Item,根據(jù)姓名判斷這個Item是否有被修改。
實(shí)際上,這個Callback抽象類還有一個方法getChangePayload() ,這個方法的作用是我們可以通過這個方法告訴Adapter對這個Item進(jìn)行局部的更新而不是整個更新。
先要知道這個payload是什么?payload是一個用來描述Item變化的對象,也就是我們的Item發(fā)生了哪些變化,這些變化就封裝成一個payload,所以我們一般可以用Bundle來充當(dāng)。
接著,getChangePayload()方法是在areItemsTheSame()返回true,而areContentsTheSame()返回false時被回調(diào)的,也就是一個Item的內(nèi)容發(fā)生了變化,而這個變化有可能是局部的(例如微博的點(diǎn)贊,我們只需要刷新圖標(biāo)而不是整個Item)。所以可以在getChangePayload()中封裝一個Object來告訴RecyclerView進(jìn)行局部的刷新。
假設(shè)上例中學(xué)號和姓名用不同的TextView顯示,當(dāng)我們修改了一個學(xué)號對應(yīng)的姓名時,局部刷新姓名即可(這里例子可能顯得比較多余,但是如果一個Item很復(fù)雜,用處就比較大了):
先是重寫Callback中的該方法:
@Nullable
@Override
public Object getChangePayload(int oldItemPosition, int newItemPosition) {
Student newStudent = newStudents.get(newItemPosition);
Bundle diffBundle = new Bundle();
diffBundle.putString(NAME_KEY, newStudent.getName());
return diffBundle;
}
返回的這個對象會在什么地方收到呢?實(shí)際上在RecyclerView.Adapter中有兩個onBindViewHolder方法,一個是我們必須要重寫的,而另一個的第三個參數(shù)就是一個payload的列表:
@Override
public void onBindViewHolder(RecyclerView.ViewHolder holder, int position, List payloads) {}
所以我們只需在Adapter中重寫這個方法,如果List為空,執(zhí)行原來的onBindViewHolder進(jìn)行整個Item的更新,否則根據(jù)payloads的內(nèi)容進(jìn)行局部刷新:
@Override
public void onBindViewHolder(RecyclerView.ViewHolder holder, int position, List payloads) {
if (payloads.isEmpty()) {
onBindViewHolder(holder, position);
} else {
MyViewHolder myViewHolder = (MyViewHolder) holder;
Bundle bundle = (Bundle) payloads.get(0);
if (bundle.getString(NAME_KEY) != null) {
myViewHolder.name.setText(bundle.getString(NAME_KEY));
myViewHolder.name.setTextColor(Color.BLUE);
}
}
}
這里的payloads不會為null,所以直接判斷是否為空即可。

這里注意:如果RecyclerView中加載了大量數(shù)據(jù),那么算法可能不會馬上完成,要注意ANR的問題,可以開啟單獨(dú)的線程進(jìn)行計(jì)算。
總結(jié)
Android中DiffUtil的使用就介紹到這了,希望這篇文章能對Android開發(fā)者們有所幫助,如果有疑問大家可以留言交流。
- Android7.0 工具類:DiffUtil詳解
- 一看就懂的Android APP開發(fā)入門教程
- Android基礎(chǔ)之使用Fragment控制切換多個頁面
- 六款值得推薦的android(安卓)開源框架簡介
- Android應(yīng)用開發(fā)SharedPreferences存儲數(shù)據(jù)的使用方法
- android TextView設(shè)置中文字體加粗實(shí)現(xiàn)方法
- Android 動畫之TranslateAnimation應(yīng)用詳解
- Android Bitmap詳細(xì)介紹
- android PopupWindow 和 Activity彈出窗口實(shí)現(xiàn)方式
- 解決Android SDK下載和更新失敗的方法詳解
相關(guān)文章
Android Studio實(shí)現(xiàn)補(bǔ)間動畫
這篇文章主要為大家詳細(xì)介紹了Android Studio實(shí)現(xiàn)補(bǔ)間動畫,文中示例代碼介紹的非常詳細(xì),具有一定的參考價值,感興趣的小伙伴們可以參考一下2021-11-11
Android自定義view實(shí)現(xiàn)滑動解鎖九宮格控件
這篇文章主要介紹了Android自定義view實(shí)現(xiàn)滑動解鎖九宮格控件,文中通過示例代碼介紹的非常詳細(xì),對大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價值,需要的朋友們下面隨著小編來一起學(xué)習(xí)吧2023-02-02
分享安裝Android Studio3.6的經(jīng)驗(yàn)教訓(xùn)
這篇文章主要介紹了Android Studio3.6的安裝錯誤問題及解決方法,非常值得大家參考,現(xiàn)把整個過程分享到腳本之家平臺,需要的朋友參考下吧2020-02-02
淺析Flutter AbsorbPointer 與 IgnorePointer的區(qū)別
Flutter是Google一個新的用于構(gòu)建跨平臺的手機(jī)App的SDK。這篇文章主要介紹了Flutter AbsorbPointer 與 IgnorePointer的區(qū)別,本文通過實(shí)例代碼給大家介紹的非常詳細(xì),對大家的學(xué)習(xí)或工作具有一定的參考借鑒價值,需要的朋友可以參考下2020-04-04
Jetpack Compose圖片組件使用實(shí)例詳細(xì)講解
在Compose中,圖片組件主要有兩種,分別是顯示圖標(biāo)的Icon組件和顯示圖片的Image組件,當(dāng)我們顯示一系列的小圖標(biāo)的時候,我們可以使用Icon組件,當(dāng)顯示圖片時,我們就用專用的Image組件2023-04-04
Android TreeView效果實(shí)現(xiàn)方法(附demo源碼下載)
這篇文章主要介紹了Android TreeView效果實(shí)現(xiàn)方法,結(jié)合實(shí)例形式分析了Android TreeView效果的實(shí)現(xiàn)原理與具體技巧,并附帶demo源碼供讀者下載,需要的朋友可以參考下2016-02-02
自定義View系列之kotlin繪制手勢設(shè)置溫度控件的方法
這篇文章主要給大家介紹了關(guān)于自定義View系列之kotlin繪制手勢設(shè)置溫度控件的相關(guān)資料,文中通過示例代碼介紹的非常詳細(xì),對大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價值,需要的朋友們下面隨著小編來一起學(xué)習(xí)學(xué)習(xí)吧2018-07-07
Android自定義Gallery控件實(shí)現(xiàn)3D圖片瀏覽器
這篇文章主要介紹了Android自定義Gallery控件實(shí)現(xiàn)3D圖片瀏覽器,具有一定的參考價值,感興趣的小伙伴們可以參考一下2018-04-04

