メモ2ブログ

メモtoウェブログ。旧ブログはこちら。 http://sakebook.blogspot.jp/

DroidKaigi 2017で登壇してChrome Custom Tabsについて発表しました。

スライドとサンプルアプリと、そのコードです。

speakerdeck.com

play.google.com

github.com

スピーカーから見た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日目 登壇

やり取りのメールを眺めると、上のような感じです。基本スピーカーは、届いたメールやアンケートに返信すれば良いだけになってます。

応募して採択されたけどやっぱり怖い!という人のために、一応内定を断ることもできます。

やることは少ないのですが、自分は画像のアップロードを忘れてしまっていて、デフォルト画像での参加となりました。

f:id:sakebook:20170312214749j:plain

DroidKaigi 前日

スピーカーズディナーに参加して、贈り物みたいなやつ(言葉でないw)を受け取りました。 居酒屋かな?と思っていたら、立食で、思った以上に人がいて驚きました。

DroidKaigi 1日目

受付が、専用受付がありスムーズでした。

登壇当日の動き

登壇当日は一つ前のセッションから、控室で資料の最終確認を行っていました。

めっちゃ緊張してたので、控室で緊張しない方法を尋ねると、色々温かいレスポンスがもらえたのが嬉しかったです。


20分の移動時間が始まり、部屋に移動しました。 早くから部屋に来てた方にChrome Custom Tabsを使ったことがあるか聞いたら、殆どの方が使ったことがないみたいでした。

事前にサンプルアプリを作ってGooglePlayに公開していました。 発表前にQRコードをスライドに写しておくことで、わりとスムーズにアプリの共有はできたのかなと思います。

発表前は、スタッフの方とタイトルの確認とオフィスアワー有無の確認をしました。


発表後はオフィスアワーとして20分程度バリスタさんのコーヒーを飲んでました。

個人的なメモ

アイコン大事

スピーカーズディナーとアフターパーティーで思ったのが、アイコン大事ってことです。

「あぁ!あのアイコンの人だ!」って思うことがよくあったので、忘れた自分は少し機会損失があったのかもしれないなと思いました。

事前知識の共有

1日目は、純粋に発表を聞くのと、スライドの作りや構成で参考にできないかなという目線で見てました。

自分も聞いててあったのですが、わからない部分が出てきたとき、それが今後の前提となってくると、ずっと置いてけぼりをくらってだんだんわからなくなる負の連鎖があります。

回避方法として、要所要所にまとめを設けるのと、事前知識として共有しておかないとまずいことは、時間を割いてでも共有しておくことで防げるのかなと思いました。

主観ですが、これができている発表は聞きやすかったように思います。

しかし、自分の発表には適応できませんでした。 Chrome Custom Tabsに触ったことのない方が多かったので、最低限のコードは入れておくと、後のイメージがしやすくなったんだろうなと思いました。

人に見てもらう

若干早いという指摘もありました(実際20分くらいで終わってしまった)。

リハでも早く終わってたので、質疑応答に充てようと思っていたのですが、実際は、前述のような追加できる要素が残っていました。

社内の人や、同じスピーカーの方に見てもらうなどできれば気づけた部分だったのかなと思いました。

スピーカー同士で、スライドの確認をできる場が公式であっても良いのかなとちょっと思いました。

やってよかった

お金を払って聞きに来てくださった方に、聞いてよかったという価値を提供することができたんだろうか?と不安だったのですが、発表後にも、オフィスアワーにも、アフターパーティーでも質問をしてもらったので、ある程度価値を提供できたのかなと思いました。

何も起きなかった運営すごい

前述の通り、スピーカーは発表以外にやることがほとんどありません。負担がないし、参加者としても問題なく楽しく参加できました。

当たり前のようになってますが、有志でこれだけ安定してイベントを運営するのは本当にすごいと思います。

まとめ

大きなイベントの登壇は不安ですが、大丈夫です。

来年も何か見つけてCFP応募するか、スタッフとしてお手伝いきればよいなと思いました。 両方してる人たちすごい。。

参考

DroidKaigi 2017 / DroidKaigi

Bottom Navigationをガイドラインぽく実装する

Bottom Navigationはサポートライブラリが提供されています。 しかし、単純に使うだけではガイドラインぽくなりません。

ガイドラインぽく実装してみました。

f:id:sakebook:20170212031306g:plain

ガイドラインぽさ

  • 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の挙動になります。

f:id:sakebook:20170212031616p:plain

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"/>

実際に指定している値は次の通りです

    <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を表示している間、どのような挙動が望ましいか調べきれませんでした。

  1. Snackbar表示中もBottom Navigationの動きに合わせて上下する
  2. Snackbar表示中はBottom Navigationは動かさない
  3. 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が追加されたのですが、本当に最低限度しか用意されていませんでした。。

f:id:sakebook:20170303134743p:plain

f:id:sakebook:20170303134808p:plain

参考

Bottom navigation - Components - / Material design guidelines

StatusBar 透明化の正しい方法 / Y.A.M の 雑記帳

nickbutcher/plaid / GitHub

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

Google Play ニューススタンド / Google Play

Link Stackをメジャーアップデートしました

リンクをちょっとだけ後で読むアプリ、Link Stackをメジャーアップデートしました。

play.google.com

このアプリを作った経緯はこちらです。

sakebook.hatenablog.com

メジャーアップデートの機能紹介と、なぜその機能を付けたかについて書きます。

2つの新機能

  • 未読記事一覧
  • 閲覧履歴分析
未読記事一覧 閲覧履歴分析
f:id:sakebook:20170204210604p:plain:w240 f:id:sakebook:20170204210627p:plain:w240

それぞれタブを追加してます。実装的には、サポートライブラリの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