前言
RecyclerView提升性能很重要的一点就是支持局部更新效果,以前的ListView如果修改了数据通常要调用notifyDatasetChanged导致整个ListView内的布局都要重新刷新,现在RecyclerView除了全部刷新的方法之外还提供了单项、多条数据更新的回调方法。
增删改交换
方法 | 意义 |
---|---|
notifyItemInserted(int item) | 通知增加了单条数据 |
notifyItemRemoved(int item) | 通知删除了单条数据 |
notifyItemChanged(int item) | 通知修改了单条数据 |
notifyItemMoved(int from, int to) | 通知移动了两条数据 |
notifyItemRangeInserted(int pos, int count) | 通知增加了一批数据 |
notifyItemRangeRemoved(int pos, int count) | 通知删除了一批数据 |
notifyDataSetChanged() | 通知所有的数据发生了变化,前面的更新方式都有动画效果(如果用户设置了),这种更新方式没有动画效果 |
DiffUtils整体绑定
DiffUtils是Android7.0新引入的工具,用来对比当前的旧数据和新得到的数据之间的差异,最后通过上面的各种通知操作完成更新,这种实现优点在数据基本上相同的情况下只更新修改的少量数据,不必对整体的列表数据做刷新操作,而且还支持数据的刷新的各种动画效果。实现时首先需要定义对比新旧数据的回调接口,然后工具计算会返回差异结果,最后将差异结果提交个Adapter来刷新RecyclerView,刷新的时候发现数据有变化的条目所有的控件都会被重新设置内容,所以也就是整体绑定,如果用户希望只设置内容改变的控件可以使用后面提到的部分绑定。
// RecyclerView.Adapter的子类方法,负责更新所有数据
public void replace() {
this.data = userList;
DiffUtil.DiffResult diffResult = DiffUtil.calculateDiff(new DiffUtil.Callback() {
// 旧数据的数量
@Override
public int getOldListSize() {
return users.size();
}
// 新数据数量
@Override
public int getNewListSize() {
return userList.size();
}
// 数据条目是否是同一条数据,这里使用名字来判断是否是同一条数据
@Override
public boolean areItemsTheSame(int oldItemPosition, int newItemPosition) {
User user = users.get(oldItemPosition);
User newUser = userList.get(newItemPosition);
return user.getName().equalsIgnoreCase(newUser.getName());
}
// 同一条数据的内容是否发生过更改,这里对比User里的所有内容
@Override
public boolean areContentsTheSame(int oldItemPosition, int newItemPosition) {
User user = users.get(oldItemPosition);
User newUser = userList.get(newItemPosition);
return user.equals(newUser);
}
}, true);
// 得到差异结果之后发布到Adapter并更新界面
diffResult.dispatchUpdatesTo(this);
}
public void replaceAll() {
this.data = userList;
notifyDataSetChanged();
}
调用这种方式实现的批量更新效果如下:
同时在使用前面提到的notifyDatasetChanged方法来更新数据,再看下这种更新方式的实现效果:
对比发现DiffUtils的实现效果用户体验更好,而且性能更好,不过计算过程有时候非常的耗时,通常用户获取数据都是在子线程中,可以先在子线程中计算完结果之后再到主线程中更新界面。
DiffUtils部分绑定
上面的虽然已经做到不需要整体数据全部刷新,但是对于更改的数据条目整条都需要更新以便,加入条目里很多地方都没改,只改了一个文案,其实刷新其他控件就没有必要了,DiffUtils的回调接口里提供了一个getPayload的方法返回实际被更新的部分,然后在Adapter.onBindViewHolder(position, viewHolder, payload)里用这个方法里只刷新改变了的控件内容,实现更加高效的部分绑定。
@Nullable
@Override
public Object getChangePayload(int oldItemPosition, int newItemPosition) {
User user = users.get(oldItemPosition);
User newUser = userList.get(newItemPosition);
// 将相同的数据条目里不同的部分记录下来
Map<Integer, String> map = new HashMap<>();
if (!user.getName().equalsIgnoreCase(newUser.getName())) {
map.put(R.id.name_attr, newUser.getName());
}
if (!user.getDesc().equalsIgnoreCase(newUser.getDesc())) {
map.put(R.id.desc_attr, newUser.getDesc());
}
if (user.getPortrait() != newUser.getPortrait()) {
map.put(R.id.portrait_attr, String.valueOf(newUser.getPortrait()));
}
// 将记录不同数据的map返回作为payload对象
return map;
}
覆盖Adapter的onBindViewHolder方法,记得需要是三个参数的那个方法,最后那个参数是一个List的参数,如果当时payload对应的View没有被attach到RecyclerView上,这个对象列表就是空的,当然如果默认用户没返回payload它一样是空的但不是null。在这里获取返回的payload对象并且遍历它内部记录的不同属性值,更新对应的控件内容。
@Override
public void onBindViewHolder(RecyclerViewHolder holder, int position, List<Object> payloads) {
// 如果没有payload对象,那么直接采用整体绑定
if (payloads.isEmpty()) {
super.onBindViewHolder(holder, position, payloads);
} else {
// 如果有payload对象那么根据记录的更改内容更新对应的控件内容
Map<Integer, String> map = (Map<Integer, String>) payloads.get(0);
for (int strId : map.keySet()) {
switch (strId) {
case R.id.name_attr:
holder.name.setText(map.get(R.id.name_attr));
break;
case R.id.desc_attr:
holder.desc.setText(map.get(R.id.desc_attr));
break;
case R.id.portrait_attr:
holder.portrait.setImageResource(Integer.parseInt(map.get(R.id.portrait_attr)));
break;
}
}
}
}