English | 简体中文 | 繁體中文 | Русский язык | Français | Español | Português | Deutsch | 日本語 | 한국어 | Italiano | بالعربية

Android7.0ユーティリティクラス:DiffUtilの詳細

第1章 概説

DiffUtilはsupport-v7:24.2.0に新しいツールクラスがあり、それを使って二つのデータセットを比較し、古いデータセット-》新しいデータセットの最小変更量。

データセットについて言えば、皆さんは誰に関連しているかご存知でしょう、それは私の大好きなRecyclerViewです。

就这几天の使用経験から見て、それが最大の役割はRecyclerViewの更新時、無腦のmAdapter.notifyDataSetChanged()を使わないことです。

以前無腦のmAdapter.notifyDataSetChanged()には二つの欠点がありました:

1.RecyclerViewのアニメーション(削除、追加、移動、変更アニメーション)をトリガーしません。

2.パフォーマンスが低いです。なぜなら、RecyclerView全体を無思考にリフレッシュしているからです。極端な場合、新旧データセットが完全に同じであれば、効率は最も低いです。

DiffUtilを使用した後、以下のように変更されます:

DiffUtil.DiffResult diffResult = DiffUtil.calculateDiff(new DiffCallBack(mDatas, newDatas), true);
diffResult.dispatchUpdatesTo(mAdapter);

それは自動的に新旧データセットの差異を計算し、差異状況に応じて以下の4つのメソッドを自動的に呼び出します。

adapter.notifyItemRangeInserted(position, count);
adapter.notifyItemRangeRemoved(position, count);
adapter.notifyItemMoved(fromPosition, toPosition);
adapter.notifyItemRangeChanged(position, count, payload);

明らかに、これらの4つのメソッドは実行時、RecyclerViewのアニメーションが伴い、すべてが定向リフレッシュメソッドであり、リフレッシュ効率が急上昇します。
従来通り、まず図を示します。

図1はnotifyDataSetChanged()を無思考に使った效果图です。リフレッシュのインタラクションが生硬で、アイテムが突然ある場所に現れることが見られます:

図2はDiffUtilsを使用した效果图です。最も明らかなのは、アイテムの挿入や移動アニメーションがあります:

GIFは少し粗いですが、最後に示したデモをダウンロードして実行すると、より良い効果が得られます。

この記事には以下の内容が含まれますが、それに限りません:

1 まずDiffUtilの簡単な使い方を紹介し、リフレッシュ時の「増量更新」効果を実現します。(「増量更新」は私の呼び名です)
2 DiffUtilの高度な使い方、アイテムの位置(position)が変わらない場合に、内容(data)のみが変更されたアイテムに対して、部分更新(公式ではPartial bindと呼ばれる)を完了します。
3 RecyclerView.Adapterにpublic void onBindViewHolder(VH holder, int position, List<Object> payloads)メソッドがあることを知り、それを理解します。
4 DiffResultをサブスレッドで計算し、RecyclerViewをメインスレッドでリフレッシュします。
5 一部の人々が嫌がるnotifyItemChanged()によるアイテムに白光が一瞬に現れるアニメーションをどう除くか。
6 DiffUtilの部分クラス、メソッドの公式コメントの日本語化

二 DiffUtilの簡単な使い方

前述のように、DiffUtilは、RecyclerViewをリフレッシュする際に新旧データセットの差異を計算し、RecyclerView.Adapterのリフレッシュメソッドを自動的に呼び出して、効率的なリフレッシュとアイテムアニメーション効果を提供するために役立ちます。

それでは、それを学ぶ前に準備を整え、まずは普通の若者版を書いて、notifyDataSetChanged()を無思考に使ったデモを作成します。

1 普通のJavaBeanですが、cloneメソッドを実装しており、デモ用にのみ使用されており、実際のプロジェクトでは必要ありません。なぜなら、リフレッシュ時には、データはすべてネットワークから取得されるからです。

