2016年2月19日金曜日

StatusBar 透明化の正しい方法

各属性についての説明や、なぜこのような設定になっているのかは Droid Kaigi 2016 の発表資料の 121p 以降 を参照してください。

values/styles.xml <resources> <style name="Theme.AppTheme.TranslucentStatusBar" parent="Theme.AppCompat.Light.NoActionBar" /> </resources> values-v19/styles.xml <resources> <style name="Theme.AppTheme.TranslucentStatusBar" parent="Theme.AppCompat.Light.NoActionBar"> <item name="android:windowTranslucentStatus">true</item> </style> </resources> values-v21/styles.xml <resources> <style name="Theme.AppTheme.TranslucentStatusBar" parent="Theme.AppCompat.Light.NoActionBar"> <item name="android:statusBarColor">@android:color/transparent</item> </style> </resources> values-v23/styles.xml <resources> <style name="Theme.AppTheme.TranslucentStatusBar" parent="Theme.AppCompat.Light.NoActionBar"> <item name="android:statusBarColor">@android:color/transparent</item> <item name="android:windowLightStatusBar">true</item> </style> </resources>

public class SplashActivity extends AppCompatActivity { @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) { findViewById(android.R.id.content).setSystemUiVisibility( View.SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN | View.SYSTEM_UI_FLAG_LAYOUT_STABLE); } setContentView(R.layout.activity_splash); } } <activity android:name=".SplashActivity" android:theme="@style/Theme.AppTheme.TranslucentStatusBar" />

上記のような指定を行うと次のような見た目になります。

4.3以下


4.4


5系


6系



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() 時のアニメーションが移動だけになります。