DroidKaigi 2017で登壇してChrome Custom Tabsについて発表しました。
スライドとサンプルアプリと、そのコードです。
スピーカーから見たDroidKaigiと、それにまつわる個人的なメモです。
スピーカーから見たDroidKaigi
DroidKaigiスケジュール
日程 | 出来事 | 行動 |
---|---|---|
2016.10.01 | CFP募集開始 | |
2016.11.05 | CFP応募 | |
2016.11.15 | 採択内定 | 内定承諾、希望発表時間帯、前日のレセプションへの参加、個別の依頼事項、質問要望等返信 |
2017.01.10 | タイムテーブル確定 | オフィスアワーの可否、Tシャツサイズ選択 |
2017.02.13 | 会場設備について共有 | プロフィール画像のアップロード |
2017.02.22 | スピーカーズディナー(前日のレセプション)案内 | |
2017.03.07 | 当日の会場時刻と設備の案内 | |
2017.03.09 | 1日目 | |
2017.03.10 | 2日目 | 登壇 |
やり取りのメールを眺めると、上のような感じです。基本スピーカーは、届いたメールやアンケートに返信すれば良いだけになってます。
応募して採択されたけどやっぱり怖い!という人のために、一応内定を断ることもできます。
やることは少ないのですが、自分は画像のアップロードを忘れてしまっていて、デフォルト画像での参加となりました。
DroidKaigi 前日
スピーカーズディナーに参加して、贈り物みたいなやつ(言葉でないw)を受け取りました。 居酒屋かな?と思っていたら、立食で、思った以上に人がいて驚きました。
DroidKaigi 1日目
受付が、専用受付がありスムーズでした。
登壇当日の動き
登壇当日は一つ前のセッションから、控室で資料の最終確認を行っていました。
めっちゃ緊張してたので、控室で緊張しない方法を尋ねると、色々温かいレスポンスがもらえたのが嬉しかったです。
20分の移動時間が始まり、部屋に移動しました。 早くから部屋に来てた方にChrome Custom Tabsを使ったことがあるか聞いたら、殆どの方が使ったことがないみたいでした。
事前にサンプルアプリを作ってGooglePlayに公開していました。 発表前にQRコードをスライドに写しておくことで、わりとスムーズにアプリの共有はできたのかなと思います。
発表前は、スタッフの方とタイトルの確認とオフィスアワー有無の確認をしました。
発表後はオフィスアワーとして20分程度バリスタさんのコーヒーを飲んでました。
個人的なメモ
アイコン大事
スピーカーズディナーとアフターパーティーで思ったのが、アイコン大事ってことです。
「あぁ!あのアイコンの人だ!」って思うことがよくあったので、忘れた自分は少し機会損失があったのかもしれないなと思いました。
事前知識の共有
1日目は、純粋に発表を聞くのと、スライドの作りや構成で参考にできないかなという目線で見てました。
自分も聞いててあったのですが、わからない部分が出てきたとき、それが今後の前提となってくると、ずっと置いてけぼりをくらってだんだんわからなくなる負の連鎖があります。
回避方法として、要所要所にまとめを設けるのと、事前知識として共有しておかないとまずいことは、時間を割いてでも共有しておくことで防げるのかなと思いました。
主観ですが、これができている発表は聞きやすかったように思います。
しかし、自分の発表には適応できませんでした。 Chrome Custom Tabsに触ったことのない方が多かったので、最低限のコードは入れておくと、後のイメージがしやすくなったんだろうなと思いました。
人に見てもらう
若干早いという指摘もありました(実際20分くらいで終わってしまった)。
リハでも早く終わってたので、質疑応答に充てようと思っていたのですが、実際は、前述のような追加できる要素が残っていました。
社内の人や、同じスピーカーの方に見てもらうなどできれば気づけた部分だったのかなと思いました。
スピーカー同士で、スライドの確認をできる場が公式であっても良いのかなとちょっと思いました。
やってよかった
お金を払って聞きに来てくださった方に、聞いてよかったという価値を提供することができたんだろうか?と不安だったのですが、発表後にも、オフィスアワーにも、アフターパーティーでも質問をしてもらったので、ある程度価値を提供できたのかなと思いました。
何も起きなかった運営すごい
前述の通り、スピーカーは発表以外にやることがほとんどありません。負担がないし、参加者としても問題なく楽しく参加できました。
当たり前のようになってますが、有志でこれだけ安定してイベントを運営するのは本当にすごいと思います。
まとめ
大きなイベントの登壇は不安ですが、大丈夫です。
「当たり前じゃんと思うかもしれませんが」 大丈夫、それが聞きたいんだ! 他所様の「当たり前」のことを僕らは「知見」と呼んでるんだ!! #DroidKaigi #DroidKaigi1
— なかざん@Gen&Co. (@Nkzn) 2017年3月10日
来年も何か見つけてCFP応募するか、スタッフとしてお手伝いきればよいなと思いました。 両方してる人たちすごい。。
参考
Bottom Navigationをガイドラインぽく実装する
Bottom Navigationはサポートライブラリが提供されています。 しかし、単純に使うだけではガイドラインぽくなりません。
ガイドラインぽく実装してみました。
ガイドラインぽさ
- Navigation Barを含んでBottom Navigationにする
- 下にスクロールすると隠れる
- コンテンツを最大限見せる
- Snackbarを上に表示する
- 影を表示する
- 同じタブを選択するとタブの一番上にスクロールする
- ステートの初期化
Navigation Barを含んでBottom Navigationにする
戻るボタンなどが置いてある、Navigation BarもBottom Navigationに合わせた配色になっています。
API Level 21から、Navigation Barの色を変更できるのでstyleで設定しています。
- values-21/styles.xml
<resources> <style name="AppTheme" parent="BaseTheme"> <item name="android:statusBarColor">@color/translucent</item> <item name="android:windowDrawsSystemBarBackgrounds">true</item> <item name="android:navigationBarColor">@color/colorPrimaryDark</item> </style> </resources>
下にスクロールすると隠れる
画面内のスクロールに合わせて、画面下に向かうときはコンテンツの邪魔にならないように隠れ、画面上に向かうときは再び表示させるという動きが必要です。
CoordinatorLayoutを使い、AppBarLayoutの動きに合わせたBehavior定義することで実現させます。この記事のカスタムBehaviorと同じ動きです。
コンテンツを最大限見せる
Activity#onCreate
で、全画面の描画を可能にします。 setContentView
の前に呼ぶ必要があります。
- MainActivity.kt
findViewById(android.R.id.content).systemUiVisibility = View.SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN or View.SYSTEM_UI_FLAG_LAYOUT_STABLE or View.SYSTEM_UI_FLAG_LAYOUT_HIDE_NAVIGATION setContentView(R.layout.activity_main)
ここまでだと以下のような表示になります。 ガイドラインのgifっぽくはなりません。Navigation Barは隠れないし、スクロールの動きと同期したBottom Navigationの挙動になります。
ToolbarとBottom Navigationをハックします。
Toolbarハック
ガイドラインはSystem Barが半透明に残っているので、System Barは半透明にします。ガイドライン的に合わせ、20%透過の黒にします。 #33000000
半透明にしただけでは、Toolbarが食い込んで見えるだけなので、ToolbarにSystem Bar分のPaddingを与えて、いい感じに見えるようにします。
- activity_main.xml
<android.support.v7.widget.Toolbar android:id="@+id/toolbar" android:paddingTop="@dimen/status_bar_height" android:layout_width="match_parent" android:layout_height="@dimen/tool_bar_height" android:theme="@style/ToolbarTheme" android:background="@color/colorPrimary" app:layout_scrollFlags="scroll|enterAlways" android:elevation="@dimen/small_margin"/>
Toolbarの高さは、Paddingを加えた高さに指定します。こうすることでタイトルの表示位置がずれるのを防ぎます。
Bottom Navigationハック
Toolbarでやったように、こちらもNavigation Barで隠れてしまうので、Navigation Barの色を半透明にします。
そして、Paddingを与えて調整します。
- activity_main.xml
<android.support.design.widget.BottomNavigationView android:id="@+id/bottom_navigation" android:paddingBottom="@dimen/navigation_bar_padding" android:background="@color/colorPrimary" app:menu="@menu/bottom_navigation" app:itemIconTint="@drawable/bottom_navigation_selector" app:itemTextColor="@drawable/bottom_navigation_selector" android:layout_width="match_parent" android:layout_height="@dimen/bottom_navigation_height"/>
実際に指定している値は次の通りです
- dimens.xml
<dimen name="status_bar_height">24dp</dimen> <dimen name="tool_bar_height">80dp</dimen><!-- status bar(24dp) + tool bar(56dp) --> <dimen name="navigation_bar_padding">46dp</dimen><!-- navigation bar(46dp) --> <dimen name="bottom_navigation_height">104dp</dimen><!-- navigation bar(46dp) + bottom navigation(58dp) -->
このままだと横画面に対応できないので、調整します。横画面では、Toolbarの高さが変わるのと、Navigation Barが横にあるという違いがあります。
- values-land/dimens.xml
<dimen name="status_bar_height">24dp</dimen> <dimen name="tool_bar_height">72dp</dimen><!-- status bar(24dp) + landscape tool bar(48dp) --> <dimen name="navigation_bar_padding">0dp</dimen><!-- landscape navigation bar(0dp) --> <dimen name="bottom_navigation_height">58dp</dimen><!-- landscape navigation bar(0dp) + bottom navigation(58dp) -->
Bottom Navigationの高さがToolbarの高さと等しくなくなったので、現状のカスタムBehaviorでは対応できません。スクロールと非同期にし、完全に隠れるように修正します。
スクロールは以前と同じくAppBarLayoutから受け取り、アニメーションを制御することで対応します。
- BottomLayoutBehavior.kt
private fun bottomLayoutChange(bottomLayout: BottomNavigationView, dependency: View) { val diff = Math.abs(lastPosition - dependency.top) val scrollLimit = dependency.top - dependency.bottom if (lastPosition == dependency.top && lastPosition == 0) { // on top limit animation(bottomLayout, true) } else if (lastPosition == dependency.top && lastPosition == scrollLimit) { // on bottom limit animation(bottomLayout, false) } else if (diff > THRESHOLD && lastPosition < dependency.y) { // go to top animation(bottomLayout, true) } else if (diff > THRESHOLD && lastPosition > dependency.y) { // go to bottom animation(bottomLayout, false) } else { // nothing } lastPosition = dependency.y.toInt() } private fun animation(view: View, reverse: Boolean) { if (isAnimate) { return } isAnimate = true val animateValue = (view.bottom - view.top).toFloat() ViewCompat.animate(view) .setDuration(250) .translationY(if (reverse) 0f else animateValue) .setInterpolator(AccelerateDecelerateInterpolator()) .setListener(object : ViewPropertyAnimatorListener { override fun onAnimationEnd(view: View?) { isAnimate = false } override fun onAnimationCancel(view: View?) { isAnimate = false } override fun onAnimationStart(view: View?) { isAnimate = true } }) .start() }
ViewCompat.animate
で、Bottom Navigationの高さ分アニメーションさせています。
Snackbarを上に表示する
何もしなければSnackbarはBottom Navigationに隠れてしまいます。こちらも、カスタムBehaviorで解決させます。 (余談ですがToolbar, Snackbarは単語をつなげるけどNavigation Bar, System Barは単語を分けて書いてます。個人的な感覚ですが、Viewとして触るからかこれがしっくり来てます。)
Bottom Navigationの位置に応じて、Paddingを与えることで隠れないようにします。
- BottomLayoutBehavior.kt
private fun snackbarChange(bottomLayout: BottomNavigationView, dependency: View) { val diff = bottomLayout.bottom - bottomLayout.y // If bottom layout is collapsing, need navigation bar padding. val padding = when(bottomLayout.bottom == bottomLayout.y.toInt()) { true -> context.resources.getDimension(R.dimen.navigation_bar_padding) false -> 0f } ViewCompat.setPaddingRelative(dependency, dependency.paddingStart, dependency.paddingTop, dependency.paddingEnd, (diff + padding).toInt()) }
Bottom Navigationが動くので、Snackbarを表示している間、どのような挙動が望ましいか調べきれませんでした。
- Snackbar表示中もBottom Navigationの動きに合わせて上下する
- Snackbar表示中はBottom Navigationは動かさない
- Snackbat表示中はBottom Navigationは展開された状態で動かさない
以上のようなパターンが考えられたのですが、2の案にしました。1は見にくいのでやめ、3はBottom Navigationが隠れている場合に敢えて合わせて出すのは、見せたいものが違うんじゃないかと思ったので、消去法で2の案にしました。
Snackbarが表示中かどうか判定することでBotom Navigationのアニメーションを制御しています。
- BottomLayoutBehavior.kt
override fun onDependentViewChanged(parent: CoordinatorLayout, bottomLayout: BottomNavigationView, dependency: View): Boolean { when(dependency) { is AppBarLayout -> { if (snacking) { // Do not animation while the Snackbar is fixed. return true } bottomLayoutChange(bottomLayout, dependency) } is Snackbar.SnackbarLayout -> { snacking = dependency.top == dependency.y.toInt() // If true, Snackbar is completely displayed. snackbarChange(bottomLayout, dependency) } } return true }
影を表示する
現状のサポートライブラリ(25.1.1)では、Bottom Navigationに影がつきません。なので影っぽいViewを用意して、影を表現します。
影もBottom Navigationに合わせて動く必要があるので、2つをLinearLayoutに入れてBehaviorで制御するようにしました。
- activity_main.xml
<LinearLayout android:id="@+id/layout_bottom" android:orientation="vertical" android:gravity="bottom" android:layout_gravity="bottom" android:elevation="@dimen/small_margin" app:layout_behavior="@string/bottom_layout_behavior" android:layout_width="match_parent" android:layout_height="wrap_content"> <View android:id="@+id/view_shadow" android:layout_width="match_parent" android:layout_height="@dimen/shadow_height" android:background="@drawable/shadow"/> <android.support.design.widget.BottomNavigationView android:id="@+id/bottom_navigation" android:paddingBottom="@dimen/navigation_bar_padding" android:background="@color/colorPrimary" app:menu="@menu/bottom_navigation" app:itemIconTint="@drawable/bottom_navigation_selector" app:itemTextColor="@drawable/bottom_navigation_selector" android:layout_width="match_parent" android:layout_height="@dimen/bottom_navigation_height"/> </LinearLayout>
Behaviorはxml上で補完が効かなかったので、サポートライブラリのようにstrings.xmlに定義しました。
strings.xml
<string name="bottom_layout_behavior" translatable="false">com.sakebook.android.sample.bottomnavigationviewsample.BottomLayoutBehavior</string>
同じタブを選択するとタブの一番上にスクロールする
ガイドラインでは、同じタブを選択した場合は一番上にスクロールするのが望ましいとされていますが、現状のサポートライブラリ(25.1.1)では、選択したタブが同じだったかどうかの判定はしてくれません。TabLayoutにはListenerがあるのに。。
Bottom Navigationのタブを押された際に、現在表示しているFragmentと同じかどうか確認し、同じなら一番上にスクロールさせてます。
Fragmentは都度生成しています(後述)。
- MainActivity.kt
private fun updateBottomNavigation(position: Int, tag: String): Boolean { val fragment = supportFragmentManager.findFragmentByTag(tag) when(fragment) { null -> { supportFragmentManager .beginTransaction() .replace(R.id.layout_container, MainFragment.newInstance(position), tag) .setTransition(FragmentTransaction.TRANSIT_FRAGMENT_FADE) .commit() } else -> { if (fragment is BottomNavigationInterface) { fragment.scrollToTop() } } } return true }
- MainFragment.kt
override fun scrollToTop() { recyclerView?.smoothScrollToPosition(0) }
ステートの初期化
ガイドライン的には、タブが切り替えて表示されたときには、そのタブの状態を初期化することが推奨されています。
そのために今回はFragmentを作り直しています。 Fragmentの生成コストが高い場合は問題だなと思ってます。また、縦に長かったり、階層を持つコンテンツの場合、先程まで触っていたものに戻れないので、適さないと感じました。 縦に長い場合はキャッシュして二度目は高速表示できるようしたり、階層を持つものの場合はBottom Navigationを隠して表示するとか工夫が必要な気がします。
Webでいうサイドバーと同じ効果を期待しているものみたいなのでそうなっているのかなと思いました。
Play ニュースが初期化するようになっているので参考になるかもしれません。
まとめ
ガイドライン通りに実装するには現状工夫が必要になります。 別のライブラリも出ているので、こだわるならそちらを使うのも一つかもしれません。 タブを追加するだけなら簡単なので、コンテンツが合うなら良いと思います。
サンプルはこちらです。
GitHub - sakebook/BottomNavigationViewSample: BottomNavigation sample with support library.
追記(2017/3/3)
Android Studio 2.3から、プロジェクトテンプレートにBottom Navigation Activityが追加されたのですが、本当に最低限度しか用意されていませんでした。。
参考
Bottom navigation - Components - / Material design guidelines
StatusBar 透明化の正しい方法 / Y.A.M の 雑記帳
Android transparent status bar and actionbar / Stack Overflow
AndroidのCoordinatorLayoutを使いこなして、モダンなスクロールを実装しよう / Yahoo! JAPAN Tech Blog
Structure - Layout - / Material design guidelines
Move snackbar above the bottom bar / Stack Overflow
Android Bottom Navigation Bar with drop shadow / Stack Overflow
TabLayout.OnTabSelectedListener / Android Developers
Selected tab’s color in Bottom Navigation View / Stack Overflow
Link Stackをメジャーアップデートしました
リンクをちょっとだけ後で読むアプリ、Link Stackをメジャーアップデートしました。
このアプリを作った経緯はこちらです。
メジャーアップデートの機能紹介と、なぜその機能を付けたかについて書きます。
2つの新機能
- 未読記事一覧
- 閲覧履歴分析
未読記事一覧 | 閲覧履歴分析 |
---|---|
それぞれタブを追加してます。実装的には、サポートライブラリのBottom NavigationViewを使いました。
未読記事一覧
起動して表示されるフィード以外に、未読記事のみを抽出した未読記事一覧を作りました。未読記事一覧では、未読記事を見ることはもちろん、既読チェックだけをつけることもできます。
閲覧履歴分析
Link Stackで表示した記事を対象に、グラフィカルに閲覧履歴がわかる機能を付けました。
- 閲覧ドメインTOP10
- よく見るサイトとその割合がわかります。
- 曜日ごとの平均記事閲覧数
- どの曜日に記事をどれだけ見ているかがわかります。
- 毎日の記事閲覧数
- 一日どれだけ記事を見ているのかがわかります。
どうしてその機能を付けたのか
普段使いにしてるからこそ痒いところを解決したかった
Android 7.0以降で、特に指定がなければ同じアプリからの通知が3通貯まるとまとまって表示されるようになりました。 展開すると3通入っている別の通知が作成される感じです。
Link Stackは、未読通知はスワイプでは消せないようにしているのですが、まとまる通知になってしまうと スワイプで消せるようになってしまいます。
まとめさせないことは簡単なのですが、OSの挙動に逆らう感じがして気乗りせず、対応していませんでした。
なので、誤って消してしまうことが多くなりました。 また、端末ももう2年以上使っているからか、意味の分からない再起動や、バッテリー切れなどが度々起きてしまい、未読のまま読み忘れる記事が結構多くなってきていました。
読み忘れたものの一覧を表示して、再び読む機会が欲しいと思っていました。
foursqaureのstatsが好きだった
今はSwarmに機能が移ってますが、自分のチェックイン履歴から、どこにどれだけ行っているとか誰と何回チェックインしているとかを分析してくれる機能です。 Link Stackを開発してすぐに、これは似たようなことができるかもしれないと思い、データだけ貯めていました。
初期からあった展望で、いつか追加したいと思ってました。追加するならBottom Navigationで実装したいなと思っていたのですが、3つ目のタブが思いつかず、実装まで着手していませんでした。 前述の履歴問題が顕著になってきて、あぁ、これをタブにすればと気づいたので合わせて実装することにしました。
今後の展望
気づきを与えてくれたり、何かのきっかけをくれるサービスが好きです。 Link Stackもそういうサービスにしていけたらなと思います。
まだ案はいくつもあるので、適宜更新していきたいと思います。
参考
BottomNavigationView / Android Developers
Android 7.0 for Developers / Android Developers
Introducing Swarm 4.0: Stats on stats on stats / The Foursquare Blog
Bottom navigation - Components - / Material design guidelines