class TestBean implements Cloneable {
 private String name;
 private String desc;
 ....//get set方法省略
 //仅写DEMO 用 实现克隆方法
 @Override
 public TestBean clone() throws CloneNotSupportedException {
  TestBean bean = null;
  try {
   bean = (TestBean) super.clone();
  } catch (CloneNotSupportedException e) {
   e.printStackTrace();
  }
  return bean;
 }

2 実現了一个普通的RecyclerView.Adapter。

public class DiffAdapter extends RecyclerView.Adapter<DiffAdapter.DiffVH> {
 private final static String TAG = "zxt";
 private List<TestBean> mDatas;
 private Context mContext;
 private LayoutInflater mInflater;
 public DiffAdapter(Context mContext, List<TestBean> mDatas) {
  this.mContext = mContext;
  this.mDatas = mDatas;
  mInflater = LayoutInflater.from(mContext);
 }
 public void setDatas(List<TestBean> mDatas) {
  this.mDatas = mDatas;
 }
 @Override
 public DiffVH onCreateViewHolder(ViewGroup parent, int viewType) {
  return new DiffVH(mInflater.inflate(R.layout.item_diff, parent, false));
 }
 @Override
 public void onBindViewHolder(final DiffVH holder, final int position) {
  TestBean bean = mDatas.get(position);
  holder.tv1.setText(bean.getName());
  holder.tv2.setText(bean.getDesc());
  holder.iv.setImageResource(bean.getPic());
 }
 @Override
 public int getItemCount() {
  return mDatas != null63; mDatas.size() : 0;
 }
 class DiffVH extends RecyclerView.ViewHolder {
  TextView tv1, tv2;
  ImageView iv;
  public DiffVH(View itemView) {
   super(itemView);
   tv1 = (TextView) itemView.findViewById(R.id.tv1);
   tv2 = (TextView) itemView.findViewById(R.id.tv2);
   iv = (ImageView) itemView.findViewById(R.id.iv);
  }
 }
}

3 アクティビティコード:

public class MainActivity extends AppCompatActivity {
 private List<TestBean> mDatas;
 private RecyclerView mRv;
 private DiffAdapter mAdapter;
 @Override
 protected void onCreate(Bundle savedInstanceState) {
  super.onCreate(savedInstanceState);
  setContentView(R.layout.activity_main);
  initData();
  mRv = (RecyclerView) findViewById(R.id.rv);
  mRv.setLayoutManager(new LinearLayoutManager(this));
  mAdapter = new DiffAdapter(this, mDatas);
  mRv.setAdapter(mAdapter);
 }
 private void initData() {
  mDatas = new ArrayList<>();
  mDatas.add(new TestBean("張旭童"1"Android", R.drawable.pic1));
  mDatas.add(new TestBean("張旭童"2"Java", R.drawable.pic2));
  mDatas.add(new TestBean("張旭童"3"背锅", R.drawable.pic3));
  mDatas.add(new TestBean("張旭童"4"手撕製品", R.drawable.pic4));
  mDatas.add(new TestBean("張旭童"5"手撕テスト", R.drawable.pic5));
 }
 /**
  * シミュレーションリフレッシュ操作
  *
  * @param view
  */
 public void onRefresh(View view) {
  try {
   List<TestBean> newDatas = new ArrayList<>();
   for (TestBean bean : mDatas) {
    newDatas.add(bean.clone());//古いデータをクローンして、リフレッシュ操作をシミュレートします
   }
   newDatas.add(new TestBean("趙子龍", "帅", R.drawable.pic6));//データの追加のシミュレーション
   newDatas.get(0).setDesc("Android+");
   newDatas.get(0).setPic(R.drawable.pic7);//データの変更のシミュレーション
   TestBean testBean = newDatas.get(1);//データシフトのシミュレーション
   newDatas.remove(testBean);
   newDatas.add(testBean);
   //新しいデータをAdapterに渡すことを忘れないでください
   mDatas = newDatas;
   mAdapter.setDatas(mDatas);
   mAdapter.notifyDataSetChanged();//以前はほとんどの場合、このようにしかできませんでした
  } catch (CloneNotSupportedException e) {
   e.printStackTrace();
  }
 }
}

非常にシンプルですが、新しいデータソースnewDatasを構築する際には、古いデータソースmDatasを巡回し、各dataのclone()メソッドを呼び出して、新旧データソースがデータが一致していることを確認し、メモリアドレス(ポインタ)が異なることを確保します。これにより、後でnewDatas内の値を変更するときに、mDatas内の値が一緒に変更されないようにします。

4 activity_main.xml いくつかの幅高さコードを削除して、RecyclerViewとButtonを使用してリフレッシュをシミュレートしています。:

<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
>
 <android.support.v7.widget.RecyclerView
  android:id="@"+id/rv" />
 <Button
  android:id="@"+id/btnRefresh"
  android:layout_alignParentRight="true"
  android:onClick="onRefresh"
  android:text="模拟刷新" />
</RelativeLayout>

上記は普通の青年が簡単に書ける、通知を無視するnotifyDataSetChanged()のdemoで、実行結果は第一节の図1の通りです。
しかし、私たちはすべて文艺青年になりたいと思っています、だから

以下では、DiffUtilの簡単な使用方法について説明します。追加でクラスを1つ作成する必要があります。

文艺青年になるために、DiffUtil.Callbackを継承したクラスを実現する必要があります。その4つのabstractメソッドを実装します。
このクラスの名前はCallbackですが、DiffUtil.Callbackを以下のように理解する方が適切です:新旧Itemが等しいか比較するための契約(Contract)、規則(Rule)を定義するクラスです。

DiffUtil.Callback抽象クラスは以下の通りです:

