前言
RecyclerView刷新时,一行代码,简单方便adapter.notifyDataSetChanged()
无脑的刷新了一遍整个RecyclerView,新老数据一样时,本来不用刷新,但却刷新了。而且也不会触发RecyclerView的动画(删除、新增、位移、change动画)。效率较低。
Adapter 对此有四个函数
adapter.notifyItemRangeChanged();
adapter.notifyItemRangeInserted();
adapter.notifyItemRangeRemoved();
adapter.notifyItemMoved();
局部刷新只会刷新指定position的item,解决了上述简单粗暴的刷新方式。
我想要单纯的刷新Item的控件?Adapter还有这样的函数吗?
DiffUtil
一 概述
使用DiffUtil后它用来比较两个数据集,寻找出旧数据集和新数据集的最小变化量。
二 使用步骤:
1.继承DiffUtil.Callback函数
public abstract static class Callback {
public abstract int getOldListSize();
public abstract int getNewListSize();
/**
*新老数据集Item是否是同一个 (一般比较的id)
**/
public abstract boolean areItemsTheSame(int oldItemPosition, int newItemPosition);
/**
* 比较同一个item 的内容是否相同(上面方法返回true 时,会调用此方法)
**/
public abstract boolean areContentsTheSame(int oldItemPosition, int newItemPosition);
/**
* Object就是表示Item改变了哪些内容(上面方法返回false 时,会调用此方法)
**/
@Nullable
public Object getChangePayload(int oldItemPosition, int newItemPosition) {
return null;
}
}
- 计算新旧数据集的差异 (Myers 差分算法 )
DiffUtil.DiffResult diffResult = DiffUtil.calculateDiff(new DiffCallBack());
比较计算差异,用的Myers差分算法(后面介绍)
比较差异的源码,我也看不懂,别着急。(后面了解算法的思想)
private static Snake diffPartial(Callback cb, int startOld, int endOld,
int startNew, int endNew, int[] forward, int[] backward, int kOffset) {
...
for (int d = 0; d <= dLimit; d++) {
for (int k = -d; k <= d; k += 2) {
// find forward path
// we can reach k from k - 1 or k + 1. Check which one is further in the graph
int x;
final boolean removal;
if (k == -d || (k != d && forward[kOffset + k - 1] < forward[kOffset + k + 1])) {
x = forward[kOffset + k + 1];
removal = false;
} else {
x = forward[kOffset + k - 1] + 1;
removal = true;
}
// set y based on x
int y = x - k;
// move diagonal as long as items match
while (x < oldSize && y < newSize
&& cb.areItemsTheSame(startOld + x, startNew + y)) {
x++;
y++;
}
forward[kOffset + k] = x;
for (int k = -d; k <= d; k += 2) {
// find reverse path at k + delta, in reverse
final int backwardK = k + delta;
int x;
final boolean removal;
if (backwardK == d + delta || (backwardK != -d + delta
&& backward[kOffset + backwardK - 1] < backward[kOffset + backwardK + 1])) {
x = backward[kOffset + backwardK - 1];
removal = false;
} else {
x = backward[kOffset + backwardK + 1] - 1;
removal = true;
}
// set y based on x
int y = x - backwardK;
// move diagonal as long as items match
while (x > 0 && y > 0
&& cb.areItemsTheSame(startOld + x - 1, startNew + y - 1)) {
x--;
y--;
}
backward[kOffset + backwardK] = x;
if (!checkInFwd && k + delta >= -d && k + delta <= d) {
if (forward[kOffset + backwardK] >= backward[kOffset + backwardK]) {
Snake outSnake = new Snake();
outSnake.x = backward[kOffset + backwardK];
outSnake.y = outSnake.x - backwardK;
outSnake.size =
forward[kOffset + backwardK] - backward[kOffset + backwardK];
outSnake.removal = removal;
outSnake.reverse = true;
return outSnake;
}
}
}
}
...
}
3.将Adapter传给DiffUtil.DiffResult
diffResult.dispatchUpdatesTo(Adapter);
根据差异情况,自动调用adapter这四个方法
adapter.notifyItemRangeInserted(position, count);
adapter.notifyItemRangeRemoved(position, count);
adapter.notifyItemMoved(fromPosition, toPosition);
adapter.notifyItemRangeChanged(position, count, payload);
Demo:
继承DiffUtil.Callback,重写Callback的方法(根据需要)
@Override
public boolean areItemsTheSame(int oldItemPosition, int newItemPosition) {
UserBean userBeanOld = oldList.get(oldItemPosition);
UserBean userBeanNew = newList.get(newItemPosition);
if (userBeanOld.id == userBeanNew.id){
return true;
}
return false;
}
@Override
public boolean areContentsTheSame(int oldItemPosition, int newItemPosition) {
UserBean userBeanOld = oldList.get(oldItemPosition);
UserBean userBeanNew = newList.get(newItemPosition);
if (!TextUtils.equals(userBeanOld.name,userBeanNew.name)) {
return false;
}
if (!TextUtils.equals(userBeanOld.tel,userBeanNew.tel)) {
return false;
}
return true;
}
@Override
public Object getChangePayload(int oldItemPosition, int newItemPosition) {
UserBean userBeanOld = oldList.get(oldItemPosition);
UserBean userBeanNew = newList.get(newItemPosition);
Bundle bundle = new Bundle();
if (!TextUtils.equals(userBeanOld.name,userBeanNew.name)) {
bundle.putString("name",userBeanNew.name);
}
if (!TextUtils.equals(userBeanOld.tel,userBeanNew.tel)) {
bundle.putString("tel",userBeanNew.tel);
}
return bundle;
}
public void onBindViewHolder(@NonNull RecyclerView.ViewHolder holder, int position, @NonNull List payloads) {
if(payloads == null ||payloads.isEmpty() || payloads.get(0)== null){
super.onBindViewHolder(holder, position, payloads);
}
else {
Bundle bundle =(Bundle)payloads.get(0);
String name = bundle.getString("name");
if(!TextUtils.isEmpty(name)){
((viewholder) holder).NameTextView.setText(name);
}
String tel = bundle.getString("tel");
if(!TextUtils.isEmpty(tel)){
((viewholder) holder).TelTextView.setText(tel);
}
}
}
DiffUtilsCallbackDemo diffUtilsCallbackDemo= new DiffUtilsCallbackDemo();
diffUtilsCallbackDemo.setOldList(oldList);
diffUtilsCallbackDemo.setNewList(newList);
DiffUtil.DiffResult diffResult= DiffUtil.calculateDiff(diffUtilsCallbackDemo);
diffResult.dispatchUpdatesTo(myAdapter);
计算的时间:
<li>100 items and 10 modifications: avg: 0.39 ms, median: 0.35 ms
* <li>100 items and 100 modifications: 3.82 ms, median: 3.75 ms
* <li>100 items and 100 modifications without moves: 2.09 ms, median: 2.06 ms
* <li>1000 items and 50 modifications: avg: 4.67 ms, median: 4.59 ms
* <li>1000 items and 50 modifications without moves: avg: 3.59 ms, median: 3.50 ms
* <li>1000 items and 200 modifications: 27.07 ms, median: 26.92 ms
* <li>1000 items and 200 modifications without moves: 13.54 ms, median: 13.36 ms
一般我们不在主线程计算数据,如果在主线程中计算的时间过长,可能出现ANR。因此就有了AsyncListDiffer。
AsyncListDiffer
使用步骤:
1.自实现DiffUtil.ItemCallback,给出item差异性计算条件
2.使用submitList()更新数据,并刷新ui
demo:
public class AsyncCallback extends DiffUtil.ItemCallback<UserBean> {
@Override
public boolean areItemsTheSame(@NonNull UserBean oldItem, @NonNull UserBean newItem) {
return oldItem.id == newItem.id;
}
@Override
public boolean areContentsTheSame(@NonNull UserBean userBeanOld, @NonNull UserBean userBeanNew) {
if (!TextUtils.equals(userBeanOld.name,userBeanNew.name)) {
return false;
}
if (!TextUtils.equals(userBeanOld.tel,userBeanNew.tel)) {
return false;
}
return true;
}
@Override
public Object getChangePayload(@NonNull UserBean userBeanOld, @NonNull UserBean userBeanNew) {
Bundle bundle = new Bundle();
if (!TextUtils.equals(userBeanOld.name,userBeanNew.name)) {
bundle.putString("name",userBeanNew.name);
}
if (!TextUtils.equals(userBeanOld.tel,userBeanNew.tel)) {
bundle.putString("tel",userBeanNew.tel);
}
return bundle;
}
}
AsyncListDiffer内部自动维护数据集List
submitList()源码:
@SuppressWarnings("WeakerAccess")
public void submitList(@Nullable final List<T> newList,
@Nullable final Runnable commitCallback) {
// incrementing generation means any currently-running diffs are discarded when they finish
final int runGeneration = ++mMaxScheduledGeneration;
// 首先判断 newList 与 AsyncListDiffer 中缓存的数据集 mList 是否为同一个地址,如果是的话,直接返回不做任何处理。也就是说,调用 submitList() 方法所传递数据集时,需要new一个新的List。
if (newList == mList) {
if (commitCallback != null) {
commitCallback.run();
}
return;
}
final List<T> previousList = mReadOnlyList;
// 如果新数据集为空,此种情况不需要计算diff
// 判断新数据集newList是否为null。若新数据集newList 为null,将移除所有 Item 的操作并分发给 ListUpdateCallback,让ListUpdateCallback执行onRemoved,直接清空数据即可(mList 置为 null),同时将只读List mReadOnlyList 清空
if (newList == null) {
//noinspection ConstantConditions
int countRemoved = mList.size();
mList = null;
mReadOnlyList = Collections.emptyList();
// notify last, after list is updated
mUpdateCallback.onRemoved(0, countRemoved);
onCurrentListChanged(previousList, commitCallback);
return;
}
// 如果旧数据集为空,此种情况也不需要计算diff
// 直接将新数据添加到旧数据集即可
// 通知item insert 新数据的全部
if (mList == null) {
mList = newList;
mReadOnlyList = Collections.unmodifiableList(newList);
mUpdateCallback.onInserted(0, newList.size());
onCurrentListChanged(previousList, commitCallback);
return;
}
final List<T> oldList = mList;
// 在子线程中计算DiffResult
mConfig.getBackgroundThreadExecutor().execute(new Runnable() {
@Override
public void run() {
final DiffUtil.DiffResult result = DiffUtil.calculateDiff(new DiffUtil.Callback() {
@Override
public int getOldListSize() {
return oldList.size();
}
@Override
public int getNewListSize() {
return newList.size();
}
@Override
public boolean areItemsTheSame(int oldItemPosition, int newItemPosition) {
T oldItem = oldList.get(oldItemPosition);
T newItem = newList.get(newItemPosition);
if (oldItem != null && newItem != null) {
return mConfig.getDiffCallback().areItemsTheSame(oldItem, newItem);
}
// If both items are null we consider them the same.
return oldItem == null && newItem == null;
}
@Override
public boolean areContentsTheSame(int oldItemPosition, int newItemPosition) {
T oldItem = oldList.get(oldItemPosition);
T newItem = newList.get(newItemPosition);
if (oldItem != null && newItem != null) {
return mConfig.getDiffCallback().areContentsTheSame(oldItem, newItem);
}
if (oldItem == null && newItem == null) {
return true;
}
// There is an implementation bug if we reach this point. Per the docs, this
// method should only be invoked when areItemsTheSame returns true. That
// only occurs when both items are non-null or both are null and both of
// those cases are handled above.
throw new AssertionError();
}
@Nullable
@Override
public Object getChangePayload(int oldItemPosition, int newItemPosition) {
T oldItem = oldList.get(oldItemPosition);
T newItem = newList.get(newItemPosition);
if (oldItem != null && newItem != null) {
return mConfig.getDiffCallback().getChangePayload(oldItem, newItem);
}
// There is an implementation bug if we reach this point. Per the docs, this
// method should only be invoked when areItemsTheSame returns true AND
// areContentsTheSame returns false. That only occurs when both items are
// non-null which is the only case handled above.
throw new AssertionError();
}
});
// 主线程中更新UI
mMainThreadExecutor.execute(new Runnable() {
@Override
public void run() {
if (mMaxScheduledGeneration == runGeneration) {
latchList(newList, result, commitCallback);
}
}
});
}
});
}
Myers 差分算法(Myers Difference Algorithm)
Myers差分算法是由Eugene W.Myers在1986年发表的一篇论文中提出
DiffUtil 对比列表item 数据,git diff文件对比都用到了这个算法。
什么是diff?
diff 就是目标文本和源文本之间的差异。也就是将原文本变成目标文本的差异。
比较字符串A 和字符串B diff。
String A = "ABCABBA"
String B = "CBABAC"
一眼看不出来什么不同。那字符串A = 123 和字符串 B = 0123 呢 ? 字符串B比A多了字符0;
比较的依据 :计算最大重合部分
算法中的概念:
snake :一步所走的路径 为 snake。
k : 定义x 轴向右增大,y轴 向下增大,定义 k = x-y;
d : 定义为步数。(斜线不算步数)
对图的解释:
1.X坐标是字符串A,Y坐标是字符串 B;
2.(0,0)走到右下角;
3.下走一步是删除一个 A 里面的元素,右走一步是添加一个 B 里面的元素,斜线箭头是不操作。(尽可能走斜线);
这幅图可以非常简单的抽象成带权重的有向图:
假设横向纵向权值为 1,对角权值是 0,那么只要总和权重最低,这就是我们的最优解。这幅图中的坐标自变量是d和k,表示了在不同d取值以及不同的k取值最终可以到达的坐标。
目的:
然后找到最短的d(走尽可能多的斜线),到达我们要的终点。
我们从坐标 (0, 0) 开始,此时,d=0,k=0,然后逐步增加 d,计算每个 k 值下对应的最优坐标。
因为每一步要么向右(x + 1),要么向下(y + 1),对角线不影响路径长度,所以,当 d=1 时,k 只可能有两个取值,要么是 1,要么是 -1。
当 d=1,k=1 时,最优坐标是 (1, 0)。
当 d=1,k=-1 时,最优坐标是 (0, 1)。
因为 d=1 时,k 要么是 1,要么是 -1,当 d=2 时,表示在 d=1 的基础上再走一步,k 只有三个可能的取值,分别是 -2,0,2。
当 d=2,k=-2 时,最优坐标是 (2, 4)。
当 d=2,k=0 时,最优坐标是 (2, 2)。
当 d=2,k=2 时,最优坐标是 (3, 1)。
以此类推,直到我们找到一个 d 和 k 值,达到最终的目标坐标 (7, 6)。
下图横轴代表 d,纵轴代表 k,中间是最优坐标,从这张图可以清晰的看出,当 d=5,k=1 时,我们到达了目标坐标 (7, 6),因此,”最短的直观的“路径就是 (0, 0) -> (1, 0) -> (3, 1) -> (5, 4) -> (7, 5) -> (7, 6)
对于步数的d的取值范围肯定是大于0小于字符串A于字符串B之和(即A全部删除,B全部添加) 0<= d<=M+N
k = x-y ; 我们可以通过x 求y ,v[]数组里面以k为index,存储最优坐标的x值,取的时候只要知道k值,因为v[k] =x;通过有y =v[k]-k 就可以算出y;
k 结合图形,我们从(0,0)开始,每次只增长一步,向下一步=>k-1,向右一步=>k+1;
第一步:k的极限,k-1<= k <=k+1
第二步:k的极限,k-2<= k <=k+2
…
第d步: k的极限,k-d<= k <= k+d
论文中的伪代码如下:
V[ 1 ] = 0;
//外层循环 d 步数 最多为全部删除,全部添加(N 为A序列长度, M 为B序列长度)
for ( int d = 0 ; d <= N + M ; d++ ){
// k 为斜线集合
for ( int k = -d ; k <= d ; k += 2 ){
// (遵循先删除后添加的原则)
bool down = ( k == -d || ( k != d && V[ k - 1 ] < V[ k + 1 ] ) );
//得到上一个的K值
int kPrev = down ? k + 1 : k - 1;
// 上一步的point(x,y)
int xStart = V[ kPrev ];
int yStart = xStart - kPrev;
//中点
int xMid = down ? xStart : xStart + 1;
int yMid = xMid - k;
//终点
int xEnd = xMid;
int yEnd = yMid;
//斜线数量
int snake = 0;
//遇到斜线,(数据相等),然后,x,y 各加一
while ( xEnd < N && yEnd < M && A[ xEnd ] == B[ yEnd ] ) {
xEnd++;
yEnd++;
snake++; }
// 保存到数组
V[ k ] = xEnd;
//到了最终点(N,M),找到了最短步数的方案 跳出循环
if ( xEnd >= N && yEnd >= M )
}
}
参考链接:
[https://www.codeproject.com/Articles/42279/Investigating-Myers-diff-algorithm-Part-1-of-2#heading0014]
参考链接