谷歌最近更新了Support Library 24.2.0,而DiffUtil就是在这个版本添加的一个工具类。

DiffUtil是一个查找集合变化的工具类,是搭配RecyclerView一起使用的,如果你还不了解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使用教程的更多相关文章

  1. Android开发学习之路-RecyclerView滑动删除和拖动排序

    Android开发学习之路-RecyclerView使用初探 Android开发学习之路-RecyclerView的Item自定义动画及DefaultItemAnimator源码分析 Android开 ...

  2. Android开发学习之路--基于vitamio的视频播放器(二)

      终于把该忙的事情都忙得差不多了,接下来又可以开始good good study,day day up了.在Android开发学习之路–基于vitamio的视频播放器(一)中,主要讲了播放器的界面的 ...

  3. Android开发学习之路--Android Studio cmake编译ffmpeg

      最新的android studio2.2引入了cmake可以很好地实现ndk的编写.这里使用最新的方式,对于以前的android下的ndk编译什么的可以参考之前的文章:Android开发学习之路– ...

  4. Android开发学习之路--网络编程之xml、json

    一般网络数据通过http来get,post,那么其中的数据不可能杂乱无章,比如我要post一段数据,肯定是要有一定的格式,协议的.常用的就是xml和json了.在此先要搭建个简单的服务器吧,首先呢下载 ...

  5. Android开发学习之路--Activity之初体验

    环境也搭建好了,android系统也基本了解了,那么接下来就可以开始学习android开发了,相信这么学下去肯定可以把android开发学习好的,再加上时而再温故下linux下的知识,看看androi ...

  6. Android开发学习之路--Android系统架构初探

    环境搭建好了,最简单的app也运行过了,那么app到底是怎么运行在手机上的,手机又到底怎么能运行这些应用,一堆的电子元器件最后可以运行这么美妙的界面,在此还是需要好好研究研究.这里从芯片及硬件模块-& ...

  7. Android开发学习之路--MAC下Android Studio开发环境搭建

    自从毕业开始到现在还没有系统地学习android应用的开发,之前一直都是做些底层的驱动,以及linux上的c开发.虽然写过几个简单的app,也对android4.0.3的源代码做过部分的分析,也算入门 ...

  8. Android开发学习之路-记一次CSDN公开课

    今天的CSDN公开课Android事件处理重难点快速掌握中老师讲到一个概念我觉得不正确. 原话是这样的:点击事件可以通过事件监听和回调两种方法实现. 我一听到之后我的表情是这样的: 这跟我学的看的都不 ...

  9. Android开发学习之路-Android Studio开发小技巧

    上一次发过了一个介绍Studio的,这里再发一个补充下. 我们都知道,Android Studio的功能是非常强大的,也是很智能的.如果有人告诉你学Android开发要用命令行,你可以告诉他Andro ...

随机推荐

  1. 【分享】标准springMVC+mybatis项目maven搭建最精简教程

    文章由来:公司有个实习同学需要做毕业设计,不会搭建环境,我就代劳了,顺便分享给刚入门的小伙伴,我是自学的JAVA,所以我懂的.... (大图直接观看显示很模糊,请在图片上点击右键然后在新窗口打开看) ...

  2. 解决 Springboot Unable to build Hibernate SessionFactory @Column命名不起作用

    问题: Springboot启动报错: Caused by: org.springframework.beans.factory.BeanCreationException: Error creati ...

  3. ExtJS 4.2 业务开发(一)主页搭建

    本篇开始搭建一个ExtJS 4.2单页面应用, 这里先介绍主页的搭建,内容包括:主页结构说明.扩展功能等方面. 目录 1. 主页结构说明 2. 扩展功能 3. 在线演示 1. 主页结构说明 1.1 主 ...

  4. 利用XAG在RAC环境下实现GoldenGate自动Failover

    概述 在RAC环境下配置OGG,要想实现RAC节点故障时,OGG能自动的failover到正常节点,要保证两点: 1. OGG的checkpoint,trail,BR文件放置在共享的集群文件系统上,R ...

  5. 谈谈一些有趣的CSS题目(七)-- 消失的边界线问题

    开本系列,谈谈一些有趣的 CSS 题目,题目类型天马行空,想到什么说什么,不仅为了拓宽一下解决问题的思路,更涉及一些容易忽视的 CSS 细节. 解题不考虑兼容性,题目天马行空,想到什么说什么,如果解题 ...

  6. Autofac - 方法注入

    方法注入, 其实就是在注册类的时候, 把这个方法也注册进去. 那么在生成实例的时候, 会自动调用这个方法. 其实现的方法, 有两种. 准备工作: public interface IAnimal { ...

  7. prometheus监控系统

    关于Prometheus Prometheus是一套开源的监控系统,它将所有信息都存储为时间序列数据:因此实现一种Profiling监控方式,实时分析系统运行的状态.执行时间.调用次数等,以找到系统的 ...

  8. Python学习基础

    1.使用范围: 大数据 .图像处理.web .运维.爬虫.自动化.科学计算 2.准备环境: linux/mac python 3.5.2 ipython vim/sublime/atom 3.列表 3 ...

  9. LeetCode All in One 题目讲解汇总(持续更新中...)

    终于将LeetCode的免费题刷完了,真是漫长的第一遍啊,估计很多题都忘的差不多了,这次开个题目汇总贴,并附上每道题目的解题连接,方便之后查阅吧~ 477 Total Hamming Distance ...

  10. SQL Server 索引和表体系结构(聚集索引)

    聚集索引 概述 关于索引和表体系结构的概念一直都是讨论比较多的话题,其中表的各种存储形式是讨论的重点,在各个网站上面也有很多关于这方面写的不错的文章,我写这篇文章的目的也是为了将所有的知识点尽可能的组 ...