 public abstract static class Callback {
  public abstract int getOldListSize();//古いデータセットのサイズ
  public abstract int getNewListSize();//新しいデータセットのサイズ
  public abstract boolean areItemsTheSame(int oldItemPosition, int newItemPosition);//新旧データセットが同じポジションのItemがオブジェクトかどうか?(内容が異なる可能性がありますが、ここでtrueを返した場合、以下のメソッドが呼び出されます)
  public abstract boolean areContentsTheSame(int oldItemPosition, int newItemPosition);//このメソッドは上記のメソッドがtrueを返した場合にのみ呼び出されます。私の理解では、notifyItemRangeChanged()のみが呼び出されますが、itemの内容が変更されたかどうかを判断します
  //このメソッドはDiffUtilの高度な使用法で使用されますが、ここでは触れません
  @Nullable
  public Object getChangePayload(int oldItemPosition, int newItemPosition) {
   return null;
  }
 }

  このDemoではDiffUtil.Callbackを実現し、核心メソッドには中国語と英語の両方のコメントがあります(簡単に言えば、公式の英語コメントを翻訳して、皆さんがよりよく理解できるようにしています)。

/**
 * 紹介:核心クラス、新旧Itemが等しいか判断するために使用されます
 * 作者:zhangxutong
 * メール:[email protected]
 * 時間: 2016/9/12.
 */
public class DiffCallBack extends DiffUtil.Callback {
 private List<TestBean> mOldDatas, mNewDatas;//名前を見る
 public DiffCallBack(List<TestBean> mOldDatas, List<TestBean> mNewDatas) {
  this.mOldDatas = mOldDatas;
  this.mNewDatas = mNewDatas;
 }
 //古いデータセットのサイズ
 @Override
 public int getOldListSize() {
  return mOldDatas != null ? mOldDatas.size() : 0;
 }
 //新しいデータセットのサイズ
 @Override
 public int getNewListSize() {
  return mNewDatas != null ? mNewDatas.size() : 0;
 }
 /**
  * DiffUtilが呼び出され、二つのオブジェクトが同じアイテムを表しているかを決定するために使用されます。
  * DiffUtilが呼び出され、二つのオブジェクトが同じアイテムであるかを判断するために使用されます。
  * 例えば、あなたのアイテムがユニークなidを持っている場合、このメソッドはそのidの等価性をチェックすべきです。
  * 例えば、あなたのアイテムがユニークなidフィールドを持っている場合、このメソッドはidの等価性を判断します。
  * この例では、nameフィールドが一致しているかどうかを判断しています。
  *
  * @param oldItemPosition 古いリスト内のitemの位置
  * @param newItemPosition 新しいリスト内のitemの位置
  * @return 二つのアイテムが同じオブジェクトを表している場合はTrue、異なる場合はfalse。
  */
 @Override
 public boolean areItemsTheSame(int oldItemPosition, int newItemPosition) {
  return mOldDatas.get(oldItemPosition).getName().equals(mNewDatas.get(newItemPosition).getName());
 }
 /**
  * DiffUtilが、二つのアイテムが同じデータを持っているかを確認したいときに呼び出されます。
  * DiffUtilが呼び出し、2つのアイテムが同じデータを含んでいるかを確認するために使用されます。
  * DiffUtilは、この情報を使用してアイテムの内容が変更されたかどうかを検出します。
  * DiffUtilは、この情報を使用して現在のアイテムの内容が変更されたかどうかを検出します。
  * DiffUtilは、このメソッドを使用して{@link Object#equals(Object)}の代わりに等価性を確認します。
  * DiffUtilは、このメソッドを使用してequalsメソッドの代わりに等価性を確認します。
  * あなたのUIに応じてその動作を変更できるようにします。
  * したがって、あなたのUIに応じてその返り値を変更することができます。
  * 例えば、DiffUtilとRecyclerView.Adapterを組み合わせて使用する場合、
  * {@link android.support.v7.widget.RecyclerView.Adapter RecyclerView.Adapter}, あなたは
  * アイテムの視覚的な表現が同じかどうかを返します。
  * 例えば、RecyclerView.AdapterとDiffUtilを組み合わせて使用する場合、アイテムの視覚的な表現が同じかどうかを返す必要があります。
  * このメソッドは{@link #areItemsTheSame(int, int)}が返した場合にのみ呼び出されます。
  * {@code true} for these items.
  * このメソッドはareItemsTheSame()がtrueを返したときのみ呼び出されます。
  * @param oldItemPosition 古いリスト内のitemの位置
  * @param newItemPosition The position of the item in the new list which replaces the
  *      oldItem
  * @return True if the contents of the items are the same or false if they are different.
  */
 @Override
 public boolean areContentsTheSame(int oldItemPosition, int newItemPosition) {}}
  TestBean beanOld = mOldDatas.get(oldItemPosition);
  TestBean beanNew = mNewDatas.get(newItemPosition);
  if (!beanOld.getDesc().equals(beanNew.getDesc())) {
   return false;//内容が異なる場合は、falseを返します。
  }
  if (beanOld.getPic() != beanNew.getPic()) {
   return false;//内容が異なる場合は、falseを返します。
  }
  return true; //デフォルトでは、二つのdataの内容は同じです。
 }

