2016年3月5日土曜日

CollapsingToolbarLayout で status bar を透明にする方法

注意:以下は support library v23.2.0 での動作をもとにしています。

追記
AppBarLayout と CollapsingToolbarLayout にも android:fitsSystemWindows="true" の指定を追加するようにしました。 これにより、v25.0.1, v25.0,0, v24.2.1, v24.2.0, v24.1.1, v24.1.0, v24.0.0, v23.4.0, v23.3.0, v23.2.1 でも動作することを確認してあります。



わかりやすいように

colorPrimary : #ff0000(赤)
colorPrimaryDark : #99ff00ff(マゼンダ)

contentScrim : #990000ff(青)
statusBarScrim : #9900ffff(シアン)

Toolbar の background : #9900ff00(緑)

にしてあります。

普通に実装するとこんな感じになります。 <?xml version="1.0" encoding="utf-8"?> <android.support.design.widget.CoordinatorLayout xmlns:android="http://schemas.android.com/apk/res/android" xmlns:app="http://schemas.android.com/apk/res-auto" xmlns:tools="http://schemas.android.com/tools" android:layout_width="match_parent" android:layout_height="match_parent"> <android.support.design.widget.AppBarLayout android:layout_width="match_parent" android:layout_height="wrap_content" android:fitsSystemWindows="true" android:theme="@style/ThemeOverlay.AppCompat.Dark.ActionBar"> <android.support.design.widget.CollapsingToolbarLayout android:id="@+id/toolbar_layout" android:layout_width="match_parent" android:layout_height="wrap_content" android:fitsSystemWindows="true" app:contentScrim="#990000ff" app:layout_scrollFlags="scroll|exitUntilCollapsed" app:statusBarScrim="#9900ffff"> <ImageView android:layout_width="match_parent" android:layout_height="360dp" android:scaleType="centerCrop" android:src="@drawable/sample" tools:ignore="ContentDescription" /> <android.support.v7.widget.Toolbar android:id="@id/toolbar" android:layout_width="match_parent" android:layout_height="?attr/actionBarSize" app:layout_collapseMode="pin" tools:background="#9900ff00" /> </android.support.design.widget.CollapsingToolbarLayout> </android.support.design.widget.AppBarLayout> <android.support.v4.widget.NestedScrollView android:layout_width="match_parent" android:layout_height="match_parent" android:layout_gravity="fill_vertical" app:layout_behavior="@string/appbar_scrolling_view_behavior"> ... </android.support.v4.widget.NestedScrollView> </android.support.design.widget.CoordinatorLayout> これを実行するとこうなります。



マゼンダ色の status bar が表示されています。この status bar を透明にして ImageView をその分上にあげるにはどうすればいいか。

そのためにまずテーマに <item name="android:statusBarColor">@android:color/transparent</item> を指定して、Activity の onCreate() に以下の処理を追加します。 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); } すると次のようになります。



マゼンダから赤色に変わりました。これはステータスバーの色が見えているのではなく CollapsingToolbar の領域が見えています。その証拠にスクロールするとこの領域も移動します。

