可以看到,当我们点击按钮的时候,这个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);}@Overridepublic RecyclerView.ViewHolder onCreateViewHolder(ViewGroup parent, int viewType) { View itemView = LayoutInflater.from(RecyclerViewActivity.this).inflate(R.layout.itemview, null); return new MyViewHolder(itemView);}@Overridepublic void onBindViewHolder(RecyclerView.ViewHolder holder, int position) { MyViewHolder myViewHolder = (MyViewHolder) holder; Student student = data.get(position); myViewHolder.tv.setText(student.getNum() + "." + student.getName());}@Overridepublic 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,先记着,后面会解释为什么要这样做。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中的集合为旧的数据。
DiffUtil.calculateDiff
方法来计算集合的差别,这里要传入一个CallBack接口的实现类(用于指定计算的规则)并且把新旧数据都传递给这个接口的实现类,最后还有一个boolean类型的参数,这个参数指定是否需要进行Move的检测,如果不需要,如果有Item移动了,会被认为是先remove,然后insert。这里指定为true,所以就有了动图显示的移动效果。dispatchUpdatesTo
方法通知RecyclerView刷新对应发生变化的Item。setData
方法,因为我们在这里要区分两个集合,如果在setData
方法中直接保存引用,那么在2-6行的修改就直接修改了Adapter中的集合了(Java知识)。
接着我们看看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;}@Overridepublic int getOldListSize() { return old_students.size();}@Overridepublic int getNewListSize() { return new_students.size();}// 判断Item是否已经存在@Overridepublic boolean areItemsTheSame(int oldItemPosition, int newItemPosition) { return old_students.get(oldItemPosition).getNum() == new_students.get(newItemPosition).getNum();}// 如果Item已经存在则会调用此方法,判断Item的内容是否一致@Overridepublic boolean areContentsTheSame(int oldItemPosition, int newItemPosition) { return old_students.get(oldItemPosition).getName().equals(new_students.get(newItemPosition).getName());} }这里根据学号判断是否同一个Item,根据姓名判断这个Item是否有被修改。
getChangePayload()
,这个方法的作用是我们可以通过这个方法告诉Adapter对这个Item进行局部的更新而不是整个更新。getChangePayload()
方法是在areItemsTheSame()
返回true,而areContentsTheSame()
返回false时被回调的,也就是一个Item的内容发生了变化,而这个变化有可能是局部的(例如微博的点赞,我们只需要刷新图标而不是整个Item)。所以可以在getChangePayload()
中封装一个Object来告诉RecyclerView进行局部的刷新。@Nullable@Overridepublic 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的内容进行局部刷新:
@Overridepublic 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开发者们有所帮助,如果有疑问大家可以留言交流。