詳細なコメントを書いています。+シンプルなコードで、一目でわかります。

その後、使用時に以前に書いたnotifyDatasetChanged()メソッドをコメントアウトして、以下のコードに置き換えてください:

//文艺青年の新しいお気に入り
//DiffUtil.calculateDiff()メソッドを使用して、ルールDiffUtil.Callbackオブジェクトと、itemの移動を検出するかどうかのboolean変数を入力して、DiffUtil.DiffResultオブジェクトを取得します。
DiffUtil.DiffResult diffResult = DiffUtil.calculateDiff(new DiffCallBack(mDatas, newDatas), true);
//DiffUtil.DiffResultオブジェクトのdispatchUpdatesTo()メソッドを使用して、RecyclerViewのAdapterに簡単に渡せます。文艺青年になるのはこれで簡単です。
diffResult.dispatchUpdatesTo(mAdapter);
//新しいデータをAdapterに渡すことを忘れないでください
mDatas = newDatas;
mAdapter.setDatas(mDatas);

説明:

ステップ一

newDatasをAdapterに設定する前に、まずDiffUtil.calculateDiff()メソッドを呼び出してください。新しい古いデータセットを変換して最小の更新セットを計算するのは、

DiffUtil.DiffResultオブジェクト。
DiffUtil.calculateDiff()メソッドの定義は以下の通りです:
第一个引数はDiffUtil.Callbackオブジェクトです。
第二个引数はItemの移動を検出するかどうかを表すもので、falseにするとアルゴリズムの効率が高くなります。必要に応じて設定してください。ここではtrueにしています。

public static DiffResult calculateDiff(Callback cb, boolean detectMoves)

ステップ2

次に、DiffUtil.DiffResultオブジェクトのdispatchUpdatesTo()メソッドを使用して、RecyclerViewのAdapterに渡し、普通の若者に使われるmAdapter.notifyDataSetChanged()メソッドを置き換えます。

