2016年2月2日火曜日

Theme.NoDisplay は onCreate() で finish() する Activity 以外で使うと Android 6.0 でクラッシュする

Issue 2353: Activity crash with @android:style/Theme.NoDisplay : android-developer-preview

Android 6.0 から onCreate() で finish() していない Activity に Theme.NoDisplay をセットすると startActivity() したときにクラッシュします。 Theme.Translucent や Theme.Translucent.NoTitleBar はクラッシュしません。クラッシュ時の Exception は IllegalStateException で、メッセージは did not call finish() prior to onResume() completing です。

この原因となるテーマ属性は windowNoDisplay です。デフォルトは false ですが、Theme.NoDisplay では true がセットされています。つまり
onCreate() で finish() していない Activity のテーマで windowNoDisplay が true だとクラッシュします。

次のように onCreate() で finish() していればクラッシュしません。 public class NoDisplayActivity extends Activity { @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); finish(); } } <activity android:name=".NoDisplayActivity" android:theme="@android:style/Theme.NoDisplay" /> windowNoDisplay のドキュメントには
if set to true, and this window is the main window of an Activity, then it will never actually be added to the window manager. This means that your activity must immediately quit without waiting for user interaction, because there will be no such interaction coming.
とあるので、正しい挙動になったと言えるのでしょう。

- 関連 - 参考

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>