Upgrade to Pro — share decks privately, control downloads, hide ads and more …

詳解 WindowInsets

詳解 WindowInsets

新型コロナウイルスによって中止となった DroidKaigi 2020 にて2日目11:00より発表予定だった「詳解 WindowInsets」の資料です

Yuta Takahashi

February 21, 2020
Tweet

More Decks by Yuta Takahashi

Other Decks in Programming

Transcript

  1. 詳解
 WindowInsets Yuta Takahashi

  2. None
  3. 髙橋 佑太 Yuta Takahashi 2019年6⽉より株式会社 justInCase にて Android / Server

    side (Kotlin) の開発に従事。 保険のサービスをつくっています。 @yt_hizi @yt-tkhs
  4. 1. Edge-to-edge について 2. WindowInsets の概要 3. Edge-to-edge をアプリに適⽤する 4.

    WindowInsets のなかみ 5. まとめ アジェンダ
  5. Edge-to-edge について

  6. Edge-to-edge System Bar の背後を含む画⾯全体にわたって アプリのコンテンツを表⽰すること
 
 より没⼊感のあるアプリ体験を
 提供することができる Status Bar

    Navigation Bar
  7. 3-Button 2-Button Android P 〜 〜 Android O

  8. 3-Button 2-Button Gesture Android P 〜 〜 Android O Android

    10
  9. Gesture ・Gesture Navigation の導⼊により
 アプリが利⽤できる画⾯の領域が拡⼤した ・Android 10 からは Edge-to-edge に対応する


    ことが推奨されている ・Android P 以下での対応は任意 Android 10
  10. Edge-to-edge 対応 — Navigation bar 参考: https://developers-jp.googleblog.com/2019/10/gesture-navigation-going-edge-to-edge.html ・Gesture navigation により,

    Navigation bar は
 ⼩さく⽬⽴たなくなったため, 背後にコンテンツを
 描画することが推奨されている
  11. Edge-to-edge 対応 — Status bar ・デザイン上, 有意義である場合にのみ対応すればよい 参考: https://developers-jp.googleblog.com/2019/10/gesture-navigation-going-edge-to-edge.html

  12. WindowInsets の概要

  13. WindowInsets ・System UI のサイズを取得するためのAPI
 ・端末やシステムナビゲーションの種類,
 System Bar の可視性によって値が変わる Top Right

    Left Bottom
  14. WindowInsets の動作 ・親 View から ⼦ View に伝播される
 ・Insets が消費されるとそれ以降


    伝播されない ViewGroup1 ViewGroup2 ViewGroup3 View1 View2 View3
  15. WindowInsets の動作 ViewGroup1 ViewGroup2 ViewGroup3 View1 View2 View3 消費されない場合

  16. WindowInsets の動作 ViewGroup1 ViewGroup2 ViewGroup3 View1 View2 View3 消費されない場合

  17. WindowInsets の動作 ViewGroup1 ViewGroup2 ViewGroup3 View1 View2 View3 消費されない場合

  18. WindowInsets の動作 ViewGroup1 ViewGroup2 ViewGroup3 View1 View2 View3 消費されない場合

  19. WindowInsets の動作 ViewGroup1 ViewGroup2 ViewGroup3 View1 View2 View3 消費されない場合

  20. WindowInsets の動作 ViewGroup1 ViewGroup2 View1 View2 View3 ViewGroup3 消費されない場合

  21. WindowInsets の動作 ViewGroup1 ViewGroup2 ViewGroup3 View1 View2 View3 消費されない場合

  22. WindowInsets の動作 ViewGroup2 で消費された場合 ViewGroup1 ViewGroup2 ViewGroup3 View1 View2 View3

  23. WindowInsets の動作 ViewGroup1 ViewGroup2 ViewGroup3 View1 View2 View3 ViewGroup2 で消費された場合

  24. WindowInsets の動作 ViewGroup1 ViewGroup2 ViewGroup3 View1 View2 View3 ViewGroup2 で消費された場合

  25. WindowInsets の動作 ViewGroup1 ViewGroup2 ViewGroup3 View1 View2 View3 Consume ViewGroup2

    で消費された場合
  26. WindowInsets の動作 ViewGroup1 ViewGroup2 ViewGroup3 View1 View2 View3 Consume ⼦View

    に伝播されない ViewGroup2 で消費された場合
  27. WindowInsets の動作 ViewGroup1 ViewGroup3 View1 View2 View3 ViewGroup2 Consume したことが伝わる

    ViewGroup2 で消費された場合 ⼦View に伝播されない
  28. WindowInsets の動作 ViewGroup1 ViewGroup3 View1 View2 View3 ViewGroup2 同階層のViewに伝播されない Consume

    したことが伝わる ViewGroup2 で消費された場合 ⼦View に伝播されない
  29. Insets の種類 ❶ System Window Insets ❷ System Gesture Insets

    ❸ Mandatory System Gesture Insets ❹ Stable Insets ❺ Tappable Element Insets Window Insets として提供される Insets は5種類
  30. System Window Insets ・表⽰されている Status bar, Navigation bar のサイズ ・Edge-to-edge

    対応の際によくつかう ⚠ View.SYSTEM_UI_FLAG_LAYOUT_STABLE を指定すると⾮表⽰でも取得できる
  31. System Window Insets Gesture Navigation の場合 Bottom Right Top Left

    Status bar の⾼さ Navigation bar の⾼さ 171px 0px 0px 56px ⚠ Pixel 3 XL の場合
  32. System Gesture Insets ・ジェスチャー操作を受け付ける System UI のサイズ ・Status bar や

    Gesture navigation の状態に影響される ・Edge-to-edge 対応の際に⽤いる
  33. System Gesture Insets 171px 84px 84px 112px Status bar の通知パネル表⽰

    Gesture navigation のホーム/アプリ切替 Gesture navigation の Back Gesture navigation の Back Gesture Navigation の場合 Bottom Right Top Left ⚠ Pixel 3 XL の場合 Navigation bar の⾼さより⼤きい
  34. Mandatory System Gesture Insets ・アプリ側でコントロールできない, ジェスチャー操作を
 受け付ける System UI のサイズ

    ・Gesture Navigation の設定によって
 部分的に除外できない領域のみが取得できる
  35. Mandatory System Gesture Insets 171px 0px 0px 112px Status bar

    の通知パネル表⽰ Gesture navigation のホーム/アプリ切替 Bottom Right Top Left Gesture Navigation の場合 View.setSystemGestureExclusionRects()
 によってオーバライド可能 ⚠ Pixel 3 XL の場合
  36. Stable Insets ・Status bar, Navigation bar のサイズ ・System bar の可視性にかかわらず取得できる

    ・View階層の上位にある DecorView で消費されるが
 System Window Insets で代替可能なため使うことはない
  37. Stable Insets Gesture Navigation の場合 Bottom Right Top Left Status

    bar の⾼さ Navigation bar の⾼さ 171px 0px 0px 56px ⚠ Pixel 3 XL の場合
  38. Tappable Element Insets ・すべてのタッチイベントを消費する System UI のサイズ ・ジェスチャーは不可能だが単純なタップは可能なものは
 除外される ・ほとんど使⽤することがない

  39. Tappable Element Insets 171px 0px 0px 0px Status bar の通知パネル表⽰

    Bottom Right Top Left Pixel 3 XL / Gesture Navigation の場合 ⚠ Pixel 3 XL の場合
  40. consume* その他のAPI ・Insets を消費するためのメソッド
 
 ・それ以降の Insets の伝達が⽌まり, Insets が反映されなく

    なる ・将来的になくなる可能性がある (後述) consumeSystemWindowInsets()
 consumeSystemGestureInsets()
  41. Edge-to-edge をアプリに適⽤する

  42. ・androidx.core:core に, バージョン間の差異を
 吸収できる WindowInsetsCompat が⽤意されている ・Gesture Navigation 関連の Insets

    を取得するには
 1.2.0-alpha01 以降を使⽤する必要がある WindowInsets 最新版は 1.3.0-alpha01 (2020年2⽉21⽇現在)
  43. None
  44. dependencies に追加する dependencies { implementation 'androidx.core:core:1.3.0-alpha01' }

  45. systemUiVisibility を設定する window.decorView.systemUiVisibility = View.SYSTEM_UI_FLAG_LAYOUT_STABLE or View.SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN or View.SYSTEM_UI_FLAG_LAYOUT_HIDE_NAVIGATION

  46. systemUiVisibility を設定する window.decorView.systemUiVisibility = View.SYSTEM_UI_FLAG_LAYOUT_STABLE or View.SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN or View.SYSTEM_UI_FLAG_LAYOUT_HIDE_NAVIGATION Status

    bar / Navigation bar が⾮表⽰の場合でも
 Insets の値を取得できるようにする
  47. systemUiVisibility を設定する window.decorView.systemUiVisibility = View.SYSTEM_UI_FLAG_LAYOUT_STABLE or View.SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN or View.SYSTEM_UI_FLAG_LAYOUT_HIDE_NAVIGATION Status

    bar の背後に View を描画する
  48. window.decorView.systemUiVisibility = View.SYSTEM_UI_FLAG_LAYOUT_STABLE or View.SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN or View.SYSTEM_UI_FLAG_LAYOUT_HIDE_NAVIGATION systemUiVisibility を設定する Navigation

    bar の背後に View を描画する
  49. Theme を編集する <style name="AppTheme" parent="Theme.MaterialComponents.Light.NoActionBar"> <item name="android:navigationBarColor">@android:color/transparent</item> <item name="android:statusBarColor">@android:color/transparent</item> </style>

  50. Theme を編集する <style name="AppTheme" parent="Theme.MaterialComponents.Light.NoActionBar"> <item name="android:navigationBarColor">@android:color/transparent</item> <item name="android:statusBarColor">@android:color/transparent</item> </style>

    Navigation bar の背景を透明にする
  51. <style name="AppTheme" parent="Theme.MaterialComponents.Light.NoActionBar"> <item name="android:navigationBarColor">@android:color/transparent</item> <item name="android:statusBarColor">@android:color/transparent</item> </style> Theme を編集する

    Status bar の背景を透明にする
  52. Theme を編集する <style name="AppTheme" parent="Theme.MaterialComponents.Light.NoActionBar"> <item name="android:navigationBarColor">@android:color/transparent</item> <item name="android:statusBarColor">@android:color/transparent</item> </style>

  53. None
  54. ・Toolbar が Status bar と重なる ・FAB が Navigation bar と重なる

    ・RecyclerView が Navigation bar と重なる (⼀番下までスクロールしたとき) WindowInsets を使って解消していく
  55. 重なりの解消 − Toolbar Toolbar の padding に Status barの⾼さ分を
 セットしてあげればよさそう

  56. 重なりの解消 − Toolbar ViewCompat.setOnApplyWindowInsetsListener(binding.toolbar) { _, insets -> binding.toolbar.updatePadding( left

    = insets.systemWindowInsets.left, top = insets.systemWindowInsets.top, right = insets.systemWindowInsets.right ) insets }
  57. ViewCompat.setOnApplyWindowInsetsListener(binding.toolbar) { _, insets -> binding.toolbar.updatePadding( left = insets.systemWindowInsets.left, top

    = insets.systemWindowInsets.top, right = insets.systemWindowInsets.right ) insets } 重なりの解消 − Toolbar WindowInsets の値を取得する
  58. ViewCompat.setOnApplyWindowInsetsListener(binding.toolbar) { _, insets -> binding.toolbar.updatePadding( left = insets.systemWindowInsets.left, top

    = insets.systemWindowInsets.top, right = insets.systemWindowInsets.right ) insets } 重なりの解消 − Toolbar Status bar の Height
  59. 重なりの解消 − Toolbar 画⾯回転時には Left または Right に Status bar

    の⾼さが⼊る ViewCompat.setOnApplyWindowInsetsListener(binding.toolbar) { _, insets -> binding.toolbar.updatePadding( left = insets.systemWindowInsets.left, top = insets.systemWindowInsets.top, right = insets.systemWindowInsets.right ) insets }
  60. 重なりの解消 − Toolbar

  61. 重なりの解消 − Toolbar

  62. FAB の margin に Navigation bar の⾼さ分を
 追加してあげればよさそう...? 重なりの解消 −

    FAB あらかじめ 16dp の bottom margin が
 セットされている
  63. 重なりの解消 − FAB ViewCompat.setOnApplyWindowInsetsListener(binding.fab) { _, insets -> binding.fab.updateLayoutParams<ViewGroup.MarginLayoutParams> {

    leftMargin += insets.systemWindowInsets.left rightMargin += insets.systemWindowInsets.right bottomMargin += insets.systemWindowInsets.bottom } insets }
  64. ViewCompat.setOnApplyWindowInsetsListener(binding.fab) { _, insets -> binding.fab.updateLayoutParams<ViewGroup.MarginLayoutParams> { leftMargin += insets.systemWindowInsets.left

    rightMargin += insets.systemWindowInsets.right bottomMargin += insets.systemWindowInsets.bottom } insets } 重なりの解消 − FAB Navigation bar の Height
  65. ViewCompat.setOnApplyWindowInsetsListener(binding.fab) { _, insets -> binding.fab.updateLayoutParams<ViewGroup.MarginLayoutParams> { leftMargin += insets.systemWindowInsets.left

    rightMargin += insets.systemWindowInsets.right bottomMargin += insets.systemWindowInsets.bottom } insets } 重なりの解消 − FAB 画⾯回転時には Left または Right に Status bar の⾼さが⼊る
  66. 重なりの解消 − FAB

  67. 重なりの解消 − FAB ⚠ WindowInsets のリスナーは複数回呼ばれる可能性がある

  68. 重なりの解消 − FAB ⚠ WindowInsets のリスナーは複数回呼ばれる可能性がある

  69. 重なりの解消 − FAB 複数回呼び出されることを前提に, 冪等性を確保しておく必要がある ViewCompat.setOnApplyWindowInsetsListener(binding.fab) { _, insets ->

    binding.fab.updateLayoutParams<ViewGroup.MarginLayoutParams> { leftMargin += insets.systemWindowInsets.left rightMargin += insets.systemWindowInsets.right bottomMargin += insets.systemWindowInsets.bottom } insets }
  70. 重なりの解消 − FAB val fabLeftMargin = binding.fab.marginLeft val fabRightMargin =

    binding.fab.marginRight val fabBottomMargin = binding.fab.marginBottom ViewCompat.setOnApplyWindowInsetsListener(binding.fab) {a_, insets -> binding.fab.updateLayoutParams<ViewGroup.MarginLayoutParams> {a leftMargin = fabLeftMargin + insets.systemWindowInsets.left rightMargin = fabRightMargin + insets.systemWindowInsets.right bottomMargin = fabBottomMargin + insets.systemWindowInsets.bottom }anim insets }anim
  71. val fabLeftMargin = binding.fab.marginLeft val fabRightMargin = binding.fab.marginRight val fabBottomMargin

    = binding.fab.marginBottom ViewCompat.setOnApplyWindowInsetsListener(binding.fab) {a_, insets -> binding.fab.updateLayoutParams<ViewGroup.MarginLayoutParams> {a leftMargin = fabLeftMargin + insets.systemWindowInsets.left rightMargin = fabRightMargin + insets.systemWindowInsets.right bottomMargin = fabBottomMargin + insets.systemWindowInsets.bottom }anim insets }anim 重なりの解消 − FAB 各 margin の初期値を保持しておく
  72. val fabLeftMargin = binding.fab.marginLeft val fabRightMargin = binding.fab.marginRight val fabBottomMargin

    = binding.fab.marginBottom ViewCompat.setOnApplyWindowInsetsListener(binding.fab) {a_, insets -> binding.fab.updateLayoutParams<ViewGroup.MarginLayoutParams> {a leftMargin = fabLeftMargin + insets.systemWindowInsets.left rightMargin = fabRightMargin + insets.systemWindowInsets.right bottomMargin = fabBottomMargin + insets.systemWindowInsets.bottom }anim insets }anim 重なりの解消 − FAB 各 margin の初期値 + Navigation bar のサイズ
  73. 重なりの解消 − FAB

  74. 重なりの解消 − FAB

  75. RecyclerView の paddingBottom に Navigation barの⾼さ分を
 追加してあげればよさそう 重なりの解消 − RecyclerView

  76. 重なりの解消 − RecyclerView ViewCompat.setOnApplyWindowInsetsListener(binding.recyclerView) { _, insets -> binding.recyclerView.updatePadding(bottom =

    insets.systemWindowInsets.bottom) insets }
  77. 重なりの解消 − RecyclerView ViewCompat.setOnApplyWindowInsetsListener(binding.recyclerView) { _, insets -> binding.recyclerView.updatePadding(bottom =

    insets.systemWindowInsets.bottom) insets } Navigation bar の Height
  78. <androidx.recyclerview.widget.RecyclerView android:id="@+id/recycler_view" android:layout_width="match_parent" android:layout_height="match_parent" android:clipToPadding="false" android:elevation="2dp"
 … /> 重なりの解消 −

    RecyclerView Padding の領域にも⼦ View を描画する
 (スクロール時にアイテムが表⽰されるように)
  79. 重なりの解消 − Toolbar

  80. None
  81. None
  82. 2-Button Gesture

  83. ・Toolbar, FAB, RecyclerView といった
 画⾯の上下にくっついて表⽰される View は対応が必要
 ・すでにライブラリ側で対応されているものもある
 BottomNavigationView /

    Snackbar など 対応が必要な View com.google.android.material:material:1.2.0-alpha04
  84. BottomNavigationView private void applyWindowInsets() { ViewUtils.doOnApplyWindowInsets( this, new ViewUtils.OnApplyWindowInsetsListener() {

    @NonNull @Override public androidx.core.view.WindowInsetsCompat onApplyWindowInsets( View view, @NonNull androidx.core.view.WindowInsetsCompat insets, @NonNull RelativePadding initialPadding) { initialPadding.bottom += insets.getSystemWindowInsetBottom(); initialPadding.applyToView(view); return insets; }anim }); }anim
  85. private void applyWindowInsets() { ViewUtils.doOnApplyWindowInsets( this, new ViewUtils.OnApplyWindowInsetsListener() { @NonNull

    @Override public androidx.core.view.WindowInsetsCompat onApplyWindowInsets( View view, @NonNull androidx.core.view.WindowInsetsCompat insets, @NonNull RelativePadding initialPadding) { initialPadding.bottom += insets.getSystemWindowInsetBottom(); initialPadding.applyToView(view); return insets; }anim }); }anim BottomNavigationView コンストラクタから呼ばれる
  86. private void applyWindowInsets() { ViewUtils.doOnApplyWindowInsets( this, new ViewUtils.OnApplyWindowInsetsListener() { @NonNull

    @Override public androidx.core.view.WindowInsetsCompat onApplyWindowInsets( View view, @NonNull androidx.core.view.WindowInsetsCompat insets, @NonNull RelativePadding initialPadding) { initialPadding.bottom += insets.getSystemWindowInsetBottom(); initialPadding.applyToView(view); return insets; }anim }); }anim BottomNavigationView padding bottom に Navigation bar の⾼さを追加
  87. BottomNavigationView

  88. Snackbar protected BaseTransientBottomBar(...) { ...
 ViewCompat.setOnApplyWindowInsetsListener( view, new OnApplyWindowInsetsListener() {

    @NonNull @Override public WindowInsetsCompat onApplyWindowInsets( View v, @NonNull WindowInsetsCompat insets) { extraBottomMarginWindowInset = insets.getSystemWindowInsetBottom(); extraLeftMarginWindowInset = insets.getSystemWindowInsetLeft(); extraRightMarginWindowInset = insets.getSystemWindowInsetRight(); updateMargins(); return insets; }anim }); ...
  89. protected BaseTransientBottomBar(...) { ...
 ViewCompat.setOnApplyWindowInsetsListener( view, new OnApplyWindowInsetsListener() { @NonNull

    @Override public WindowInsetsCompat onApplyWindowInsets( View v, @NonNull WindowInsetsCompat insets) { extraBottomMarginWindowInset = insets.getSystemWindowInsetBottom(); extraLeftMarginWindowInset = insets.getSystemWindowInsetLeft(); extraRightMarginWindowInset = insets.getSystemWindowInsetRight(); updateMargins(); return insets; }anim }); ... Snackbar BaseTransientBottomBar を継承している
  90. protected BaseTransientBottomBar(...) { ...
 ViewCompat.setOnApplyWindowInsetsListener( view, new OnApplyWindowInsetsListener() { @NonNull

    @Override public WindowInsetsCompat onApplyWindowInsets( View v, @NonNull WindowInsetsCompat insets) { extraBottomMarginWindowInset = insets.getSystemWindowInsetBottom(); extraLeftMarginWindowInset = insets.getSystemWindowInsetLeft(); extraRightMarginWindowInset = insets.getSystemWindowInsetRight(); updateMargins(); return insets; }anim }); ... Snackbar Navigation Bar の⾼さを各 margin に設定
 Left / Right は画⾯回転時にも重ならないようにするため
  91. Snackbar

  92. WindowInsets のなかみ

  93. ・WindowInsets は親 View から⼦ View に伝播していく ・Consume されるとそれ以降の伝播が⽌まる WindowInsets の動作(おさらい)

  94. ViewGroup ViewGroup ViewGroup View View View

  95. ViewRootImpl DecorView ViewGroup ViewGroup ViewGroup View View View 実際にはこの間にいくつかの View

    が存在する ⚠
  96. ViewRootImpl ・View 階層の最上位にあるクラス
 ・ViewParent を継承しているが
 これ⾃⾝は View ではない ViewRootImpl DecorView

    ViewGroup ViewGroup ViewGroup View View View View の親として振る舞うためのインタフェース
 この他に ViewGroup が実装している
  97. DecorView ・すべての View のルートとなる View
 ・FrameLayout を継承している ViewRootImpl DecorView ViewGroup

    ViewGroup ViewGroup View View View ➡ ViewGroup ➡ View
  98. WindowInsets の⽣成 ViewRootImpl DecorView ViewGroup ViewGroup ViewGroup View View View

  99. WindowInsets の⽣成 android.view.ViewRootImpl private void performTraversals() { ... if (mFirst)

    { ... dispatchApplyInsets(host); } else { ... }anim }anim
  100. private void performTraversals() { ... if (mFirst) { ... dispatchApplyInsets(host);

    } else { ... }anim }anim 毎フレームの描画時に呼ばれる WindowInsets の⽣成 android.view.ViewRootImpl
  101. private void performTraversals() { ... if (mFirst) { ... dispatchApplyInsets(host);

    } else { ... }anim }anim 初回のみ WindowInsets の処理を⾏う WindowInsets の⽣成 android.view.ViewRootImpl
  102. void dispatchApplyInsets(View host) {
 ... WindowInsets insets = getWindowInsets(true /*

    forceConstruct */); ...
 host.dispatchApplyWindowInsets(insets); ... }anim WindowInsets を計算し, View 階層の最上位にある View に渡す WindowInsets の⽣成 android.view.ViewRootImpl
  103. void dispatchApplyInsets(View host) {
 ... WindowInsets insets = getWindowInsets(true /*

    forceConstruct */); ...
 host.dispatchApplyWindowInsets(insets); ... }anim WindowInsets の計算 WindowInsets の⽣成 android.view.ViewRootImpl
  104. WindowInsets getWindowInsets(boolean forceConstruct) { if (mLastWindowInsets == null || forceConstruct)

    { ... mLastWindowInsets = mInsetsController.calculateInsets( mContext.getResources().getConfiguration().isScreenRound(), mAttachInfo.mAlwaysConsumeSystemBars, displayCutout, contentInsets, stableInsets, mWindowAttributes.softInputMode); } return mLastWindowInsets; } WindowInsets の⽣成 android.view.ViewRootImpl
  105. WindowInsets getWindowInsets(boolean forceConstruct) { if (mLastWindowInsets == null || forceConstruct)

    { ... mLastWindowInsets = mInsetsController.calculateInsets( mContext.getResources().getConfiguration().isScreenRound(), mAttachInfo.mAlwaysConsumeSystemBars, displayCutout, contentInsets, stableInsets, mWindowAttributes.softInputMode); } return mLastWindowInsets; } WindowInsets の⽣成 android.view.ViewRootImpl
  106. public WindowInsets calculateInsets(boolean isScreenRound, boolean alwaysConsumeSystemBars, DisplayCutout cutout, Rect legacyContentInsets,

    Rect legacyStableInsets, int legacySoftInputMode) { mLastLegacyContentInsets.set(legacyContentInsets); mLastLegacyStableInsets.set(legacyStableInsets); mLastLegacySoftInputMode = legacySoftInputMode; mLastInsets = mState.calculateInsets(mFrame, isScreenRound, alwaysConsumeSystemBars, cutout, legacyContentInsets, 
 legacyStableInsets, legacySoftInputMode, null /* typeSideMap */); return mLastInsets; } WindowInsets の⽣成 android.view.InsetsController 実際に WindowInsets を計算しているクラス
  107. WindowInsets getWindowInsets(boolean forceConstruct) { if (mLastWindowInsets == null || forceConstruct)

    { ... mLastWindowInsets = mInsetsController.calculateInsets( mContext.getResources().getConfiguration().isScreenRound(), mAttachInfo.mAlwaysConsumeSystemBars, displayCutout, contentInsets, stableInsets, mWindowAttributes.softInputMode); } return mLastWindowInsets; } WindowInsets の⽣成 android.view.ViewRootImpl
  108. void dispatchApplyInsets(View host) {
 ... WindowInsets insets = getWindowInsets(true /*

    forceConstruct */); ...
 host.dispatchApplyWindowInsets(insets); ... }anim WindowInsets の⽣成 android.view.ViewRootImpl
  109. void dispatchApplyInsets(View host) {
 ... WindowInsets insets = getWindowInsets(true /*

    forceConstruct */); ...
 host.dispatchApplyWindowInsets(insets); ... }anim DecorView に WindowInsets を渡す WindowInsets の⽣成 android.view.ViewRootImpl
  110. WindowInsets の⽣成 ViewRootImpl DecorView InsetsController InsetsState InsetsSource WindowInsets の計算 WindowInsets

    の伝達 View 階層
  111. WindowInsets の伝達 ViewRootImpl DecorView ViewGroup ViewGroup ViewGroup View View View

  112. void dispatchApplyInsets(View host) {
 ... WindowInsets insets = getWindowInsets(true /*

    forceConstruct */); ...
 host.dispatchApplyWindowInsets(insets); ... }anim DecorView に WindowInsets を渡す WindowInsets の伝達 android.view.ViewRootImpl
  113. @Override public WindowInsets dispatchApplyWindowInsets(WindowInsets insets) { insets = super.dispatchApplyWindowInsets(insets); if

    (View.sBrokenInsetsDispatch) { return brokenDispatchApplyWindowInsets(insets); } else { return newDispatchApplyWindowInsets(insets); }anim }anim WindowInsets の伝達 (DecorView の継承元) android.view.ViewGroup
  114. @Override public WindowInsets dispatchApplyWindowInsets(WindowInsets insets) { insets = super.dispatchApplyWindowInsets(insets); if

    (View.sBrokenInsetsDispatch) { return brokenDispatchApplyWindowInsets(insets); } else { return newDispatchApplyWindowInsets(insets); }anim }anim (DecorView の継承元) WindowInsets の伝達 android.view.ViewGroup
  115. public WindowInsets dispatchApplyWindowInsets(WindowInsets insets) { try { mPrivateFlags3 |= PFLAG3_APPLYING_INSETS;

    if (mListenerInfo != null && mListenerInfo.mOnApplyWindowInsetsListener != null) { return mListenerInfo .mOnApplyWindowInsetsListener .onApplyWindowInsets(this, insets); } else { return onApplyWindowInsets(insets); } } finally { mPrivateFlags3 &= ~PFLAG3_APPLYING_INSETS; } (ViewGroup の継承元) WindowInsets の伝達 android.view.View
  116. public WindowInsets dispatchApplyWindowInsets(WindowInsets insets) { try { mPrivateFlags3 |= PFLAG3_APPLYING_INSETS;

    if (mListenerInfo != null && mListenerInfo.mOnApplyWindowInsetsListener != null) { return mListenerInfo .mOnApplyWindowInsetsListener .onApplyWindowInsets(this, insets); } else { return onApplyWindowInsets(insets); } } finally { mPrivateFlags3 &= ~PFLAG3_APPLYING_INSETS; } (ViewGroup の継承元) WindowInsets の伝達 android.view.View
  117. public WindowInsets dispatchApplyWindowInsets(WindowInsets insets) { try { mPrivateFlags3 |= PFLAG3_APPLYING_INSETS;

    if (mListenerInfo != null && mListenerInfo.mOnApplyWindowInsetsListener != null) { return mListenerInfo .mOnApplyWindowInsetsListener .onApplyWindowInsets(this, insets); } else { return onApplyWindowInsets(insets); } } finally { mPrivateFlags3 &= ~PFLAG3_APPLYING_INSETS; } (ViewGroup の継承元) WindowInsets の伝達 android.view.View
  118. public WindowInsets dispatchApplyWindowInsets(WindowInsets insets) { try { mPrivateFlags3 |= PFLAG3_APPLYING_INSETS;

    if (mListenerInfo != null && mListenerInfo.mOnApplyWindowInsetsListener != null) { return mListenerInfo .mOnApplyWindowInsetsListener .onApplyWindowInsets(this, insets); } else { return onApplyWindowInsets(insets); } } finally { mPrivateFlags3 &= ~PFLAG3_APPLYING_INSETS; } (ViewGroup の継承元) WindowInsets の伝達 android.view.View
  119. @Override public WindowInsets dispatchApplyWindowInsets(WindowInsets insets) { insets = super.dispatchApplyWindowInsets(insets); if

    (View.sBrokenInsetsDispatch) { return brokenDispatchApplyWindowInsets(insets); } else { return newDispatchApplyWindowInsets(insets); }anim }anim (DecorView の継承元) WindowInsets の伝達 android.view.ViewGroup
  120. @Override public WindowInsets dispatchApplyWindowInsets(WindowInsets insets) { insets = super.dispatchApplyWindowInsets(insets); if

    (View.sBrokenInsetsDispatch) { return brokenDispatchApplyWindowInsets(insets); } else { return newDispatchApplyWindowInsets(insets); }anim }anim (ViewGroup の継承元) WindowInsets の伝達 android.view.ViewGroup
  121. @Override public WindowInsets dispatchApplyWindowInsets(WindowInsets insets) { insets = super.dispatchApplyWindowInsets(insets); if

    (View.sBrokenInsetsDispatch) { return brokenDispatchApplyWindowInsets(insets); } else { return newDispatchApplyWindowInsets(insets); }anim }anim (ViewGroup の継承元) WindowInsets の伝達 android.view.ViewGroup
  122. private WindowInsets brokenDispatchApplyWindowInsets(WindowInsets insets) { if (!insets.isConsumed()) { final int

    count = getChildCount(); for (int i = 0; i < count; i++) { insets = getChildAt(i).dispatchApplyWindowInsets(insets); if (insets.isConsumed()) { break; }anim }anim }anim return insets; }anim (ViewGroup の継承元) WindowInsets の伝達 android.view.ViewGroup
  123. (ViewGroup の継承元) WindowInsets の伝達 private WindowInsets brokenDispatchApplyWindowInsets(WindowInsets insets) { if

    (!insets.isConsumed()) { final int count = getChildCount(); for (int i = 0; i < count; i++) { insets = getChildAt(i).dispatchApplyWindowInsets(insets); if (insets.isConsumed()) { break; }anim }anim }anim return insets; }anim android.view.ViewGroup
  124. private WindowInsets brokenDispatchApplyWindowInsets(WindowInsets insets) { if (!insets.isConsumed()) { final int

    count = getChildCount(); for (int i = 0; i < count; i++) { insets = getChildAt(i).dispatchApplyWindowInsets(insets); if (insets.isConsumed()) { break; }anim }anim }anim return insets; }anim (ViewGroup の継承元) WindowInsets の伝達 android.view.ViewGroup
  125. private WindowInsets brokenDispatchApplyWindowInsets(WindowInsets insets) { if (!insets.isConsumed()) { final int

    count = getChildCount(); for (int i = 0; i < count; i++) { insets = getChildAt(i).dispatchApplyWindowInsets(insets); if (insets.isConsumed()) { break; }anim }anim }anim return insets; }anim (ViewGroup の継承元) ⼦ View に WindowInsets を渡す WindowInsets の伝達 android.view.ViewGroup
  126. private WindowInsets brokenDispatchApplyWindowInsets(WindowInsets insets) { if (!insets.isConsumed()) { final int

    count = getChildCount(); for (int i = 0; i < count; i++) { insets = getChildAt(i).dispatchApplyWindowInsets(insets); if (insets.isConsumed()) { break; }anim }anim }anim return insets; }anim (ViewGroup の継承元) ⼦ View で消費されたら, それ以降の⼦ View には渡さない WindowInsets の伝達 android.view.ViewGroup
  127. private WindowInsets brokenDispatchApplyWindowInsets(WindowInsets insets) { if (!insets.isConsumed()) { final int

    count = getChildCount(); for (int i = 0; i < count; i++) { insets = getChildAt(i).dispatchApplyWindowInsets(insets); if (insets.isConsumed()) { break; }anim }anim }anim return insets; }anim (ViewGroup の継承元) ⼦ View が ViewGroup の場合 WindowInsets の伝達 android.view.ViewGroup
  128. ⼦ View が ViewGroup の場合は
 同様にこの View の⼦ View (孫

    View) に伝達していく WindowInsets の伝達 @Override public WindowInsets dispatchApplyWindowInsets(WindowInsets insets) { insets = super.dispatchApplyWindowInsets(insets); if (View.sBrokenInsetsDispatch) { return brokenDispatchApplyWindowInsets(insets); } else { return newDispatchApplyWindowInsets(insets); }anim }anim android.view.ViewGroup
  129. private WindowInsets brokenDispatchApplyWindowInsets(WindowInsets insets) { if (!insets.isConsumed()) { final int

    count = getChildCount(); for (int i = 0; i < count; i++) { insets = getChildAt(i).dispatchApplyWindowInsets(insets); if (insets.isConsumed()) { break; }anim }anim }anim return insets; }anim ⼦ View が View の場合 WindowInsets の伝達 android.view.ViewGroup
  130. public WindowInsets dispatchApplyWindowInsets(WindowInsets insets) { try { mPrivateFlags3 |= PFLAG3_APPLYING_INSETS;

    if (mListenerInfo != null && mListenerInfo.mOnApplyWindowInsetsListener != null) { return mListenerInfo .mOnApplyWindowInsetsListener .onApplyWindowInsets(this, insets); } else { return onApplyWindowInsets(insets); } } finally { mPrivateFlags3 &= ~PFLAG3_APPLYING_INSETS; } ⼦ View が View の場合は, ⾃⾝が⼦ View を持たないので
 ⾃⾝に WindowInsets を適⽤するだけ WindowInsets の伝達 android.view.View
  131. WindowInsets の伝達 ViewGroup View dispatchApplyWindowInsets View ViewGroup dispatchApplyWindowInsets

  132. @Override public WindowInsets dispatchApplyWindowInsets(WindowInsets insets) { insets = super.dispatchApplyWindowInsets(insets); if

    (View.sBrokenInsetsDispatch) { return brokenDispatchApplyWindowInsets(insets); } else { return newDispatchApplyWindowInsets(insets); }anim }anim WindowInsets の内部 android.view.ViewGroup
  133. WindowInsets の内部 public View(Context context) { ... sBrokenInsetsDispatch = ViewRootImpl.sNewInsetsMode

    != NEW_INSETS_MODE_FULL || targetSdkVersion < Build.VERSION_CODES.Q; ... } sNewInsetsMode が NEW_INSETS_MODE_FULL でないか
 Android Q 未満の場合は true android.view.View
  134. private static final String USE_NEW_INSETS_PROPERTY = "persist.wm.new_insets"; public static int

    sNewInsetsMode = SystemProperties.getInt(USE_NEW_INSETS_PROPERTY, 0); public static final int NEW_INSETS_MODE_NONE = 0; public static final int NEW_INSETS_MODE_IME = 1; public static final int NEW_INSETS_MODE_FULL = 2; WindowInsets の内部 ୺຤͝ͱʹઃఆ͞Εͨ؀ڥม਺ͷΑ͏ͳ΋ͷ
 Pixel 3 XL(API29) Ͱ͸ NEW_INSETS_MODE_NONE android.view.ViewRootImpl
  135. WindowInsets の内部 Broken とは...? public View(Context context) { ... sBrokenInsetsDispatch

    = ViewRootImpl.sNewInsetsMode != NEW_INSETS_MODE_FULL || targetSdkVersion < Build.VERSION_CODES.Q; ... } android.view.View
  136. WindowInsets の内部 /** * Prior to Q, {@link #dispatchApplyWindowInsets} had

    some issues: * <ul> * <li>The modified insets changed by {@link #onApplyWindowInsets} were passed to the * entire view hierarchy in prefix order, including siblings as well as siblings of parents * further down the hierarchy. This violates the basic concepts of the view hierarchy, and * thus, the hierarchical dispatching mechanism was hard to use for apps.</li> * * <li>Dispatch was stopped after the insets were fully consumed. This is somewhat confusing * for developers, but more importantly, by adding more granular information to * {@link WindowInsets} it becomes really cumbersome to define what consumed actually means * </li> * </ul> * * In order to make window inset dispatching work properly, we dispatch window insets * in the view hierarchy in a proper hierarchical manner and don't stop dispatching if the * insets are consumed if this flag is set to {@code false}. */ static boolean sBrokenInsetsDispatch; android.view.View
  137. /** * Prior to Q, {@link #dispatchApplyWindowInsets} had some issues:

    * <ul> * <li>The modified insets changed by {@link #onApplyWindowInsets} were passed to the * entire view hierarchy in prefix order, including siblings as well as siblings of parents * further down the hierarchy. This violates the basic concepts of the view hierarchy, and * thus, the hierarchical dispatching mechanism was hard to use for apps.</li> * * <li>Dispatch was stopped after the insets were fully consumed. This is somewhat confusing * for developers, but more importantly, by adding more granular information to * {@link WindowInsets} it becomes really cumbersome to define what consumed actually means * </li> * </ul> * * In order to make window inset dispatching work properly, we dispatch window insets * in the view hierarchy in a proper hierarchical manner and don't stop dispatching if the * insets are consumed if this flag is set to {@code false}. */ static boolean sBrokenInsetsDispatch; ・現在の仕組みは View 階層の基本的な概念に違反しており
 アプリで扱うのが難しい
 ・特に Consume の仕組みは開発者を混乱させている ・sBrokenInsetsDispatch が false の場合は
 たとえ Consume されても伝播を⽌めないようになる WindowInsets の内部 android.view.View
  138. @Override public WindowInsets dispatchApplyWindowInsets(WindowInsets insets) { insets = super.dispatchApplyWindowInsets(insets); if

    (View.sBrokenInsetsDispatch) { return brokenDispatchApplyWindowInsets(insets); } else { return newDispatchApplyWindowInsets(insets); }anim }anim WindowInsets の内部 android.view.ViewGroup
  139. private WindowInsets newDispatchApplyWindowInsets(WindowInsets insets) { final int count = getChildCount();

    for (int i = 0; i < count; i++) { getChildAt(i).dispatchApplyWindowInsets(insets); }anim return insets; }anim WindowInsets の内部 android.view.ViewGroup
  140. private WindowInsets newDispatchApplyWindowInsets(WindowInsets insets) { final int count = getChildCount();

    for (int i = 0; i < count; i++) { getChildAt(i).dispatchApplyWindowInsets(insets); }anim return insets; }anim WindowInsets の内部 Consume に関するチェックがなくなっている android.view.ViewGroup
  141. WindowInsets の内部 /** * If set to 2, the view

    system will switch from using rectangles retrieved from window to * dispatch to the view hierarchy to using {@link InsetsController}, that derives the insets * directly from the full configuration, enabling richer information about the insets state, as * well as new APIs to control it frame-by-frame, and synchronize animations with it. * <p> * Only set this to 2 once the new insets system is productionized and the old APIs are * fully migrated over. * <p> * If set to 1, this will switch to a mode where we only use the new approach for IME, but not * for the status/navigation bar. */ private static final String USE_NEW_INSETS_PROPERTY = "persist.wm.new_insets"; android.view.ViewRootImpl
  142. /** * If set to 2, the view system will

    switch from using rectangles retrieved from window to * dispatch to the view hierarchy to using {@link InsetsController}, that derives the insets * directly from the full configuration, enabling richer information about the insets state, as * well as new APIs to control it frame-by-frame, and synchronize animations with it. * <p> * Only set this to 2 once the new insets system is productionized and the old APIs are * fully migrated over. * <p> * If set to 1, this will switch to a mode where we only use the new approach for IME, but not * for the status/navigation bar. */ private static final String USE_NEW_INSETS_PROPERTY = "persist.wm.new_insets"; ・NEW_INSETS_MODE_FULL になると
 InsetsController を使った新しい⽅式で WindowInsets が 計算される ・新しいInsetsの仕組みがリリースされ, 古いAPIが全て移⾏ された時に NEW_INSETS_MODE_FULL になる WindowInsets の内部 android.view.ViewRootImpl
  143. /** * If set to 2, the view system will

    switch from using rectangles retrieved from window to * dispatch to the view hierarchy to using {@link InsetsController}, that derives the insets * directly from the full configuration, enabling richer information about the insets state, as * well as new APIs to control it frame-by-frame, and synchronize animations with it. * <p> * Only set this to 2 once the new insets system is productionized and the old APIs are * fully migrated over. * <p> * If set to 1, this will switch to a mode where we only use the new approach for IME, but not * for the status/navigation bar. */ private static final String USE_NEW_INSETS_PROPERTY = "persist.wm.new_insets"; ・NEW_INSETS_MODE_FULL になると
 InsetsController を使った新しい⽅式で WindowInsets が 計算される ・新しいInsetsの仕組みがリリースされ, 古いAPIが全て移⾏ された時に NEW_INSETS_MODE_FULL になる WindowInsets の内部 ❓ android.view.ViewRootImpl
  144. WindowInsets の⽣成 ViewRootImpl DecorView InsetsController InsetsState InsetsSource WindowInsets の計算 WindowInsets

    の伝達 View 階層
  145. WindowInsets getWindowInsets(boolean forceConstruct) { if (mLastWindowInsets == null || forceConstruct)

    { ... mLastWindowInsets = mInsetsController.calculateInsets( mContext.getResources().getConfiguration().isScreenRound(), mAttachInfo.mAlwaysConsumeSystemBars, displayCutout, contentInsets, stableInsets, mWindowAttributes.softInputMode); } return mLastWindowInsets; } WindowInsets の⽣成 System Window Insets と Stable Insets
 を渡している android.view.ViewRootImpl
  146. WindowInsets getWindowInsets(boolean forceConstruct) { if (mLastWindowInsets == null || forceConstruct)

    { ... mLastWindowInsets = mInsetsController.calculateInsets( mContext.getResources().getConfiguration().isScreenRound(), mAttachInfo.mAlwaysConsumeSystemBars, displayCutout, contentInsets, stableInsets, mWindowAttributes.softInputMode); } return mLastWindowInsets; } WindowInsets の⽣成 ❶ System Window Insets ❷ System Gesture Insets ❸ Mandatory System Gesture Insets ❹ Stable Insets ❺ Tappable Element Insets android.view.ViewRootImpl
  147. WindowInsets getWindowInsets(boolean forceConstruct) { if (mLastWindowInsets == null || forceConstruct)

    { ... mLastWindowInsets = mInsetsController.calculateInsets( mContext.getResources().getConfiguration().isScreenRound(), mAttachInfo.mAlwaysConsumeSystemBars, displayCutout, contentInsets, stableInsets, mWindowAttributes.softInputMode); } return mLastWindowInsets; } WindowInsets の⽣成 ❶ System Window Insets ❷ System Gesture Insets ❸ Mandatory System Gesture Insets ❹ Stable Insets ❺ Tappable Element Insets レガシーな⽅法により事前に計算されている android.view.ViewRootImpl
  148. WindowInsets getWindowInsets(boolean forceConstruct) { if (mLastWindowInsets == null || forceConstruct)

    { ... mLastWindowInsets = mInsetsController.calculateInsets( mContext.getResources().getConfiguration().isScreenRound(), mAttachInfo.mAlwaysConsumeSystemBars, displayCutout, contentInsets, stableInsets, mWindowAttributes.softInputMode); } return mLastWindowInsets; } WindowInsets の⽣成 ❶ System Window Insets ❷ System Gesture Insets ❸ Mandatory System Gesture Insets ❹ Stable Insets ❺ Tappable Element Insets InsetsController を使って計算されている android.view.ViewRootImpl
  149. sBrokenInsetsDispatch ͱ sNewInsetsMode ・将来的に Consume という概念がなくなることで
 これまでより扱いやすくなる ・⼀部の WindowInsets の計算⽅法が置き換わる


    (取得できる値は変わらない)
  150. まとめ

  151. ・Android Q の時代では, Edge-to-edge への対応によって
 ユーザ体験を⾼めていくべきである ・WindowInsets の種類や動作を正しく理解して使っていこう ・将来的にはもっと扱いやすくなる予定

  152. 開催されていれば DroidKaigi 当⽇…

  153. Android 11 DP1 2020/2/20 にリリース

  154. None
  155. Android 11 で WindowInsets の API が完全に置き換わった

  156. 発表当⽇に内容が古くなってしまった

  157. IME の Visibility やサイズがとれるようになった これまでは特定の設定をした場合や、ハック的な⼿法でしか取れなかった

  158. AndroidX でのバックポートも 検討されている!

  159. https://cs.android.com/

  160. ありがとうございました @yt_hizi この資料は Apache License, Version 2.0 でライセンスされた The Android

    Open Source Project の成果物を含んでいます