ソースコードを確認すると、このメソッドの内部では、状況に応じてadapterの4つの特定の更新メソッドを呼び出していることがわかります。

 public void dispatchUpdatesTo(final RecyclerView.Adapter adapter) {
   dispatchUpdatesTo(new ListUpdateCallback() {
    @Override
    public void onInserted(int position, int count) {
     adapter.notifyItemRangeInserted(position, count);
    }
    @Override
    public void onRemoved(int position, int count) {
     adapter.notifyItemRangeRemoved(position, count);
    }
    @Override
    public void onMoved(int fromPosition, int toPosition) {
     adapter.notifyItemMoved(fromPosition, toPosition);
    }
    @Override
    public void onChanged(int position, int count, Object payload) {
     adapter.notifyItemRangeChanged(position, count, payload);
    }
   });
  }

まとめ:

だから、DiffUtilはRecyclerViewと組み合わせるだけでなく、自分自身でListUpdateCallbackインターフェースの4つのメソッドを実装して何かすることもできます。(私の思い付きで一つ選ぶと、自分のプロジェクトに組み込める九宮格コントロール?それとも、先日の記事で書いたNestFullListViewを最適化する?ちょっとした紹介、ListView、RecyclerView、ScrollViewに嵌められたListViewの相対的に美しい解決策:http://blog.csdn.net/zxt0601/article/details/52494665)

これで、私たちは文芸青年に進化しました、実行結果は最初の図の2番目の図とほぼ同じです、

唯一異なるのは、この時adapter.notifyItemRangeChanged()はItem白光一闪の更新アニメーションがあることです(本文のDemoのpositionが0のitem)。この一闪するアニメーションは好みの問題で、どちらでも重要ではありません。

なぜなら、第三節のDiffUtilの使い方を学んだら、ItemChangeアニメーションが好きかどうかに関わらず、すべて風に乗って消えます。(公式バグの可能性があります)
効果は最初の図の2番目の図です、私たちのitem0は画像とテキストが変わったけど、この変更にはどんなアニメーションも伴いませんでした。

文芸青年の文芸青年への道を歩んでみましょう。

DiffUtilの高度な使用法

理論:

高度な使用法は、たった2つのメソッドに関連しています。
DiffUtil.Callbackの
public Object getChangePayload(int oldItemPosition, int newItemPosition)メソッド、
が返すObjectは、Itemがどのような内容を変更したかを示しています。

RecyclerView.Adapterの
public void onBindViewHolder(VH holder, int position, List<Object> payloads)メソッド、
定期的なリフレッシュを完了します。(文芸青年の文芸青年、文芸青。)
ポイントを打ち出します、これは新しいメソッドで、注意して3つの引数があります。前2つの引数は馴染みがあるけど、3番目の引数はgetChangePayload()で返されるObjectを含んでいます。

それでは、まずこのメソッドがどのような神聖なものかを見てみましょう:

v7-24.2.0のソースコードでは、以下のようになっています:

 /**
   * RecyclerViewが指定されたポジションのデータを表示するために呼び出されます。このメソッド
   * should update the contents of the {@link ViewHolder#itemView} to reflect the item at
   * 指定された位置。
   * <p>
   * 注:{@link android.widget.ListView}とは異なり、RecyclerViewはこのメソッドを呼び出しません。
   * アイテム自体が変更されていない場合を除いて、データセット内のアイテムの位置が変更された場合に再度
   * 無効化された場合や新しい位置が決定できない場合があります。このため、
   * use the <code>position</コード>パラメータを使用して、関連するデータアイテムを取得する中で
   * このメソッドを使用して、それをコピーしておくべきではありません。後でアイテムの位置が必要な場合は、
   * (例えば、クリックリスナーの中で)use {@link ViewHolder#getAdapterPosition()}を使用して、
   * 更新されたAdapter位置を持っています。
   * <p>
   * 部分バインド対完全バインド:
   * <p>
   * payloadsパラメータは{@link #notifyItemChanged(int, Object)}からもたらされるマージリストです。
   * {@link #notifyItemRangeChanged(int, int, Object)}を使用すると、もしpayloadsリストが空でない場合、
   * ViewHolderは現在古いデータにバインドされており、Adapterが効率的な部分再バインドを実行する可能性があります。
   * payload情報を使用して更新することはできません。payloadが空の場合、アダプターは完全なバインドを実行する必要があります。
   * アダプターは、notifyメソッドに渡されたpayloadが受信されることを仮定していないべきです。
   * onBindViewHolder()に。例えば、ビューがスクリーンにアタッチされていない場合、
   * notifyItemChange()で通知されたpayloadの中身は単にドロップされます。
   *
   * @param holder 更新されるべきViewHolderであり、その内容を表すべきViewHolderです。
   *    データセット内の指定された位置のアイテム。
   * @param position アダプターのデータセット内のアイテムの位置。
   * @param payloads ノン-合併パイロードのリストはnullです。完全なバインドが必要な場合は空のリストでかまいません。
   *     更新。
   */
  public void onBindViewHolder(VH holder, int position, List<Object> payloads) {
   onBindViewHolder(holder, position);
  }

実際には、内部ではonBindViewHolder(holder, position)という二つの引数を持つメソッドだけが呼び出されています。(話外れですが、私のNestFullListViewのAdapterも少し似た書き方をしています。Googleの神様に一歩近づいた気分です)

ここで初めてわかったことですが、実際のonBindのエントリーポイントはこのメソッドであり、これはonCreateViewHolderと対応するメソッドです。
ソースコードを少し下にスクロールすると、public final void bindViewHolder(VH holder, int position)というメソッドがあることがわかります。その内部では、三つの引数を持つonBindViewHolderが呼び出されます。
RecyclerView.Adapterについても、一言二言で説明することはできません。(実際にはここまでしか理解していません)
それでは、脱線しないように、私たちの三引数のonBindViewHolder(VH holder, int position, List<Object> payloads)に戻りましょう。このメソッドのヘッダには多くの英語のコメントがあります。これらの英語のコメントを読むことは、メソッドの理解に非常に役立ちます。だから、私はそれを翻訳してみました。

翻訳:

RecyclerViewが呼び出して、指定された位置にデータを表示するために使用されます。
このメソッドは、ViewHolder内のItemViewの内容を更新して、指定された位置のItem(の変更)を反映するべきです。
注意してください、ListViewとは異なり、位置のitemのデータセットが変更された場合、RecyclerViewは再びこのメソッドを呼び出しません。ただし、item自体が無効(invalidated)または新しい位置が特定できない場合を除きます。
この理由から、このメソッドでは、positionパラメータを使用して関連するデータitemを取得するだけでなく、このデータitemのコピーを保持すべきではありません。
もし後でこのitemのpositionが必要な場合、例えばclickListenerを設定する場合、ViewHolder.getAdapterPosition()を使用すべきで、それは更新後の位置を提供します。
(二丁目の私はここで気づいたが、これは二引数のonBindViewHolderメソッドについて説明しているんだ)
以下はこの三引数メソッドのユニークな部分です:)
**部分(partial)バインド**vs完全(full)バインド
payloadsパラメータは、(notifyItemChanged(int, Object)またはnotifyItemRangeChanged(int, int, Object))から得られるマージリストです。
payloadsリストが空でない場合、現在の旧データをバインドしたViewHolderとAdapterは、payloadのデータを使用して一度効率的な部分更新を行うことができます。
payloadが空の場合、Adapterは一度完全なバインド(二引数のメソッドを呼び出す)を行わなければならない。
Adapterは、notifyxxxx通知メソッドで渡されたpayloadが、onBindViewHolder()メソッドで受け取られることを仮定(当然のことと思って)はならない。(この文の翻訳が難しい QAQ 例を参照してください)
例えば、Viewがスクリーンにattachedされていない場合、notifyItemChange()から来るこのpayloadは単純に捨ててしまって構いません。
payloadsオブジェクトはnullではありませんが、空(empty)である可能性があります。この場合、完全なバインドが必要です(したがって、メソッド内でisEmptyを判断すればよく、空判定を繰り返す必要はありません)。
著者談:この方法は非常に効率的な方法です。私は低効率な翻訳者ですが、40+分。ようやく重要な部分が強調表示されていることに気づきました。

