$30 off During Our Annual Pro Sale. View Details »

5年更新していなかったアプリが 海外バイヤーから数万ドルで 買収されそうになったので、 久しぶりにアプリを更新しようと思ったら 色々仕様が変わっていたので、 細々したよくやるやつをツラツラと話す話/RecentlyAndroidFunctions

AAkira
November 21, 2019

5年更新していなかったアプリが 海外バイヤーから数万ドルで 買収されそうになったので、 久しぶりにアプリを更新しようと思ったら 色々仕様が変わっていたので、 細々したよくやるやつをツラツラと話す話/RecentlyAndroidFunctions

You can check gif slide in Google Photo.
https://is.gd/nx5hJi

I talked this in the meetup of thanksgiving android presented by m3.
https://m3-engineer.connpass.com/event/152923/

AAkira

November 21, 2019
Tweet

More Decks by AAkira

Other Decks in Technology

Transcript

  1. 5年更新していなかったアプリが
    海外バイヤーから数万ドルで
    買収されそうになったので、
    久しぶりにアプリを更新しようと思ったら
    色々仕様が変わっていたので、
    細々したよくやるやつをツラツラと話す話
    @_a_akira
    荒谷 光
    あらたに あきら

    View Slide

  2. About me
    @_a_akira AAkira
    M3, Inc.
    Akira Aratani
    https://aakira.app
    !"#$%&'()*+,-./0123456789
    ✈;<=⊿
    https://aakira.studio
    aakira.studio

    View Slide

  3. View Slide

  4. ことの発端
    ある日、こんなメールが来た

    View Slide

  5. ことの発端
    発表時だけの秘密♡

    View Slide

  6. ことの発端
    発表時だけの秘密♡

    View Slide

  7. ことの発端
    $ 40,000 = 40,000 * 109
    = ¥
    4,360,000
    発表時だけの秘密♡
    発表時だけの秘密♡ 発表時だけの秘密♡

    View Slide

  8. ことの発端

    View Slide

  9. ことの発端
    念の為、色々ヒアリングすることに

    View Slide

  10. ことの発端
    < TOP3 countriesのActive User数は?
    < 本当にその額で買ってくれるの?
    < 4, 50000くらいのユーザがいれば

    40k以上は確実だよ
    発表時だけの秘密♡

    View Slide

  11. ことの発端
    < アプリとソースコードだけくれればいいよ
    < 買ってどうするの?
    < (答えになっていない...)
    < 代わりにアプデしてくれるの?

     そしたらソースコードもいるよね?

    View Slide

  12. ことの発端
    < で、TOP3 countries!>-のActive User数は?
    < どうやって、売るの?
    < flippa.com っていうサイトがあるやで

    View Slide

  13. ことの発端
    < 最近アップデートしてないし、
     日本のユーザばっかりだよ
     大体5000くらい
    < なるほどー

    ちょっと聞いてみる
    < それだと$700だって

    View Slide

  14. ことの発端
    < うーん。それだったら売らないかなー
    < 他にもいっぱいアプリ出してるじゃん。

     他はどうなん?
    < 他のも似たようなもんだから大丈夫

    View Slide

  15. View Slide

  16. まとめ
    • 買って何をするのか具体的に教えてくれないので、

    ちょっと怪しい
    • この人はバイヤーなので、裏に誰かがいる?
    • TOP5 countries [!>-"?] で

    50,000ユーザぐらいいれば4~500万円を手にすることが出来る

    (可能性がある)
    発表時だけの秘密♡

    View Slide

  17. 意外とこういうアプリでも需要があるんだなー

    View Slide

  18. 父さんな、会社辞めて
    スマートフォンアプリ開発で食っていこうと思うんだ。

    View Slide

  19. 5年更新していなかったアプリが
    海外バイヤーから数万ドルで
    買収されそうになったので、
    久しぶりにアプリを更新しようと思ったら
    色々仕様が変わっていたので、
    細々したよくやるやつをツラツラと話す話
    @_a_akira
    荒谷 光
    あらたに あきら

    View Slide

  20. 5年更新していなかったアプリを
    久しぶりに更新しようと思ったら
    色々仕様が変わっていたので、
    細々したよくやるやつをツラツラと話す話
    @_a_akira
    荒谷 光
    あらたに あきら

    View Slide

  21. 大学時代に作ったアプリ達

    View Slide

  22. 大学時代に作ったアプリ達
    ラブストーリーぐらい突然 ストアから消される

    View Slide

  23. 大学時代に作ったアプリ達
    丁度昨晩タイムリーに削除

    View Slide

  24. 大学時代に作ったアプリ達

    View Slide

  25. どんなアプリなのか
    スマホ上にフィルターをかけて
    覗き見防止するアプリ
    端末の設定以上に
    画面を暗くしたりも出来る

    View Slide

  26. どんなアプリなのか
    Androidアプリで、覗き見防止アプリがリリースされたのはこのアプリが多分2番目

    View Slide

  27. どんなアプリなのか
    昔のUI感がダサい... リニューアルしてみた(途中)

    View Slide

  28. いろいろやったリスト
    • QuickSettingsTiles(通知エリアのタイルボタン)
    • Palette
    • WindowManager
    • バックグラウンド
    • SharedElement
    • Transition animation(Explode, 角丸ピッ)
    • WindowInsets
    • AnimatedVectorDrawable
    • BottomAppBar
    • ViewPager2 (Infinity Loop, ひょっこりはん)

    View Slide

  29. いろいろやったリスト
    • QuickSettingsTiles(通知エリアのタイルボタン)
    • Palette
    • WindowManager
    • バックグラウンド
    • SharedElement
    • Transition animation(Explode, 角丸ピッ)
    • WindowInsets
    • AnimatedVectorDrawable
    • BottomAppBar
    • ViewPager2 (Infinity Loop, ひょっこりはん)
    上3つは話す予定

    あとは挙手制?

    View Slide

  30. Quick settings Tile
    https://www.instagram.com/p/B2ZWvA2AkRY/

    View Slide

  31. Quick Settings Tile
    Nougat(API24)から追加
    カスタマイズToggle Buttonを
    置くことが出来る

    View Slide

  32. Quick Settings Tile
    Notification Quick Settings Tile

    View Slide

  33. Quick Settings Tile
    class MyTileService: TileService(){
    override fun onClick() {
    super.onClick()
    // ΫϦοΫ࣌ʹݺ͹ΕΔ
    }
    override fun onTileRemoved() {
    super.onTileRemoved()
    // ฤूը໘͔ΒλΠϧ࡟আ࣌ʹݺ͹ΕΔ
    }
    override fun onTileAdded() {
    super.onTileAdded()
    // ฤूը໘͔ΒλΠϧ௥Ճ࣌ʹݺ͹ΕΔ
    }
    override fun onStartListening() {
    super.onStartListening()
    // ௨஌ΤϦΞ಺ͷλΠϧ͕දࣔ͞Εͨ࣌
    }
    override fun onStopListening() {
    super.onStopListening()
    // ௨஌ΤϦΞ಺ͷλΠϧ͕ݟ͑ͳ͘ͳͬͨ࣌
    }
    }

    View Slide

  34. class MyTileService: TileService(){
    override fun onClick() {
    super.onClick()
    // ΫϦοΫ࣌ʹݺ͹ΕΔ
    }
    override fun onTileRemoved() {
    super.onTileRemoved()
    // ฤूը໘͔ΒλΠϧ࡟আ࣌ʹݺ͹ΕΔ
    }
    override fun onTileAdded() {
    Quick Settings Tile

    View Slide

  35. override fun onClick() {
    super.onClick()
    // ΫϦοΫ࣌ʹݺ͹ΕΔ
    }
    override fun onTileRemoved() {
    super.onTileRemoved()
    // ฤूը໘͔ΒλΠϧ࡟আ࣌ʹݺ͹ΕΔ
    }
    override fun onTileAdded() {
    super.onTileAdded()
    // ฤूը໘͔ΒλΠϧ௥Ճ࣌ʹݺ͹ΕΔ
    }
    override fun onStartListening() {
    Quick Settings Tile

    View Slide

  36. override fun onTileRemoved() {
    super.onTileRemoved()
    // ฤूը໘͔ΒλΠϧ࡟আ࣌ʹݺ͹ΕΔ
    }
    override fun onTileAdded() {
    super.onTileAdded()
    // ฤूը໘͔ΒλΠϧ௥Ճ࣌ʹݺ͹ΕΔ
    }
    override fun onStartListening() {
    super.onStartListening()
    // ௨஌ΤϦΞ಺ͷλΠϧ͕දࣔ͞Εͨ࣌
    }
    override fun onStopListening() {
    Quick Settings Tile

    View Slide

  37. override fun onTileAdded() {
    super.onTileAdded()
    // ฤूը໘͔ΒλΠϧ௥Ճ࣌ʹݺ͹ΕΔ
    }
    override fun onStartListening() {
    super.onStartListening()
    // ௨஌ΤϦΞ಺ͷλΠϧ͕දࣔ͞Εͨ࣌
    }
    override fun onStopListening() {
    super.onStopListening()
    // ௨஌ΤϦΞ಺ͷλΠϧ͕ݟ͑ͳ͘ͳͬͨ࣌
    }
    }
    Quick Settings Tile
    ここでState Checkした方が良い

    View Slide

  38. override fun onTileAdded() {
    super.onTileAdded()
    // ฤूը໘͔ΒλΠϧ௥Ճ࣌ʹݺ͹ΕΔ
    }
    override fun onStartListening() {
    super.onStartListening()
    // ௨஌ΤϦΞ಺ͷλΠϧ͕දࣔ͞Εͨ࣌
    qsTile?.state = if (isFeatureRunning()) Tile.STATE_ACTIVE
    else Tile.STATE_INACTIVE
    qsTile?.updateTile()
    }
    override fun onStopListening() {
    super.onStopListening()
    // ௨஌ΤϦΞ಺ͷλΠϧ͕ݟ͑ͳ͘ͳͬͨ࣌
    }
    Quick Settings Tile
    通知エリアの状態表示

    View Slide

  39. override fun onTileAdded() {
    super.onTileAdded()
    // ฤूը໘͔ΒλΠϧ௥Ճ࣌ʹݺ͹ΕΔ
    }
    override fun onStartListening() {
    super.onStartListening()
    // ௨஌ΤϦΞ಺ͷλΠϧ͕දࣔ͞Εͨ࣌
    }
    override fun onStopListening() {
    super.onStopListening()
    // ௨஌ΤϦΞ಺ͷλΠϧ͕ݟ͑ͳ͘ͳͬͨ࣌
    }
    }
    Quick Settings Tile

    View Slide

  40. Quick Settings Tile
    android:name=".MyTileService"
    android:enabled="true"
    android:exported="true"
    android:icon="@drawable/ic_tile_icon"
    android:label="DisplayedTitle"
    android:permission="android.permission.BIND_QUICK_SETTINGS_TILE">

    android:name="android.service.quicksettings.action.QS_TILE" />


    AndroidManifest.xml

    View Slide

  41. Quick Settings Tile
    android:name=".MyTileService"
    android:enabled="true"
    android:exported="true"
    android:icon="@drawable/ic_tile_icon"
    android:label="DisplayedTitle"
    android:permission="android.permission.BIND_QUICK_SETTINGS_TILE">

    android:name="android.service.quicksettings.action.QS_TILE" />


    作成したService class
    AndroidManifest.xml

    View Slide

  42. Quick Settings Tile
    android:name=".MyTileService"
    android:enabled="true"
    android:exported="true"
    android:icon="@drawable/ic_tile_icon"
    android:label="DisplayedTitle"
    android:permission="android.permission.BIND_QUICK_SETTINGS_TILE">

    android:name="android.service.quicksettings.action.QS_TILE" />


    QuickSettingsに表示するアイコン
    AndroidManifest.xml

    View Slide

  43. Quick Settings Tile
    android:name=".MyTileService"
    android:enabled="true"
    android:exported="true"
    android:icon="@drawable/ic_tile_icon"
    android:label="DisplayedTitle"
    android:permission="android.permission.BIND_QUICK_SETTINGS_TILE">

    android:name="android.service.quicksettings.action.QS_TILE" />


    QuickSettingsに表示するラベル
    AndroidManifest.xml

    View Slide

  44. Palette
    https://www.instagram.com/p/B0gJPMsg7VK/
    知られざる名機能

    View Slide

  45. Palette
    SupportLibrary v7時代に追加

    背景画像から "良い感じな" 色を抽出

    View Slide

  46. Palette
    import androidx.palette.graphics.Palette


    Palette.from(bitmap).generate {
    val lightPalette = it?.lightVibrantSwatch ?: return@generate
    val mutedPalette = it.vibrantSwatch ?: return@generate
    appBarContent.setStatusBarScrimColor(lightPalette.rgb)
    appBarContent.setContentScrimColor(mutedPalette.rgb)
    }

    View Slide

  47. import androidx.palette.graphics.Palette


    Palette.from(bitmap).generate {
    val lightPalette = it?.lightVibrantSwatch ?: return@generate
    val mutedPalette = it.vibrantSwatch ?: return@generate
    appBarContent.setStatusBarScrimColor(lightPalette.rgb)
    appBarContent.setContentScrimColor(mutedPalette.rgb)
    }
    Palette
    非同期で取得(中身はAsyncTask)

    View Slide

  48. import androidx.palette.graphics.Palette

    Palette.from(bitmap).generate {
    val lightPalette = it?.lightVibrantSwatch ?: return@generate
    val mutedPalette = it.vibrantSwatch ?: return@generate
    appBarContent.setStatusBarScrimColor(lightPalette.rgb)
    appBarContent.setContentScrimColor(mutedPalette.rgb)
    }
    Palette.from(bitmap).generate()
    Palette
    同期取得

    View Slide

  49. import androidx.palette.graphics.Palette


    Palette.from(bitmap).generate {
    val lightPalette = it?.lightVibrantSwatch ?: return@generate
    val mutedPalette = it.vibrantSwatch ?: return@generate
    appBarContent.setStatusBarScrimColor(lightPalette.rgb)
    appBarContent.setContentScrimColor(mutedPalette.rgb)
    }
    Palette

    View Slide

  50. import androidx.palette.graphics.Palette


    Palette.from(bitmap).generate {
    val lightPalette = it?.lightVibrantSwatch ?: return@generate
    val mutedPalette = it.vibrantSwatch ?: return@generate
    appBarContent.setStatusBarScrimColor(lightPalette.rgb)
    appBarContent.setContentScrimColor(mutedPalette.rgb)
    }
    Palette
    異なる鮮やかさが取れる

    View Slide

  51. Palette
    https://developer.android.com/training/material/palette-colors

    View Slide

  52. https://www.instagram.com/p/B0vuB9rAMJh/
    Window Manager

    View Slide

  53. 画面にフィルターを重ねる

    •Lollipopまではこれだけ
    •Marshmallow~ +Runtime permissionが必要
    val intent = Intent(
    Settings.ACTION_MANAGE_OVERLAY_PERMISSION,
    Uri.parse("package:$packageName")
    )
    startActivityForResult(intent, OVERLAY_PERMISSION_REQUEST_CODE)
    •Pie~ ManifestのPermission名変更

    View Slide

  54. 画面にフィルターを重ねる
    TYPE_APPLICATION
    TYPE_PHONE
    TYPE_TOAST
    TYPE_SYSTEM_ALERT
    KEY_GUARD
    STATUS_BAR
    TYPE_SYSTEM_OVERLAY

    View Slide

  55. 画面にフィルターを重ねる
    TYPE_APPLICATION
    TYPE_PHONE
    TYPE_TOAST
    KEY_GUARD
    STATUS_BAR
    TYPE_SYSTEM_OVERLAY
    TYPE_APPLICATION_OVERLAY
    セキュリティの問題でだめ
    TYPE_SYSTEM_ALERT

    View Slide

  56. 画面にフィルターを重ねる
    TYPE_APPLICATION
    TYPE_PHONE
    TYPE_TOAST
    KEY_GUARD
    STATUS_BAR
    TYPE_SYSTEM_OVERLAY
    TYPE_APPLICATION_OVERLAY
    TYPE_APPLICATION_OVERLAYに変更

    View Slide

  57. 画面にフィルターを重ねる
    ステータスバーより下に表示される

    View Slide

  58. 画面にフィルターを重ねる
    val view: View = View(this)
    view.setBackgroundColor(Color.argb(170, 0, 0, 0))
    val params = WindowManager.LayoutParams(
    WindowManager.LayoutParams.MATCH_PARENT,
    WindowManager.LayoutParams.MATCH_PARENT,
    WindowManager.LayoutParams.TYPE_APPLICATION_OVERLAY,
    WindowManager.LayoutParams.FLAG_NOT_TOUCHABLE or
    WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE,
    PixelFormat.TRANSPARENT
    )
    params.windowAnimations = android.R.style.Animation_Toast
    windowManager.addView(view, params)

    View Slide

  59. 画面にフィルターを重ねる
    val view: View = View(this)
    view.setBackgroundColor(Color.argb(170, 0, 0, 0))
    val params = WindowManager.LayoutParams(
    WindowManager.LayoutParams.MATCH_PARENT,
    WindowManager.LayoutParams.MATCH_PARENT,
    WindowManager.LayoutParams.TYPE_APPLICATION_OVERLAY,
    WindowManager.LayoutParams.FLAG_NOT_TOUCHABLE or
    WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE,
    PixelFormat.TRANSPARENT
    )
    params.windowAnimations = android.R.style.Animation_Toast
    windowManager.addView(view, params)
    Layer指定

    View Slide

  60. 画面にフィルターを重ねる
    val view: View = View(this)
    view.setBackgroundColor(Color.argb(170, 0, 0, 0))
    val params = WindowManager.LayoutParams(
    WindowManager.LayoutParams.MATCH_PARENT,
    WindowManager.LayoutParams.MATCH_PARENT,
    WindowManager.LayoutParams.TYPE_SYSTEM_ALERT,
    WindowManager.LayoutParams.FLAG_NOT_TOUCHABLE or
    WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE,
    PixelFormat.TRANSPARENT
    )
    params.windowAnimations = android.R.style.Animation_Toast
    windowManager.addView(view, params)
    8.0以降では落ちる

    View Slide

  61. 画面にフィルターを重ねる
    val view: View = View(this)
    view.setBackgroundColor(Color.argb(170, 0, 0, 0))
    val params = WindowManager.LayoutParams(
    WindowManager.LayoutParams.MATCH_PARENT,
    WindowManager.LayoutParams.MATCH_PARENT,
    WindowManager.LayoutParams.TYPE_APPLICATION_OVERLAY,
    WindowManager.LayoutParams.FLAG_NOT_TOUCHABLE or
    WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE,
    PixelFormat.TRANSPARENT
    )
    params.windowAnimations = android.R.style.Animation_Toast
    windowManager.addView(view, params)
    Flag指定
    ContextCompat.checkSelfPermission(this,
    Manifest.permission.FOREGROUND_SERVICE)

    View Slide

  62. 画面にフィルターを重ねる
    • FLAG_NOT_TOUCHABLE

    タッチ判定を貫通する。

    Messengerアプリみたいなものは設定していない
    • FLAG_NOT_FOCUSABLE

    フォーカスをあてない

    設定しないとフォーカスが当たるのでBackボタンのイベントが取られる
    • FLAG_LAYOUT_IN_SCREEN

    StatusBar部分も重ねる
    https://developer.android.com/reference/android/view/WindowManager.LayoutParams

    View Slide

  63. 画面に重ねる
    動画だとPIPという選択肢もある
    Oreo以前はWindowMangerに描画していた
    https://play.google.com/store/apps/details?id=jp.co.cyberagent.valencia

    View Slide

  64. いろいろやったリスト
    • QuickSettingsTiles(通知エリアのタイルボタン)
    • Palette
    • WindowManager
    • バックグラウンド
    • SharedElement
    • Transition animation(Explode, 角丸ピッ)
    • WindowInsets
    • AnimatedVectorDrawable
    • BottomAppBar
    • ViewPager2 (Infinity Loop, ひょっこりはん)

    View Slide

  65. https://www.instagram.com/p/B3rzRAGAfqi/
    Background

    View Slide

  66. バックグラウンド
    •Oreo(26)まではこれだけ

    startForeground(1, notification)
    •Pie~
    startForeground(1, notification)

    View Slide

  67. バックグラウンド
    •Oreo(26)まではこれだけ

    startForeground(1, notification)
    •Pie~
    startForeground(1, notification) 5秒以内に呼ぶ

    View Slide

  68. • アプリのウィンドウが、フォアグラウンドのアクティビティであるなど、画面上に表示されている。
    • アプリのアクティビティが、フォアグラウンド タスクのバック スタックにある。
    • アプリのアクティビティが、最近使ったアプリ画面の既存タスクのバックスタックにある。
    • ごく最近開始した、このアプリのアクティビティがある。
    • ごく最近、このアプリがアクティビティで finish() を呼び出した。これは、finish() が呼び出されたときに、このアプリのアクティビティがフォアグラウンドにあった、またはフォアグラウンド タス
    クのバック スタックにあった場合のみに適用されます。
    • システムによってバインドされたサービスがこのアプリにある。この条件は次のサービスにのみ適用されます(UI の起動が必要となる場合があります)。 AccessibilityService、AutofillService、
    CallRedirectionService、HostApduService、InCallService、TileService、VoiceInteractionService、および VrListenerService。
    • このアプリには、表示されている別のアプリによってバインドされたサービスがある。バックグラウンドのアプリが正常にアクティビティを開始できるようにするため、そのサービスにバインドされ
    ているアプリは表示されたままでなければなりません。
    • このアプリがシステムから通知 PendingIntent を受信している。サービスやブロードキャスト受信プログラムのインテントが保留中の場合は、保留中のインテントが送信されてから数秒間アプリは
    アクティビティを開始できます。
    • 表示されている別のアプリから送信された PendingIntent をこのアプリが受信している。
    • アプリがシステム ブロードキャストを受信し、そのための UI を起動することになっている。その例として、ACTION_NEW_OUTGOING_CALL と SECRET_CODE_ACTION があります。アプリは、ブ
    ロードキャストが送信されてから数秒間はアクティビティを開始できます。
    • アプリが CompanionDeviceManager API によって付属ハードウェア端末に関連付けられている。この API により、アプリはペア設定した端末でユーザーが実行するアクションに反応して、アクティ
    ビティを開始できます。
    • アプリが端末所有者モードで実行されている端末ポリシー コントローラである。使用例には、完全管理対象エンタープライズ端末だけでなく、デジタル サイネージやキオスクなどの専用端末も含ま
    れています。
    • ユーザーによってアプリに SYSTEM_ALERT_WINDOW パーミッションが付与されている。
    バックグラウンドからの起動条件
    https://developer.android.com/guide/components/activities/background-starts

    View Slide

  69. •アプリのウィンドウが、フォアグラウンドのアクティビティであるなど、画面上に表示されている。
    •アプリのアクティビティが、フォアグラウンド タスクのバック スタックにある。
    •アプリのアクティビティが、最近使ったアプリ画面の既存タスクのバックスタックにある。
    •ごく最近開始した、このアプリのアクティビティがある。
    •ごく最近、このアプリがアクティビティで finish() を呼び出した。これは、finish() が呼び出されたと
    きに、このアプリのアクティビティがフォアグラウンドにあった、またはフォアグラウンド タスクの
    バック スタックにあった場合のみに適用されます。
    •システムによってバインドされたサービスがこのアプリにある。この条件は次のサービスにのみ適用さ
    れます(UI の起動が必要となる場合があります)。 AccessibilityService、AutofillService、
    CallRedirectionService、HostApduService、InCallService、TileService、VoiceInteractionService、
    および VrListenerService。
    バックグラウンドからの起動条件

    View Slide

  70. •システムによってバインドされたサービスがこのアプリにある。この条件は次のサービスにのみ適用さ
    れます(UI の起動が必要となる場合があります)。 AccessibilityService、AutofillService、
    CallRedirectionService、HostApduService、InCallService、TileService、VoiceInteractionService、
    および VrListenerService。
    •このアプリには、表示されている別のアプリによってバインドされたサービスがある。バックグラウン
    ドのアプリが正常にアクティビティを開始できるようにするため、そのサービスにバインドされている
    アプリは表示されたままでなければなりません。
    •このアプリがシステムから通知 PendingIntent を受信している。サービスやブロードキャスト受信プロ
    グラムのインテントが保留中の場合は、保留中のインテントが送信されてから数秒間アプリはアクティ
    ビティを開始できます。
    •表示されている別のアプリから送信された PendingIntent をこのアプリが受信している。
    •アプリがシステム ブロードキャストを受信し、そのための UI を起動することになっている。その例と
    して、ACTION_NEW_OUTGOING_CALL と SECRET_CODE_ACTION があります。アプリは、ブロード
    キャストが送信されてから数秒間はアクティビティを開始できます。
    バックグラウンドからの起動条件
    https://developer.android.com/guide/components/activities/background-starts

    View Slide

  71. Shared Element
    To fragment

    using the navigation
    https://www.instagram.com/p/B4-JtNegAZp/

    View Slide

  72. Shared Element
    FragmentのTransitionは
    正直しんどかったけど
    Navigationによって(少し)楽になった!
    x0.5倍速

    View Slide


  73. xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    app:startDestination="@id/navigationMainFragment"
    >
    android:id="@+id/navigationHogeFragment"
    android:name="HogeFragment"
    android:label="HogeFragment"
    >
    android:name="arg1"
    app:argType="integer"
    />
    android:name="arg2"
    app:argType="integer"
    />


    Shared Element
    navigation.xml

    View Slide

  74. xmlns:app="http://schemas.android.com/apk/res-auto"
    app:startDestination="@id/navigationMainFragment"
    >
    android:id="@+id/navigationHogeFragment"
    android:name="HogeFragment"
    android:label="HogeFragment"
    >
    android:name="arg1"
    app:argType="integer"
    />
    android:name="arg2"
    app:argType="integer"
    />


    Shared Element
    navigation.xml

    View Slide

  75. Shared Element
    ViewCompat.setTransitionName(containerLayout, TRANSITION_KEY_CONTAINER)
    ViewCompat.setTransitionName(previewImage, TRANSITION_KEY_IMAGE)
    ViewCompat.setTransitionName(titleText, TRANSITION_KEY_TITLE)
    findNavController().navigate(
    R.id.navigationHogeFragment,
    HogeFragmentArgs(hogeArgs1, hogeArgs2).toBundle(),
    null,
    FragmentNavigatorExtras(
    containerLayout to containerLayout.transitionName,
    previewImage to previewImage.transitionName,
    titleText to titleText.transitionName
    )
    )
    遷移元

    View Slide

  76. Shared Element
    ViewCompat.setTransitionName(containerLayout, TRANSITION_KEY_CONTAINER)
    ViewCompat.setTransitionName(previewImage, TRANSITION_KEY_IMAGE)
    ViewCompat.setTransitionName(titleText, TRANSITION_KEY_TITLE)
    findNavController().navigate(
    R.id.navigationHogeFragment,
    HogeFragmentArgs(hogeArgs1, hogeArgs2).toBundle(),
    null,
    FragmentNavigatorExtras(
    containerLayout to containerLayout.transitionName,
    previewImage to previewImage.transitionName,
    titleText to titleText.transitionName
    )
    )
    いつも通りKeyをセット(xmlでも良い)
    遷移元

    View Slide

  77. Shared Element
    findNavController().navigate(
    R.id.navigationHogeFragment,
    HogeFragmentArgs(hogeArgs1, hogeArgs2).toBundle(),
    null,
    FragmentNavigatorExtras(
    containerLayout to containerLayout.transitionName,
    previewImage to previewImage.transitionName,
    titleText to titleText.transitionName
    )
    )
    Destination Id
    遷移元

    View Slide

  78. Shared Element
    findNavController().navigate(
    R.id.navigationHogeFragment,
    HogeFragmentArgs(hogeArgs1, hogeArgs2).toBundle(),
    null,
    FragmentNavigatorExtras(
    containerLayout to containerLayout.transitionName,
    previewImage to previewImage.transitionName,
    titleText to titleText.transitionName
    )
    )
    Safe args
    遷移元

    View Slide

  79. Shared Element
    findNavController().navigate(
    R.id.navigationHogeFragment,
    HogeFragmentArgs(hogeArgs1, hogeArgs2).toBundle(),
    null,
    FragmentNavigatorExtras(
    containerLayout to containerLayout.transitionName,
    previewImage to previewImage.transitionName,
    titleText to titleText.transitionName
    )
    )
    NavOptions
    遷移元

    View Slide

  80. Shared Element
    findNavController().navigate(
    R.id.navigationHogeFragment,
    HogeFragmentArgs(hogeArgs1, hogeArgs2).toBundle(),
    null,
    FragmentNavigatorExtras(
    containerLayout to containerLayout.transitionName,
    previewImage to previewImage.transitionName,
    titleText to titleText.transitionName
    )
    )
    ここ大事!
    今まで通りTransition nameを設定する 遷移元

    View Slide

  81. Shared Element
    遷移先
    override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
    ViewCompat.setTransitionName(destContainerLayout, TRANSITION_KEY_CONTAINER)
    ViewCompat.setTransitionName(destPreviewImage, TRANSITION_KEY_IMAGE)
    ViewCompat.setTransitionName(destTitleText, TRANSITION_KEY_TITLE)
    }
    override fun onCreate(savedInstanceState: Bundle?) {
    super.onCreate(savedInstanceState)
    sharedElementEnterTransition = TransitionSet()
    sharedElementReturnTransition = TransitionSet()
    }

    View Slide

  82. Shared Element
    遷移先
    override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
    ViewCompat.setTransitionName(destContainerLayout, TRANSITION_KEY_CONTAINER)
    ViewCompat.setTransitionName(destPreviewImage, TRANSITION_KEY_IMAGE)
    ViewCompat.setTransitionName(destTitleText, TRANSITION_KEY_TITLE)
    override fun onCreate(savedInstanceState: Bundle?) {
    super.onCreate(savedInstanceState)
    sharedElementEnterTransition = TransitionSet()
    sharedElementReturnTransition = TransitionSet()
    }
    onCreateで設定
    カスタムTransitionにしたい場合はここを変更する

    View Slide

  83. sharedElementEnterTransition = TransitionSet()
    sharedElementReturnTransition = TransitionSet()
    }
    Shared Element
    遷移先
    override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
    ViewCompat.setTransitionName(destContainerLayout, TRANSITION_KEY_CONTAINER
    ViewCompat.setTransitionName(destPreviewImage, TRANSITION_KEY_IMAGE)
    ViewCompat.setTransitionName(destTitleText, TRANSITION_KEY_TITLE)
    }
    いつも通りKeyをセット(xmlでも良い)

    View Slide

  84. sharedElementEnterTransition = TransitionSet()
    sharedElementReturnTransition = TransitionSet()
    }
    Shared Element
    遷移先
    override fun onViewCreated(view: View, savedInstanceState: Bundle?)
    postponeEnterTransition()
    destContainerLayout.doOnPreDraw {
    startPostponedEnterTransition()
    }
    ViewCompat.setTransitionName(destContainerLayout, TRANSITION_KEY_C
    ViewCompat.setTransitionName(destPreviewImage, TRANSITION_KEY_IMAG
    ViewCompat.setTransitionName(destTitleText, TRANSITION_KEY_TITLE)
    }
    RecyclerViewとかを使っていると動かない時がある
    描画まで待つ

    View Slide

  85. sharedElementEnterTransition = TransitionSet()
    sharedElementReturnTransition = TransitionSet()
    }
    Shared Element
    遷移先
    override fun onViewCreated(view: View, savedInstanceState: Bundle?)
    postponeEnterTransition()
    destContainerLayout.doOnPreDraw {
    startPostponedEnterTransition()
    }
    ViewCompat.setTransitionName(destContainerLayout, TRANSITION_KEY_C
    ViewCompat.setTransitionName(destPreviewImage, TRANSITION_KEY_IMAG
    ViewCompat.setTransitionName(destTitleText, TRANSITION_KEY_TITLE)
    }
    描画されたらTransition開始

    View Slide

  86. Transition Animation
    https://www.instagram.com/p/B4IC2J8gYnI/

    View Slide

  87. Transition Animation
    • Radiusがかかったアイテムから

    かかっていないアイテムへの遷移で

    角丸がピッってなる問題
    • 背景、両サイドのアイテムが

    外側に逃げていく

    View Slide

  88. Transition Animation
    遷移先
    override fun onCreate(savedInstanceState: Bundle?) {
    super.onCreate(savedInstanceState)
    sharedElementEnterTransition = TransitionSet()
    sharedElementReturnTransition = TransitionSet()
    }
    ここをカスタムしていく

    View Slide

  89. Transition Animation
    https://speakerdeck.com/magiepooh/jin-sarajiao-wan-falsetransition

    View Slide

  90. Transition Animation
    https://github.com/android/animation-samples/tree/master/Motion
    Explode animationは
    Googleのanimation-samplesにある
    Motionがわかりやすい

    View Slide

  91. Transition Animation
    private fun createSharedElementTransition(duration: Long): Transition {
    return transitionTogether {
    this.duration = duration
    interpolator = FAST_OUT_SLOW_IN
    this += SharedFade()
    this += ChangeImageTransform()
    this += ChangeBounds()
    this += ChangeTransform()
    }
    }
    operator fun TransitionSet.plusAssign(transition: Transition) {
    addTransition(transition)
    }

    View Slide

  92. Transition Animation
    private fun createSharedElementTransition(duration: Long): Transition {
    return transitionTogether {
    this.duration = duration
    interpolator = FAST_OUT_SLOW_IN
    this += SharedFade()
    this += ChangeImageTransform()
    this += ChangeBounds()
    this += ChangeTransform()
    }
    }
    operator fun TransitionSet.plusAssign(transition: Transition) {
    addTransition(transition)
    }
    custom operator

    View Slide

  93. Transition Animation
    private fun createSharedElementTransition(duration: Long): Transition {
    return transitionTogether {
    this.duration = duration
    interpolator = FAST_OUT_SLOW_IN
    this += SharedFade()
    this += ChangeImageTransform()
    this += ChangeBounds()
    this += ChangeTransform()
    }
    }
    operator fun TransitionSet.plusAssign(transition: Transition) {
    addTransition(transition)
    }
    こんな感じで書ける

    View Slide

  94. private fun createSharedElementTransition(duration: Long): Transition {
    return transitionTogether {
    this.duration = duration
    interpolator = FAST_OUT_SLOW_IN
    addTransition(SharedFade())
    addTransition(ChangeImageTransform())
    addTransition(ChangeBounds())
    addTransition(ChangeTransform())
    }
    }
    Transition Animation
    operator fun TransitionSet.plusAssign(transition: Transition) {
    addTransition(transition)
    }

    View Slide

  95. Animated vector
    drawable
    https://www.instagram.com/p/B4fKculAVwI/

    View Slide

  96. Animated Vector Drawable
    アプリをちょっとおしゃれに見せる
    Vectorなので容量を食わない

    View Slide

  97. Animated Vector Drawable
    SVGを作ってShape Shifterで編集
    https://shapeshifter.design/

    View Slide

  98. https://www.instagram.com/p/B3SBtgag64W/
    View Pager2

    View Slide

  99. View Pager2 (Loop)
    class MyAdapter(
    private val itemData: List
    ) : RecyclerView.Adapter() {
    override fun getItemCount(): Int = Int.MAX_VALUE
    override fun onCreateViewHolder(parent: ViewGroup, viewType: Int)
    override fun onBindViewHolder(holder: MyViewHolder, position: Int) {
    val data = itemData[position % itemData.size]
    ...
    }
    }
    RecyclerAdapter

    View Slide

  100. View Pager2 (Loop)
    class MyAdapter(
    private val itemData: List
    ) : RecyclerView.Adapter() {
    override fun getItemCount(): Int = Int.MAX_VALUE
    override fun onCreateViewHolder(parent: ViewGroup, viewType: Int)
    override fun onBindViewHolder(holder: MyViewHolder, position: Int) {
    val data = itemData[position % itemData.size]
    ...
    }
    }
    Maxにする
    RecyclerAdapter

    View Slide

  101. View Pager2 (Loop)
    class MyAdapter(
    private val itemData: List
    ) : RecyclerView.Adapter() {
    override fun getItemCount(): Int = Int.MAX_VALUE
    override fun onCreateViewHolder(parent: ViewGroup, viewType: Int)
    override fun onBindViewHolder(holder: MyViewHolder, position: Int) {
    val data = itemData[position % itemData.size]
    ...
    }
    }
    positionを配列幅でmod
    RecyclerAdapter

    View Slide

  102. View Pager2 (Loop)
    viewPager.also {
    it.adapter = adapter
    val center = Int.MAX_VALUE / 2
    val start = center - (center % items.size)
    it.setCurrentItem(start, false)
    }
    Activitym, Fragment側

    View Slide

  103. viewPager.also {
    it.adapter = adapter
    val center = Int.MAX_VALUE / 2
    val start = center - (center % items.size)
    it.setCurrentItem(start, false)
    }
    View Pager2 (Loop)
    Activitym, Fragment側
    先頭を中心にする

    View Slide

  104. viewPager.also {
    it.adapter = adapter
    val center = Int.MAX_VALUE / 2
    val start = center - (center % items.size)
    it.setCurrentItem(start, false)
    }
    View Pager2 (Loop)
    Activitym, Fragment側
    スワイプの上限

    View Slide

  105. View Pager2 (ひょっこりはん)
    左右にチラ見せするやつ
    みんな大好きカルーセル
    (とりあえずいい感じに情報を見せたい時に

    使われがちなところがある)

    View Slide

  106. android:id="@+id/viewPager"
    android:layout_width="match_parent"
    android:layout_height="200dp"
    android:orientation="horizontal"
    />
    View Pager2 (ひょっこりはん)
    4dp
    64dp
    68dp

    View Slide

  107. />
    View Pager2 (ひょっこりはん)
    4dp
    64dp
    68dp
    android:id="@+id/cardView"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:layout_marginStart="@dimen/page_margin_and_offset"
    android:layout_marginEnd="@dimen/page_margin_and_offset"
    app:cardCornerRadius="@dimen/item_card_radius"
    >

    View Slide

  108. android:orientation="horizontal"
    />
    View Pager2 (ひょっこりはん)
    4dp
    64dp
    68dp
    android:id="@+id/cardView"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:layout_marginStart="@dimen/page_margin_and_offset"
    android:layout_marginEnd="@dimen/page_margin_and_offset"
    app:cardCornerRadius="@dimen/item_card_radius"
    >

    View Slide

  109. android:id="@+id/cardView"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:layout_marginStart="@dimen/page_margin_and_offset"
    android:layout_marginEnd="@dimen/page_margin_and_offset"
    app:cardCornerRadius="@dimen/item_card_radius"
    >
    android:orientation="horizontal"
    />
    View Pager2 (ひょっこりはん)
    4dp
    64dp
    68dp
    margin + offset

    View Slide

  110. android:id="@+id/cardView"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:layout_marginStart="@dimen/page_margin_and_offset"
    android:layout_marginEnd="@dimen/page_margin_and_offset"
    app:cardCornerRadius="@dimen/item_card_radius"
    >
    android:orientation="horizontal"
    />
    View Pager2 (ひょっこりはん)
    4dp
    64dp
    68dp

    View Slide

  111. viewpager.offscreenPageLimit = 2
    val pageMarginPx = resources.getDimensionPixelOffset(R.dimen.page_margin)
    val offsetPx = resources.getDimensionPixelOffset(R.dimen.page_offset)

    viewpager.setPageTransformer(CompositePageTransformer().apply {
    addTransformer { page, position ->
    val offset = position * (2 * offsetPx + pageMarginPx)
    page.translationX = -offset
    val scale = 1 - (abs(position) / 6)
    page.scaleX = scale
    page.scaleY = scale
    }
    })
    android:orientation="horizontal"
    />
    View Pager2 (ひょっこりはん)

    View Slide

  112. viewpager.offscreenPageLimit = 2
    val pageMarginPx = resources.getDimensionPixelOffset(R.dimen.page_margin)
    val offsetPx = resources.getDimensionPixelOffset(R.dimen.page_offset)

    viewpager.setPageTransformer(CompositePageTransformer().apply {
    addTransformer { page, position ->
    val offset = position * (2 * offsetPx + pageMarginPx)
    page.translationX = -offset
    val scale = 1 - (abs(position) / 6)
    page.scaleX = scale
    page.scaleY = scale
    }
    })
    android:orientation="horizontal"
    />
    View Pager2 (ひょっこりはん)
    左右のアイテム描画

    View Slide

  113. viewpager.offscreenPageLimit = 2
    val pageMarginPx = resources.getDimensionPixelOffset(R.dimen.page_margin)
    val offsetPx = resources.getDimensionPixelOffset(R.dimen.page_offset)

    viewpager.setPageTransformer(CompositePageTransformer().apply {
    addTransformer { page, position ->
    val offset = position * (2 * offsetPx + pageMarginPx)
    page.translationX = -offset
    val scale = 1 - (abs(position) / 6)
    page.scaleX = scale
    page.scaleY = scale
    }
    })
    android:orientation="horizontal"
    />
    View Pager2 (ひょっこりはん)
    Swipe時のアニメーションを自由に作れる

    View Slide

  114. View Pager2
    https://satoshun.github.io/2019/10/viewpager2-loop/
    https://satoshun.github.io/2019/08/viewpager2-like-a-carousel/

    View Slide

  115. Window Insets
    https://www.instagram.com/p/B1bZtyEHHG8/

    View Slide

  116. WindowInsets
    https://github.com/chrisbanes/insetter
    Insetter使っておけばおk 結構楽に出来るようになった

    View Slide

  117. WindowInsets
    https://github.com/chrisbanes/insetter
    Insetter使っておけばおk 結構楽に出来るようになった
    WindowInsetsは4系を切るだけでDXが全然違う@A

    View Slide

  118. Bottom app bar
    https://www.instagram.com/p/B0_RDbaAQ6D/

    View Slide

  119. Bottom app bar
    去年ぐらいにマテリアルデザインに
    追加されたToolbarの下バージョン
    FabがBottomAppBarに食い込む

    View Slide

  120. Bottom app bar
    android:id="@+id/bottomBar"
    android:layout_width="match_parent"
    android:layout_height="@dimen/bottom_bar_height"
    app:fabAlignmentMode="center"
    app:navigationIcon="@drawable/ic_menu_white"
    />
    android:id="@+id/bottomNavigation"
    android:layout_width="match_parent"
    android:layout_height="@dimen/bottom_bar_height"
    app:menu="@menu/menu_navigation"
    />
    •Bottom App Bar
    •Bottom Navigation View

    View Slide

  121. android:id="@+id/bottomNavigation"
    android:layout_width="match_parent"
    android:layout_height="@dimen/bottom_bar_height"
    app:menu="@menu/menu_navigation"
    />
    Bottom app bar
    android:id="@+id/bottomBar"
    android:layout_width="match_parent"
    android:layout_height="@dimen/bottom_bar_height"
    app:fabAlignmentMode="center"
    app:navigationIcon="@drawable/ic_menu_white"
    />
    •Bottom App Bar
    •Bottom Navigation View
    xmlでアイコンセット出来る

    View Slide

  122. android:id="@+id/bottomNavigation"
    android:layout_width="match_parent"
    android:layout_height="@dimen/bottom_bar_height"
    app:menu="@menu/menu_navigation"
    />
    Bottom app bar
    android:id="@+id/bottomBar"
    android:layout_width="match_parent"
    android:layout_height="@dimen/bottom_bar_height"
    app:fabAlignmentMode="center"
    app:navigationIcon="@drawable/ic_menu_white"
    />
    •Bottom App Bar
    •Bottom Navigation View
    fabを食い込ませる

    View Slide

  123. android:id="@+id/bottomNavigation"
    android:layout_width="match_parent"
    android:layout_height="@dimen/bottom_bar_height"
    app:menu="@menu/menu_navigation"
    />
    Bottom app bar
    android:id="@+id/bottomBar"
    android:layout_width="match_parent"
    android:layout_height="@dimen/bottom_bar_height"
    app:fabAlignmentMode="end"
    app:navigationIcon="@drawable/ic_menu_white"
    />
    •Bottom App Bar
    •Bottom Navigation View
    endにすると端にずらせる(startは不可)

    View Slide

  124. android:id="@+id/fab"
    android:layout_width="wrap_content"
    android:layout_height="wrap_content"
    app:layout_anchor="@id/bottomBar"
    />
    Bottom app bar
    android:id="@+id/bottomBar"
    android:layout_width="match_parent"
    android:layout_height="@dimen/bottom_bar_height"
    app:fabAlignmentMode="end"
    app:navigationIcon="@drawable/ic_menu_white"
    />

    View Slide

  125. android:id="@+id/fab"
    android:layout_width="wrap_content"
    android:layout_height="wrap_content"
    app:layout_anchor="@id/bottomBar"
    />
    Bottom app bar
    android:id="@+id/bottomBar"
    android:layout_width="match_parent"
    android:layout_height="@dimen/bottom_bar_height"
    app:fabAlignmentMode="end"
    app:navigationIcon="@drawable/ic_menu_white"
    /> fabをBottomAppBarに紐付ける

    View Slide

  126. Bottom app bar
    • 最近のマテってるデザインは

    下から出てきがち
    • Android10からのジェスチャーとの

    コンフリクトの影響だと思ってる

    View Slide

  127. class MyBottomSheetFragment : BottomSheetDialogFragment() {
    companion object {
    fun newInstance(): BottomSheetNavigationFragment =
    BottomSheetNavigationFragment()
    }
    ...
    }
    Bottom app bar
    bottomBar.setNavigationOnClickListener {
    MyBottomSheetFragment.newInstance()
    .show(supportFragmentManager, TAG_BOTTOM_SHEET)
    }
    MyBottomSheetFragment.kt
    MainActivity.kt

    View Slide

  128. class MyBottomSheetFragment : BottomSheetDialogFragment() {
    companion object {
    fun newInstance(): BottomSheetNavigationFragment =
    BottomSheetNavigationFragment()
    }
    ...
    }
    Bottom app bar
    bottomBar.setNavigationOnClickListener {
    MyBottomSheetFragment.newInstance()
    .show(supportFragmentManager, TAG_BOTTOM_SHEET)
    }
    MyBottomSheetFragment.kt
    MainActivity.kt
    これだけで呼び出せる

    View Slide

  129. https://www.instagram.com/p/B2HUrCxg12g/
    Summary

    View Slide

  130. まとめ
    • 今でも結構ハマることはたくさんある

    全部覚えてない
    • 昔と比べて便利になったなー

    久々にアプリ作って楽しかった

    やっていきB

    View Slide