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

Daec7e5cd5fae384eda88037d937343b?s=47 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/

Daec7e5cd5fae384eda88037d937343b?s=128

AAkira

November 21, 2019
Tweet

Transcript

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

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

    ✈;<=⊿ https://aakira.studio aakira.studio
  3. None
  4. ことの発端 ある日、こんなメールが来た

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

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

  7. ことの発端 $ 40,000 = 40,000 * 109 = ¥ 4,360,000

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

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

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


    40k以上は確実だよ 発表時だけの秘密♡
  11. ことの発端 < アプリとソースコードだけくれればいいよ < 買ってどうするの? < (答えになっていない...) < 代わりにアプデしてくれるの?
  そしたらソースコードもいるよね?

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

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

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

  15. None
  16. まとめ • 買って何をするのか具体的に教えてくれないので、
 ちょっと怪しい • この人はバイヤーなので、裏に誰かがいる? • TOP5 countries [!>-"?]

    で
 50,000ユーザぐらいいれば4~500万円を手にすることが出来る
 (可能性がある) 発表時だけの秘密♡
  17. 意外とこういうアプリでも需要があるんだなー

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

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

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

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

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

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

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

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

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

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

  28. いろいろやったリスト • QuickSettingsTiles(通知エリアのタイルボタン) • Palette • WindowManager • バックグラウンド •

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

    SharedElement • Transition animation(Explode, 角丸ピッ) • WindowInsets • AnimatedVectorDrawable • BottomAppBar • ViewPager2 (Infinity Loop, ひょっこりはん) 上3つは話す予定
 あとは挙手制?
  30. Quick settings Tile https://www.instagram.com/p/B2ZWvA2AkRY/

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

  32. Quick Settings Tile Notification Quick Settings Tile

  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() // ௨஌ΤϦΞ಺ͷλΠϧ͕ݟ͑ͳ͘ͳͬͨ࣌ } }
  34. class MyTileService: TileService(){ override fun onClick() { super.onClick() // ΫϦοΫ࣌ʹݺ͹ΕΔ

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

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

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

    onStartListening() { super.onStartListening() // ௨஌ΤϦΞ಺ͷλΠϧ͕දࣔ͞Εͨ࣌ } override fun onStopListening() { super.onStopListening() // ௨஌ΤϦΞ಺ͷλΠϧ͕ݟ͑ͳ͘ͳͬͨ࣌ } } Quick Settings Tile ここでState Checkした方が良い
  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 通知エリアの状態表示
  39. override fun onTileAdded() { super.onTileAdded() // ฤूը໘͔ΒλΠϧ௥Ճ࣌ʹݺ͹ΕΔ } override fun

    onStartListening() { super.onStartListening() // ௨஌ΤϦΞ಺ͷλΠϧ͕දࣔ͞Εͨ࣌ } override fun onStopListening() { super.onStopListening() // ௨஌ΤϦΞ಺ͷλΠϧ͕ݟ͑ͳ͘ͳͬͨ࣌ } } Quick Settings Tile
  40. Quick Settings Tile <service 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">

    <intent-filter> <action android:name="android.service.quicksettings.action.QS_TILE" /> </intent-filter> </service> AndroidManifest.xml
  41. Quick Settings Tile <service 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">

    <intent-filter> <action android:name="android.service.quicksettings.action.QS_TILE" /> </intent-filter> </service> 作成したService class AndroidManifest.xml
  42. Quick Settings Tile <service 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">

    <intent-filter> <action android:name="android.service.quicksettings.action.QS_TILE" /> </intent-filter> </service> QuickSettingsに表示するアイコン AndroidManifest.xml
  43. Quick Settings Tile <service 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">

    <intent-filter> <action android:name="android.service.quicksettings.action.QS_TILE" /> </intent-filter> </service> QuickSettingsに表示するラベル AndroidManifest.xml
  44. Palette https://www.instagram.com/p/B0gJPMsg7VK/ 知られざる名機能

  45. Palette SupportLibrary v7時代に追加
 背景画像から "良い感じな" 色を抽出

  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) }
  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)
  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 同期取得
  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
  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 異なる鮮やかさが取れる
  51. Palette https://developer.android.com/training/material/palette-colors

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

  53. 画面にフィルターを重ねる <uses-permission android:name="android.permission.SYSTEM_ALERT_WINDOW" /> •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名変更 <uses-permission android:name="android.permission.FOREGROUND_SERVICE" />
  54. 画面にフィルターを重ねる TYPE_APPLICATION TYPE_PHONE TYPE_TOAST TYPE_SYSTEM_ALERT KEY_GUARD STATUS_BAR TYPE_SYSTEM_OVERLAY

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

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

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

  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)
  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指定
  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以降では落ちる
  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)
  62. 画面にフィルターを重ねる • FLAG_NOT_TOUCHABLE
 タッチ判定を貫通する。
 Messengerアプリみたいなものは設定していない • FLAG_NOT_FOCUSABLE
 フォーカスをあてない
 設定しないとフォーカスが当たるのでBackボタンのイベントが取られる •

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

  64. いろいろやったリスト • QuickSettingsTiles(通知エリアのタイルボタン) • Palette • WindowManager • バックグラウンド •

    SharedElement • Transition animation(Explode, 角丸ピッ) • WindowInsets • AnimatedVectorDrawable • BottomAppBar • ViewPager2 (Infinity Loop, ひょっこりはん)
  65. https://www.instagram.com/p/B3rzRAGAfqi/ Background

  66. バックグラウンド •Oreo(26)まではこれだけ <uses-permission android:name="android.permission.FOREGROUND_SERVICE" /> startForeground(1, notification) •Pie~ startForeground(1, notification)

  67. バックグラウンド •Oreo(26)まではこれだけ <uses-permission android:name="android.permission.FOREGROUND_SERVICE" /> startForeground(1, notification) •Pie~ startForeground(1, notification)

    5秒以内に呼ぶ
  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
  69. •アプリのウィンドウが、フォアグラウンドのアクティビティであるなど、画面上に表示されている。 •アプリのアクティビティが、フォアグラウンド タスクのバック スタックにある。 •アプリのアクティビティが、最近使ったアプリ画面の既存タスクのバックスタックにある。 •ごく最近開始した、このアプリのアクティビティがある。 •ごく最近、このアプリがアクティビティで finish() を呼び出した。これは、finish() が呼び出されたと

    きに、このアプリのアクティビティがフォアグラウンドにあった、またはフォアグラウンド タスクの バック スタックにあった場合のみに適用されます。 •システムによってバインドされたサービスがこのアプリにある。この条件は次のサービスにのみ適用さ れます(UI の起動が必要となる場合があります)。 AccessibilityService、AutofillService、 CallRedirectionService、HostApduService、InCallService、TileService、VoiceInteractionService、 および VrListenerService。 バックグラウンドからの起動条件
  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
  71. Shared Element To fragment
 using the navigation https://www.instagram.com/p/B4-JtNegAZp/

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

  73. <?xml version="1.0" encoding="utf-8"?> <navigation xmlns:android="http://schemas.android.com/apk/res/android" xmlns:app="http://schemas.android.com/apk/res-auto" app:startDestination="@id/navigationMainFragment" > <fragment android:id="@+id/navigationHogeFragment"

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

    app:argType="integer" /> <argument android:name="arg2" app:argType="integer" /> </fragment> </navigation> Shared Element navigation.xml
  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 ) ) 遷移元
  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でも良い) 遷移元
  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 遷移元
  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 遷移元
  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 遷移元
  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を設定する 遷移元
  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() }
  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にしたい場合はここを変更する
  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でも良い)
  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とかを使っていると動かない時がある 描画まで待つ
  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開始
  86. Transition Animation https://www.instagram.com/p/B4IC2J8gYnI/

  87. Transition Animation • Radiusがかかったアイテムから
 かかっていないアイテムへの遷移で
 角丸がピッってなる問題 • 背景、両サイドのアイテムが
 外側に逃げていく

  88. Transition Animation 遷移先 override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) sharedElementEnterTransition

    = TransitionSet() sharedElementReturnTransition = TransitionSet() } ここをカスタムしていく
  89. Transition Animation https://speakerdeck.com/magiepooh/jin-sarajiao-wan-falsetransition

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

  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) }
  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
  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) } こんな感じで書ける
  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) }
  95. Animated vector drawable https://www.instagram.com/p/B4fKculAVwI/

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

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

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

  99. View Pager2 (Loop) class MyAdapter( private val itemData: List<Data> )

    : RecyclerView.Adapter<MyViewHolder>() { 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
  100. View Pager2 (Loop) class MyAdapter( private val itemData: List<Data> )

    : RecyclerView.Adapter<MyViewHolder>() { 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
  101. View Pager2 (Loop) class MyAdapter( private val itemData: List<Data> )

    : RecyclerView.Adapter<MyViewHolder>() { 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
  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側
  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側 先頭を中心にする
  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側 スワイプの上限
  105. View Pager2 (ひょっこりはん) 左右にチラ見せするやつ みんな大好きカルーセル (とりあえずいい感じに情報を見せたい時に
 使われがちなところがある)

  106. <androidx.viewpager2.widget.ViewPager2 android:id="@+id/viewPager" android:layout_width="match_parent" android:layout_height="200dp" android:orientation="horizontal" /> View Pager2 (ひょっこりはん) <dimen

    name="page_margin">4dp</dimen> <dimen name="page_offset">64dp</dimen> <dimen name="page_margin_and_offset">68dp</dimen>
  107. /> View Pager2 (ひょっこりはん) <dimen name="page_margin">4dp</dimen> <dimen name="page_offset">64dp</dimen> <dimen name="page_margin_and_offset">68dp</dimen>

    <com.google.android.material.card.MaterialCardView 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" >
  108. android:orientation="horizontal" /> View Pager2 (ひょっこりはん) <dimen name="page_margin">4dp</dimen> <dimen name="page_offset">64dp</dimen> <dimen

    name="page_margin_and_offset">68dp</dimen> <com.google.android.material.card.MaterialCardView 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" >
  109. <com.google.android.material.card.MaterialCardView 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 (ひょっこりはん) <dimen name="page_margin">4dp</dimen> <dimen name="page_offset">64dp</dimen> <dimen name="page_margin_and_offset">68dp</dimen> margin + offset
  110. <com.google.android.material.card.MaterialCardView 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 (ひょっこりはん) <dimen name="page_margin">4dp</dimen> <dimen name="page_offset">64dp</dimen> <dimen name="page_margin_and_offset">68dp</dimen>
  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 (ひょっこりはん)
  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 (ひょっこりはん) 左右のアイテム描画
  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時のアニメーションを自由に作れる
  114. View Pager2 https://satoshun.github.io/2019/10/viewpager2-loop/ https://satoshun.github.io/2019/08/viewpager2-like-a-carousel/

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

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

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

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

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

  120. Bottom app bar <com.google.android.material.bottomappbar.BottomAppBar 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" />

    <com.google.android.material.bottomnavigation.BottomNavigationView 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
  121. <com.google.android.material.bottomnavigation.BottomNavigationView android:id="@+id/bottomNavigation" android:layout_width="match_parent" android:layout_height="@dimen/bottom_bar_height" app:menu="@menu/menu_navigation" /> Bottom app bar <com.google.android.material.bottomappbar.BottomAppBar

    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でアイコンセット出来る
  122. <com.google.android.material.bottomnavigation.BottomNavigationView android:id="@+id/bottomNavigation" android:layout_width="match_parent" android:layout_height="@dimen/bottom_bar_height" app:menu="@menu/menu_navigation" /> Bottom app bar <com.google.android.material.bottomappbar.BottomAppBar

    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を食い込ませる
  123. <com.google.android.material.bottomnavigation.BottomNavigationView android:id="@+id/bottomNavigation" android:layout_width="match_parent" android:layout_height="@dimen/bottom_bar_height" app:menu="@menu/menu_navigation" /> Bottom app bar <com.google.android.material.bottomappbar.BottomAppBar

    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は不可)
  124. <com.google.android.material.floatingactionbutton.FloatingActionButton android:id="@+id/fab" android:layout_width="wrap_content" android:layout_height="wrap_content" app:layout_anchor="@id/bottomBar" /> Bottom app bar <com.google.android.material.bottomappbar.BottomAppBar

    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" />
  125. <com.google.android.material.floatingactionbutton.FloatingActionButton android:id="@+id/fab" android:layout_width="wrap_content" android:layout_height="wrap_content" app:layout_anchor="@id/bottomBar" /> Bottom app bar <com.google.android.material.bottomappbar.BottomAppBar

    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に紐付ける
  126. Bottom app bar • 最近のマテってるデザインは
 下から出てきがち • Android10からのジェスチャーとの
 コンフリクトの影響だと思ってる

  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
  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 これだけで呼び出せる
  129. https://www.instagram.com/p/B2HUrCxg12g/ Summary

  130. まとめ • 今でも結構ハマることはたくさんある
 全部覚えてない • 昔と比べて便利になったなー • 久々にアプリ作って楽しかった
 やっていきB