実戦:

ここまで話を進めると、実際の使用は非常に簡単です:
まずgetChangePayload()メソッドの使用方法を見てみましょう。中英双语のコメントも添付されています

  

 /**
  * When {@link #areItemsTheSame(int, int)} returns {@code true} for two items and
  * ②の変更がfalseを返した場合、DiffUtil
  * calls this method to get a payload about the change.
  * 
  * 当{@link #areItemsTheSame(int, int)}がtrueを返し、且つ{@link #areContentsTheSame(int, int)}がfalseを返した場合、DiffUtilsはこのメソッドをコールバックします
  * このItem(変更されたものが何か)のpayloadを取得するために
  * 
  * 例えば、DiffUtilと{@link RecyclerView}を使用している場合、以下のフィールドを返すことができます
  * 変更された特定のフィールドとあなたの
  * {@link android.support.v7.widget.RecyclerView.ItemAnimator ItemAnimator} それを使用して
  * information to run the correct animation.
  * 
  * 例えば、RecyclerViewとDiffUtilsを組み合わせて使用する場合、変更されたItemの変更されたフィールドを返すことができます
  * {@link android.support.v7.widget.RecyclerView.ItemAnimator ItemAnimator} どのような情報を使って正しいアニメーションを実行できます
  * 
  * デフォルトの実装は{@code null}を返します。  * デフォルトの実装はnullを返します。
  *
  * @param oldItemPosition 古いリスト内のitemの位置
  * @param newItemPosition 新しいリスト内のitemの位置
  * @return 二つのitemの変更を表すpayloadオブジェクト
  * 新旧itemの変更内容を表すpayloadオブジェクトを返します。
  */
 @Nullable
 @Override
 public Object getChangePayload(int oldItemPosition, int newItemPosition) {
  //このメソッドを実装することで、文艺青年の中の文艺青年になることができます
  // 特定の更新を行うための定向刷新
  // 効率が最高です
  //ただし、ItemChangeの白光一闪のアニメーションはありません。(私もあまり重要だとは思いません)
  TestBean oldBean = mOldDatas.get(oldItemPosition);
  TestBean newBean = mNewDatas.get(newItemPosition);
  //ここでは、核心のフィールドを比較する必要はありません。必ず一致します。
  Bundle payload = new Bundle();
  if (!oldBean.getDesc().equals(newBean.getDesc())) {
   payload.putString("KEY_DESC", newBean.getDesc());
  }
  if (oldBean.getPic() != newBean.getPic()) {
   payload.putInt("KEY_PIC", newBean.getPic());
  }
  if (payload.size() == 0)//変更がない場合、空を渡します
   return null;
  return payload;//
 }

