欢迎您访问程序员文章站本站旨在为大家提供分享程序员计算机编程知识!
您现在的位置是: 首页  >  移动技术

Android开发之DiffUtil的使用详解

程序员文章站 2024-03-05 11:14:42
写在前面的话 diffutil是一个查找集合变化的工具类,是搭配recyclerview一起使用的,如果你还不了解recyclerview,可以阅读一些资料,这里就不介绍...

写在前面的话

diffutil是一个查找集合变化的工具类,是搭配recyclerview一起使用的,如果你还不了解recyclerview,可以阅读一些资料,这里就不介绍了。

先放效果图:

Android开发之DiffUtil的使用详解

可以看到,当我们点击按钮的时候,这个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的移动,效果如下:

Android开发之DiffUtil的使用详解

接着我们看看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,所以直接判断是否为空即可。

Android开发之DiffUtil的使用详解

这里注意:如果recyclerview中加载了大量数据,那么算法可能不会马上完成,要注意anr的问题,可以开启单独的线程进行计算。

总结

android中diffutil的使用就介绍到这了,希望这篇文章能对android开发者们有所帮助,如果有疑问大家可以留言交流。