この領域はどこで確保されているかというと CollapsingToolbarLayout の onLayout() です。 @Override protected void onLayout(boolean changed, int left, int top, int right, int bottom) { super.onLayout(changed, left, top, right, bottom); ... // Update our child view offset helpers for (int i = 0, z = getChildCount(); i < z; i++) { final View child = getChildAt(i); if (mLastInsets != null && !ViewCompat.getFitsSystemWindows(child)) { final int insetTop = mLastInsets.getSystemWindowInsetTop(); if (child.getTop() < insetTop) { // If the child isn't set to fit system windows but is drawing within the inset // offset it down ViewCompat.offsetTopAndBottom(child, insetTop); } } getViewOffsetHelper(child).onViewLayout(); } ... } CollapsingToolbarLayout の直接の子ビューで fitsSystemWindows が false のものは、ビューの配置位置を insetTop(ステータスバーの高さ)分だけ下にずらすようになっています。
つまり、fitsSystemWindows = true を子ビューにセットすれば、この処理が行われないということです。

そこで ImageView に android:fitsSystemWindows="true" を追加すると <android.support.design.widget.CollapsingToolbarLayout ... > <ImageView ... android:fitsSystemWindows="true" /> <android.support.v7.widget.Toolbar ... /> </android.support.design.widget.CollapsingToolbarLayout> 次のようになります。




この方法を取る場合、4.4 で注意が必要です。 「 StatusBar 透明化の正しい方法」で書いているように、v19 では android:windowTranslucentStatus をセットして、v21 では android:statusBarColor をセットしている場合、上記の対応を入れると次のように 4.4 で余分な領域が確保されてしまいます。



そのため、v21以降だけ android:fitsSystemWindows="true" が指定されるようにします。

values/bools.xml <?xml version="1.0" encoding="utf-8"?> <resources> <bool name="fitsSystemWindowForImage">false</bool> </resources> values-v21/bools.xml <?xml version="1.0" encoding="utf-8"?> <resources> <bool name="fitsSystemWindowForImage">true</bool> </resources> <android.support.design.widget.CollapsingToolbarLayout ... > <ImageView ... android:fitsSystemWindows="@bool/fitsSystemWindowForImage" /> <android.support.v7.widget.Toolbar ... /> </android.support.design.widget.CollapsingToolbarLayout> こうすると、4.4 でも次のような結果になります。




4.4 での実行結果をよく見ると、閉じたときに status bar 分の領域が確保されずに Toolbar が status bar の下にきてしまうことがわかります。



残念ながら 4.4 で閉じた時に status bar 分を確保するシンプルな方法はありません。 以下のように inset を margin に付け替える Toolbar を用意するとこれが解決できます。 public class CustomToolbar extends Toolbar { public CustomToolbar(Context context) { super(context); } public CustomToolbar(Context context, AttributeSet attrs) { super(context, attrs); } public CustomToolbar(Context context, AttributeSet attrs, int defStyleAttr) { super(context, attrs, defStyleAttr); } @Override protected boolean fitSystemWindows(Rect insets) { final ViewGroup.LayoutParams params = getLayoutParams(); if (params instanceof MarginLayoutParams) { ((MarginLayoutParams) params).topMargin = insets.top; } return true; } }





2016年2月28日日曜日

conent_available を true にすると foreground のときしか onMessageReceived() が呼ばれない

GcmListenerService では、特定の条件の場合 onMessageReceived() を呼ばずに自分で Notification を出すような処理になっています。

この条件が Google Play Services のバージョンによって異なり、7.8.0 から 8.4.0 に変更したときにはまったので調べた結果をまとめておきます。
(調べたのは 7.8.0 と 8.4.0 で、この間の他のバージョンは調べていません。)

7.8.0



7.8.0 では、送信する JSON に "notification" payload があり、その中の "icon" の値が null じゃない場合は onMessageReceived() が呼ばれずに GcmListenerService が Notification を出します。このときの Notification には "notification" payload で指定された "title" や "body" が使われます。"title" の値が空のときはアプリ名が利用されます。

notification payload の形式については https://developers.google.com/cloud-messaging/http-server-ref#notification-payload-support 参照

つまり
"notification" の "icon" == null → onMessageReceived()
"notification" の "icon" != null → GcmListenerService が Notification 発行


8.4.0



8.4.0 では条件が大きく変わります。
"content_available" が false (指定がない場合も false)かつ "notification" の "icon" == null の場合 onMessageReceived() が呼ばれます。そうでない場合、アプリが foreground にあれば onMessageReceived() が呼ばれ、background なら GcmListenerService が Notification を出します。

つまり
"content_available" == false && "notification" の "icon" == null → onMessageReceived()
"content_available" == true || "notification" の "icon" != null →
  アプリが foreground → onMessageReceived()
  アプリが background → GcmListenerService が Notification 発行

"content_available" を true にしていると、7.8.0 から 8.4.0 に変更したときに今まで background でも onMessageReceived() が呼ばれていたのに呼ばれなくなります。全然ドキュメントにも書いてないしはまりました。。。


7.8.0 のコード

GcmListenerService private void zzt(Bundle var1) { var1.remove("message_type"); var1.remove("android.support.content.wakelockid"); if(zza.zzu(var1)) { zza.zzay(this).zzv(var1); // "notification" のデータを使って Notification を発行している } else { String var2 = var1.getString("from"); var1.remove("from"); this.onMessageReceived(var2, var1); } } zza static boolean zzu(Bundle var0) { return zzb(var0, "gcm.n.icon") != null; } "gcm.n.icon" は "notification" payload の "icon" に対応している。


8.4.0 のコード

GcmListenerService private void zzq(Intent var1) { Bundle var2 = var1.getExtras(); var2.remove("message_type"); var2.remove("android.support.content.wakelockid"); if(zzb.zzy(var2)) { if(!zzb.zzaI(this)) { // 同じプロセスの Activity が foreground かチェック、foreground じゃない場合 if の中に入る zzb.zzc(this, this.getClass()).zzA(var2); // "notification" のデータを使って Notification を発行している return; } if(zzx(var1.getExtras())) { zza.zzh(this, var1); } zzb.zzz(var2); } String var3 = var2.getString("from"); var2.remove("from"); zzw(var2); this.onMessageReceived(var3, var2); } zzb static boolean zzy(Bundle var0) { return "1".equals(zze(var0, "gcm.n.e")) || zze(var0, "gcm.n.icon") != null; } "gcm.n.e" は "content_available" に対応している。 "gcm.n.icon" は "notification" payload の "icon" に対応している。

zzb static boolean zzaI(Context var0) { KeyguardManager var1 = (KeyguardManager)var0.getSystemService("keyguard"); if(var1.inKeyguardRestrictedInputMode()) { return false; } else { int var2 = Process.myPid(); ActivityManager var3 = (ActivityManager)var0.getSystemService("activity"); List var4 = var3.getRunningAppProcesses(); if(var4 != null) { Iterator var5 = var4.iterator(); while(var5.hasNext()) { RunningAppProcessInfo var6 = (RunningAppProcessInfo)var5.next(); if(var6.pid == var2) { return var6.importance == 100; } } } return false; } }


2016年2月21日日曜日

講演の準備について

Droid Kaigi 2016 を振り返ってふと講演前に何をしてきたのかを書いてみようと思いました。

Konifar氏に「あー」とか「えー」とかをほとんど言わずあまり噛まないとお褒めいただいたことで、どうしてそうなのか改めて考えてみたことがきっかけです。

1. CFP

直後ではなく、締め切り2週間〜1週間前くらいに出しました。
すでに提出されているCFPを確認して、かぶらないような内容を選びました。
私が確認した時点ですでにCamera2やRxJava、Kotlinなどがかぶっていました。

2. 話の流れを考える

本格的に考え出したのは本番1ヶ月前くらいからです。
細かい内容よりも全体の流れを先に考えます。
風呂に入っているときや寝る前に脳内リハーサル(イメージトレーニング)します。
マインドマップは書きません。以前の講演で試したこともあるのですが合わないのか続きませんでした。

3. 裏取りをする

人に伝える以上間違ったことは話せません。
以前やったことがあることでももう一度実際に書いて試してみます。
また、このときに調べ直して新たに知ったことも多いです。
この作業時にスライドに載せるコードやキャプチャも大まかに決めます。

4. 会場のサイズを知る

スライドの文字サイズを決めるために @mhidaka に担当部屋を下見したときの写真をみせてもらいました。
写真から
- 奥行きがかなりある
- スクリーンが左右に2枚
- スクリーンの下の方は少し見ずらい(このことは講演者へのメールにも記載されていました)
ことがわかりました。

スタッフと知り合いじゃないと難しいと思うので、できれば大きなイベントの開催者は講演者へのメールに会場の写真も添付してもらえると助かる人が多いと思います。

5. 資料作成

白地に黒の方が見やすい

特に文字が細い場合黒字だと会場を暗くしないと見るのが厳しいです。
私はスライドのビジュアル(絵的に美しいかどうか)よりも見やすいかどうかを優先しています。

会場のサイズから文字サイズはかなり悩みました。
最終的に重要なコードは 24pt 以上、文章は 32pt 以上になるようにしました。

私はスライドの作成にはほぼ Keynote を使っています。慣れによるところが大きいです。

発表者ノートを使うこともあります。今回は使っていませんが、エモーショナルなスライド(写真背景 + 格言みたいなやつ)では話す内容のヒントが少ないため、そのような内容の講演だと使うことがあります。

6. 脳内リハーサル(イメージトレーニング)

多分これが噛まない秘訣(?)だと思います。実リハーサルはほぼやらないのですが1週間前から寝る前の頭の中はほぼこれです。頭の中でしゃべってます。

7. 前日

2日目だったので、1日目の最後にお願いして実際の部屋でスライドを映すテストをさせていただけました。
コードに半透明の黄色でハイライトをいれていたのですが、実際に写してみるとハイライトをいれると元の文字が見にくいということがわかり、急遽赤線を入れる方向に変えました。

8. 本番

どうしても一部コードの文字が小さいスライドがあるので、事前にスライドを公開すると決めていました。本番10分前に公開して tweet &会場で始まるまでQRコードとURLをスライドで表示しました。

9. 終わった感想

今回は話の流れの組み立てにけっこう悩みました。AndroidフレームワークのTheme・Styleの機能と AppCompat 独自の機能は別ですが、AppCompat は Material Design の見た目を実現するのにフレームワークの機能を使っています。そこをカスタマイズするときは AppCompat がどのような設定を行っているのかを知る必要があるため、AppCompat の設定値の話が多くなりました。
後から考えるともう少しAndroidフレームワークのTheme・Styleの機能についての説明も入れたほうがよかったなと思いました。(?attr/と@style/とandroid:のありなしについて後で質問されたので)

今回は想定以上にスライドの枚数が多くなり早口だったと思います。42分くらいで話終わってしまったのでもう少しゆっくり話せばよかったと反省です。枚数が多い時の時間配分は難しいですね。