簡単に言えば、このメソッドはObject型のpayloadを返します。そのpayloadには、特定のitemの変更された内容が含まれています。
ここでは、これらの変更をBundleで保存しています。

Adapter内で三引数のonBindViewHolderを以下のようにオーバーライドします:

 @Override
 public void onBindViewHolder(DiffVH holder, int position, List<Object> payloads) {
  if (payloads.isEmpty()) {
   onBindViewHolder(holder, position);
  } else {
   //文艺青年の文青
   Bundle payload = (Bundle) payloads.get(0);
   TestBean bean = mDatas.get(position);
   for (String key : payload.keySet()) {
    switch (key) {
     case "KEY_DESC":
      //ここではpayloadのデータを使用できますが、dataも新しいので、それを使用することもできます。
      holder.tv2.setText(bean.getDesc());
      break;
     case "KEY_PIC":
      holder.iv.setImageResource(payload.getInt(key));
      break;
     default:
      break;
    }
   }
  }
 }

ここで渡されるpayloadsはListであり、コメントから分かるようにnullではないため、emptyかどうかを判断します。
emptyの場合、二つの引数の関数を呼び出して、一度Full Bindを実行します。
emptyでない場合、partial bindを実行します。
getChangePayloadメソッドで返したpayloadの下标0からpayloadを取得し、payloadのkeyを巡回して検索します。payloadに対応する変更が含まれている場合、それを取り出してItemViewに更新します。
ここでは、mDatasから取得するのも最新のデータソースのデータでも良いので、payloadのデータまたは新しいデータのデータを使用して更新することができます。

これで、RecyclerViewをリフレッシュする方法、特に文艺青年たちが最も文艺的な方法を理解しました。

