2016年1月25日月曜日

RecyclerView の notifyItemChanged() 時のちらつきを止める

RecyclerView には RecyclerView.ItemAnimator として DefaultItemAnimator が最初からセットされています。

RecyclerView.AdapternotifyItemChanged()notifyItemRangeChanged() が呼ばれると、RecyclerView.AdapterDataObserver を通して DefaultItemAnimator の animateChange() が呼ばれます。 ここのコードを見ると
  1. @Override  
  2. public boolean animateChange(ViewHolder oldHolder, ViewHolder newHolder,  
  3.         int fromX, int fromY, int toX, int toY) {  
  4.     if (oldHolder == newHolder) {  
  5.         // Don't know how to run change animations when the same view holder is re-used.  
  6.         // run a move animation to handle position changes.  
  7.         return animateMove(oldHolder, fromX, fromY, toX, toY);  
  8.     }  
  9.     final float prevTranslationX = ViewCompat.getTranslationX(oldHolder.itemView);  
  10.     final float prevTranslationY = ViewCompat.getTranslationY(oldHolder.itemView);  
  11.     final float prevAlpha = ViewCompat.getAlpha(oldHolder.itemView);  
  12.     resetAnimation(oldHolder);  
  13.     int deltaX = (int) (toX - fromX - prevTranslationX);  
  14.     int deltaY = (int) (toY - fromY - prevTranslationY);  
  15.     // recover prev translation state after ending animation  
  16.     ViewCompat.setTranslationX(oldHolder.itemView, prevTranslationX);  
  17.     ViewCompat.setTranslationY(oldHolder.itemView, prevTranslationY);  
  18.     ViewCompat.setAlpha(oldHolder.itemView, prevAlpha);  
  19.     if (newHolder != null) {  
  20.         // carry over translation values  
  21.         resetAnimation(newHolder);  
  22.         ViewCompat.setTranslationX(newHolder.itemView, -deltaX);  
  23.         ViewCompat.setTranslationY(newHolder.itemView, -deltaY);  
  24.         ViewCompat.setAlpha(newHolder.itemView, 0);  
  25.     }  
  26.     mPendingChanges.add(new ChangeInfo(oldHolder, newHolder, fromX, fromY, toX, toY));  
  27.     return true;  
  28. }  
oldHolder と newHolder のインスタンスが同じときは移動のアニメーションだけを行い、異なる場合は移動 + アルファのアニメーションを行っています。
そのためレイアウト上の一部分だけを変更するとき(例えば写真のグリッド上にあるお気に入りマークの状態を更新するなど)に notifyItemChanged() を呼ぶと、アルファの処理が入るのでちらつきます。

これを防ぐにはアルファの処理が入らないパス、つまり newHolder として oldHolder と同じインスタンスが渡されるようになればいいわけです。そもそもレイアウトに変更がないのであればインスタンスを再利用しないのは無駄です。

これを切り替えるのが ItemAnimator の canReuseUpdatedViewHolder() です。 デフォルトでは true つまり再利用するようになっています。 ではどこで false が返るように変わったかというと DefaultItemAnimator の親クラスの SimpleItemAnimator です。
  1. abstract public class SimpleItemAnimator extends RecyclerView.ItemAnimator {  
  2.   
  3.     ...  
  4.     boolean mSupportsChangeAnimations = true;  
  5.   
  6.     @SuppressWarnings("unused")  
  7.     public boolean getSupportsChangeAnimations() {  
  8.         return mSupportsChangeAnimations;  
  9.     }  
  10.   
  11.     public void setSupportsChangeAnimations(boolean supportsChangeAnimations) {  
  12.         mSupportsChangeAnimations = supportsChangeAnimations;  
  13.     }  
  14.   
  15.     @Override  
  16.     public boolean canReuseUpdatedViewHolder(RecyclerView.ViewHolder viewHolder) {  
  17.         return !mSupportsChangeAnimations || viewHolder.isInvalid();  
  18.     }  
  19.   
  20.     ...  
  21. }  
デフォルトでは mSupportsChangeAnimations が true なので viewHolder.isInvalid() が true のときだけ canReuseUpdatedViewHolder() が true を返す(=再利用する)ようになっています。 mSupportsChangeAnimations の値は setSupportsChangeAnimations() で変更できるようになっているので、
  1. ((DefaultItemAnimator) recyclerView.getItemAnimator()).setSupportsChangeAnimations(false);  
のようにすれば notifyItemChanged() 時のアニメーションが移動だけになります。


0 件のコメント:

コメントを投稿