Android开发之DiffUtil的使用详解
写在前面的话
diffutil是一个查找集合变化的工具类,是搭配recyclerview一起使用的,如果你还不了解recyclerview,可以阅读一些资料,这里就不介绍了。
先放效果图:
可以看到,当我们点击按钮的时候,这个recyclerview所显示的集合发生了改变,有的元素被增加了(8.jason),也有的元素被移动了(3.rose),甚至是被修改了(2.fndroid)。
recyclerview对于每个item的动画是以不同方式刷新的:
notifyiteminserted
notifyitemchanged
notifyitemmoved
notifyitemremoved
而对于连续的几个item的刷新,可以调用:
notifyitemrangechanged
notifyitemrangeinserted
notifyitemrangeremoved
而由于集合发生变化的时候,只可以调用notifydatasetchanged
方法进行整个界面的刷新,并不能根据集合的变化为每一个变化的元素添加动画。所以这里就有了diffutil来解决这个问题。
diffutil的作用,就是找出集合中每一个item发生的变化,然后对每个变化给予对应的刷新。
这个diffutil使用的是eugene myers的差别算法,这个算法本身不能检查到元素的移动,也就是移动只能被算作先删除、再增加,而diffutil是在算法的结果后再进行一次移动检查。假设在不检测元素移动的情况下,算法的时间复杂度为o(n + d2),而检测元素移动则复杂度为o(n2)。所以,如果集合本身就已经排好序,可以不进行移动的检测提升效率。
下面我们一起来看看这个工具怎么用。
首先对于每个item,数据是一个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); } } }
初始化数据集合:
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); }
接着实例化adapter并设置给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); }
这些内容都不是本篇的内容,但是,需要注意到的一个地方是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的使用方法:
当鼠标按下时,修改arraylist的内容:
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行是对集合进行修改,第8行先获取到adapter中的集合为旧的数据。
重点看第9行调用diffutil.calculatediff
方法来计算集合的差别,这里要传入一个callback接口的实现类(用于指定计算的规则)并且把新旧数据都传递给这个接口的实现类,最后还有一个boolean类型的参数,这个参数指定是否需要进行move的检测,如果不需要,如果有item移动了,会被认为是先remove,然后insert。这里指定为true,所以就有了动图显示的移动效果。
第10行重新将新的数据设置给adapter。
第11行调用第9行得到的diffresult对象的dispatchupdatesto
方法通知recyclerview刷新对应发生变化的item。
这里回到上面说的setdata
方法,因为我们在这里要区分两个集合,如果在setdata
方法中直接保存引用,那么在2-6行的修改就直接修改了adapter中的集合了(java知识)。
如果设置不检查item的移动,效果如下:
接着我们看看callback接口的实现类如何定义:
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是否已经存在 @override public boolean areitemsthesame(int olditemposition, int newitemposition) { return old_students.get(olditemposition).getnum() == new_students.get(newitemposition).getnum(); } // 如果item已经存在则会调用此方法,判断item的内容是否一致 @override public boolean arecontentsthesame(int olditemposition, int newitemposition) { return old_students.get(olditemposition).getname().equals(new_students.get(newitemposition).getname()); } }
这里根据学号判断是否同一个item,根据姓名判断这个item是否有被修改。
实际上,这个callback抽象类还有一个方法getchangepayload()
,这个方法的作用是我们可以通过这个方法告诉adapter对这个item进行局部的更新而不是整个更新。
先要知道这个payload是什么?payload是一个用来描述item变化的对象,也就是我们的item发生了哪些变化,这些变化就封装成一个payload,所以我们一般可以用bundle来充当。
接着,getchangepayload()
方法是在areitemsthesame()
返回true,而arecontentsthesame()
返回false时被回调的,也就是一个item的内容发生了变化,而这个变化有可能是局部的(例如微博的点赞,我们只需要刷新图标而不是整个item)。所以可以在getchangepayload()
中封装一个object来告诉recyclerview进行局部的刷新。
假设上例中学号和姓名用不同的textview显示,当我们修改了一个学号对应的姓名时,局部刷新姓名即可(这里例子可能显得比较多余,但是如果一个item很复杂,用处就比较大了):
先是重写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; }
返回的这个对象会在什么地方收到呢?实际上在recyclerview.adapter
中有两个onbindviewholder
方法,一个是我们必须要重写的,而另一个的第三个参数就是一个payload的列表:
@override public void onbindviewholder(recyclerview.viewholder holder, int position, list payloads) {}
所以我们只需在adapter中重写这个方法,如果list为空,执行原来的onbindviewholder进行整个item的更新,否则根据payloads的内容进行局部刷新:
@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中加载了大量数据,那么算法可能不会马上完成,要注意anr的问题,可以开启单独的线程进行计算。
总结
android中diffutil的使用就介绍到这了,希望这篇文章能对android开发者们有所帮助,如果有疑问大家可以留言交流。
推荐阅读