2016年1月25日月曜日

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

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

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

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

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


2016年1月19日火曜日

TextInputLayout の中の EditText の baseline で揃える方法

「android:baselineAlignedChildIndex の振る舞い」で紹介した方法を使えば、TextInputLayout の中の EditText で位置を揃えることができます。 <?xml version="1.0" encoding="utf-8"?> <LinearLayout xmlns:android="http://schemas.android.com/apk/res/android" xmlns:app="http://schemas.android.com/apk/res-auto" android:layout_width="match_parent" android:layout_height="match_parent" android:orientation="horizontal" android:padding="16dp"> <TextView android:layout_width="wrap_content" android:layout_height="48dp" android:background="#ccccff" android:gravity="center_vertical" android:text="Cupcake" /> <android.support.design.widget.TextInputLayout android:id="@+id/input_layout" android:layout_width="match_parent" android:layout_height="wrap_content" android:layout_marginLeft="16dp" android:background="#ffcccc" android:baselineAlignedChildIndex="0" android:orientation="vertical" app:errorEnabled="true"> <EditText android:layout_width="match_parent" android:layout_height="wrap_content" android:hint="Donuts" /> </android.support.design.widget.TextInputLayout> </LinearLayout>

android:baselineAlignedChildIndex の振る舞い

LinearLayout が baselineAligned されている他のレイアウトの子であるとき、android:baselineAlignedChildIndex で baseline に対応させる子ビューを指定することができます。

まず、以下のようなレイアウトがあった場合 <?xml version="1.0" encoding="utf-8"?> <LinearLayout xmlns:android="http://schemas.android.com/apk/res/android" android:layout_width="match_parent" android:layout_height="match_parent" android:orientation="horizontal"> <TextView android:layout_width="wrap_content" android:layout_height="wrap_content" android:background="#ccccff" android:paddingTop="60dp" android:text="Cupcake" /> <LinearLayout android:layout_width="wrap_content" android:layout_height="wrap_content" android:layout_marginLeft="10dp" android:layout_marginRight="10dp" android:background="#ffcccc" android:orientation="vertical"> <TextView android:layout_width="wrap_content" android:layout_height="wrap_content" android:text="Donuts" /> <TextView android:layout_width="wrap_content" android:layout_height="wrap_content" android:text="Eclare" /> </LinearLayout> </LinearLayout> 実行結果はこのようになります。



LinearLayout はデフォルトで android:baselineAligned="true" がセットされているので、子ビューの LinearLayout に android:baselineAlignedChildIndex="0" を指定すると次ようなレイアウトに変わります。 <?xml version="1.0" encoding="utf-8"?> <LinearLayout xmlns:android="http://schemas.android.com/apk/res/android" ...> <TextView ... /> <LinearLayout ... android:baselineAlignedChildIndex="0"> .... </LinearLayout> </LinearLayout>

android:baselineAlignedChildIndex="1" を指定すると次のようになります。