4. 子スレッドでDiffUtilを使用する

DiffUtilのソースコードのヘッダコメントには、DiffUtilに関する情報が記述されています。
DiffUtilはEugene W. Myersのdifference algorithmを使用していますが、このアルゴリズムはitemの移動を検出することができません。したがって、Googleはその上で改良し、移動するitemの検出をサポートしましたが、移動するitemの検出はパフォーマンス消費が大きくなります。
ある1000件のデータ、200の変更時、このアルゴリズムの所要時間:
移動検出をオンにした場合:平均値:27.07ms、中央値:26.92ms。
移動検出をオフにした場合:平均値:13.54ms、中央値:13.36ms。
興味がある場合は、ソースコードのヘッダーのコメントを読んでみてください。特に役立つのは、以下の部分です:
リストが大きい場合、DiffResultを計算する時間が長くなるため、DiffResultの取得プロセスをサブスレッドに移動し、RecyclerViewを主スレッドで更新する必要があります。

ここでは、HandlerとDiffUtilを組み合わせて使用しています:

以下のコードです:

 private static final int H_CODE_UPDATE = 1;
 private List<TestBean> mNewDatas;//一時的にnewListを保存する変数を追加します
 private Handler mHandler = new Handler() {
  @Override
  public void handleMessage(Message msg) {
   switch (msg.what) {
    case H_CODE_UPDATE:
     //Resultを取り出します
     DiffUtil.DiffResult diffResult = (DiffUtil.DiffResult) msg.obj;
     diffResult.dispatchUpdatesTo(mAdapter);
     //新しいデータをAdapterに渡すことを忘れないでください
     mDatas = mNewDatas;
     mAdapter.setDatas(mDatas);
     break;
   }
  }
 });
   new Thread(new Runnable() {
    @Override
    public void run() {
     //DiffResultの計算はサブスレッドに置きます
     DiffUtil.DiffResult diffResult = DiffUtil.calculateDiff(new DiffCallBack(mDatas, mNewDatas), true);
     Message message = mHandler.obtainMessage(H_CODE_UPDATE);
     message.obj = diffResult;//objにDiffResultを保存します
     message.sendToTarget();
    }
   }).start();

これは単純なHandlerの使用であり、詳述は省略します。

5つのまとめと他

1 実際にはこの記事のコード量は少なく、Demoをダウンロードして見てください、全部で4つのクラスです。
しかし、意識せずにまた長くなってしまいました。主にソースコードのコメントの翻訳が含まれており、皆さんがより良い理解を得るのに役立ちます。

2 DiffUtilは下拉リフレッシュなどのシーンに適していますが、
更新の効率が向上し、アニメーションが付いており、さらに~あなたが考えなくても済みます。
ただし、削除やいいねなどの単純な操作をする場合は、DiffUtilsを使う必要はありません。自分でpositionを記録し、positionがスクリーン内にあるかどうかを判断して、いくつかの特定のリフレッシュメソッドを呼び出すだけで済みます。

3 実際にはDiffUtilはRecyclerView.Adapterと組み合わせるだけでなく、
私たちはListUpdateCallbackインターフェースを実装し、DIffUtilを使って新旧データセットの最小差分セットを見つけて、さらに多くのことを行うことができます。

4 DEMOを作成する際の注意点は、比較に使う新旧データセットがArrayListだけでなく、各dataも異なる必要があります。そうしないとchangedがトリガーされません。
実際のプロジェクトでは発生しにくいです、なぜなら新しいデータは通常ネットワークから来るからです。

5 今日が中秋節の最終日ですが、私たちの会社は仕事を始めました!!!怒りを抑えきれず、DiffUtilを怒って書きました、DiffUtilを使わなくても、私たちの会社と他の会社の違いを簡単に比較できます。QAQ、そして今日は状態が悪く、8時間がかかって完成しました。この記事が短編文集中に選ばれると期待していましたが、意外に長いです。忍耐力がない場合はDEMOをダウンロードして見てください、コード量は少なく、使いやすく感じられます。

githubのリンク:
https://github.com/mcxtzhang/DiffUtils

これがAndroidの7DiffUtilのツールクラスの情報整理、後日関連資料を追加していく、皆様のサポートに感謝します!

おすすめ