Slide 1

Slide 1 text

詳解
 WindowInsets Yuta Takahashi

Slide 2

Slide 2 text

No content

Slide 3

Slide 3 text

髙橋 佑太 Yuta Takahashi 2019年6⽉より株式会社 justInCase にて Android / Server side (Kotlin) の開発に従事。 保険のサービスをつくっています。 @yt_hizi @yt-tkhs

Slide 4

Slide 4 text

1. Edge-to-edge について 2. WindowInsets の概要 3. Edge-to-edge をアプリに適⽤する 4. WindowInsets のなかみ 5. まとめ アジェンダ

Slide 5

Slide 5 text

Edge-to-edge について

Slide 6

Slide 6 text

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

Slide 7

Slide 7 text

3-Button 2-Button Android P 〜 〜 Android O

Slide 8

Slide 8 text

3-Button 2-Button Gesture Android P 〜 〜 Android O Android 10

Slide 9

Slide 9 text

Gesture ・Gesture Navigation の導⼊により
 アプリが利⽤できる画⾯の領域が拡⼤した ・Android 10 からは Edge-to-edge に対応する
 ことが推奨されている ・Android P 以下での対応は任意 Android 10

Slide 10

Slide 10 text

Edge-to-edge 対応 — Navigation bar 参考: https://developers-jp.googleblog.com/2019/10/gesture-navigation-going-edge-to-edge.html ・Gesture navigation により, Navigation bar は
 ⼩さく⽬⽴たなくなったため, 背後にコンテンツを
 描画することが推奨されている

Slide 11

Slide 11 text

Edge-to-edge 対応 — Status bar ・デザイン上, 有意義である場合にのみ対応すればよい 参考: https://developers-jp.googleblog.com/2019/10/gesture-navigation-going-edge-to-edge.html

Slide 12

Slide 12 text

WindowInsets の概要

Slide 13

Slide 13 text

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

Slide 14

Slide 14 text

WindowInsets の動作 ・親 View から ⼦ View に伝播される
 ・Insets が消費されるとそれ以降
 伝播されない ViewGroup1 ViewGroup2 ViewGroup3 View1 View2 View3

Slide 15

Slide 15 text

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

Slide 16

Slide 16 text

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

Slide 17

Slide 17 text

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

Slide 18

Slide 18 text

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

Slide 19

Slide 19 text

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

Slide 20

Slide 20 text

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

Slide 21

Slide 21 text

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

Slide 22

Slide 22 text

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

Slide 23

Slide 23 text

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

Slide 24

Slide 24 text

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

Slide 25

Slide 25 text

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

Slide 26

Slide 26 text

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

Slide 27

Slide 27 text

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

Slide 28

Slide 28 text

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

Slide 29

Slide 29 text

Insets の種類 ❶ System Window Insets ❷ System Gesture Insets ❸ Mandatory System Gesture Insets ❹ Stable Insets ❺ Tappable Element Insets Window Insets として提供される Insets は5種類

Slide 30

Slide 30 text

System Window Insets ・表⽰されている Status bar, Navigation bar のサイズ ・Edge-to-edge 対応の際によくつかう ⚠ View.SYSTEM_UI_FLAG_LAYOUT_STABLE を指定すると⾮表⽰でも取得できる

Slide 31

Slide 31 text

System Window Insets Gesture Navigation の場合 Bottom Right Top Left Status bar の⾼さ Navigation bar の⾼さ 171px 0px 0px 56px ⚠ Pixel 3 XL の場合

Slide 32

Slide 32 text

System Gesture Insets ・ジェスチャー操作を受け付ける System UI のサイズ ・Status bar や Gesture navigation の状態に影響される ・Edge-to-edge 対応の際に⽤いる

Slide 33

Slide 33 text

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 の⾼さより⼤きい

Slide 34

Slide 34 text

Mandatory System Gesture Insets ・アプリ側でコントロールできない, ジェスチャー操作を
 受け付ける System UI のサイズ ・Gesture Navigation の設定によって
 部分的に除外できない領域のみが取得できる

Slide 35

Slide 35 text

Mandatory System Gesture Insets 171px 0px 0px 112px Status bar の通知パネル表⽰ Gesture navigation のホーム/アプリ切替 Bottom Right Top Left Gesture Navigation の場合 View.setSystemGestureExclusionRects()
 によってオーバライド可能 ⚠ Pixel 3 XL の場合

Slide 36

Slide 36 text

Stable Insets ・Status bar, Navigation bar のサイズ ・System bar の可視性にかかわらず取得できる ・View階層の上位にある DecorView で消費されるが
 System Window Insets で代替可能なため使うことはない

Slide 37

Slide 37 text

Stable Insets Gesture Navigation の場合 Bottom Right Top Left Status bar の⾼さ Navigation bar の⾼さ 171px 0px 0px 56px ⚠ Pixel 3 XL の場合

Slide 38

Slide 38 text

Tappable Element Insets ・すべてのタッチイベントを消費する System UI のサイズ ・ジェスチャーは不可能だが単純なタップは可能なものは
 除外される ・ほとんど使⽤することがない

Slide 39

Slide 39 text

Tappable Element Insets 171px 0px 0px 0px Status bar の通知パネル表⽰ Bottom Right Top Left Pixel 3 XL / Gesture Navigation の場合 ⚠ Pixel 3 XL の場合

Slide 40

Slide 40 text

consume* その他のAPI ・Insets を消費するためのメソッド
 
 ・それ以降の Insets の伝達が⽌まり, Insets が反映されなく なる ・将来的になくなる可能性がある (後述) consumeSystemWindowInsets()
 consumeSystemGestureInsets()

Slide 41

Slide 41 text

Edge-to-edge をアプリに適⽤する

Slide 42

Slide 42 text

・androidx.core:core に, バージョン間の差異を
 吸収できる WindowInsetsCompat が⽤意されている ・Gesture Navigation 関連の Insets を取得するには
 1.2.0-alpha01 以降を使⽤する必要がある WindowInsets 最新版は 1.3.0-alpha01 (2020年2⽉21⽇現在)

Slide 43

Slide 43 text

No content

Slide 44

Slide 44 text

dependencies に追加する dependencies { implementation 'androidx.core:core:1.3.0-alpha01' }

Slide 45

Slide 45 text

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

Slide 46

Slide 46 text

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 の値を取得できるようにする

Slide 47

Slide 47 text

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 を描画する

Slide 48

Slide 48 text

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 を描画する

Slide 49

Slide 49 text

Theme を編集する <item name="android:navigationBarColor">@android:color/transparent</item> <item name="android:statusBarColor">@android:color/transparent</item>

Slide 50

Slide 50 text

Theme を編集する <item name="android:navigationBarColor">@android:color/transparent</item> <item name="android:statusBarColor">@android:color/transparent</item> Navigation bar の背景を透明にする

Slide 51

Slide 51 text

<item name="android:navigationBarColor">@android:color/transparent</item> <item name="android:statusBarColor">@android:color/transparent</item> Theme を編集する Status bar の背景を透明にする

Slide 52

Slide 52 text

Theme を編集する <item name="android:navigationBarColor">@android:color/transparent</item> <item name="android:statusBarColor">@android:color/transparent</item>

Slide 53

Slide 53 text

No content

Slide 54

Slide 54 text

・Toolbar が Status bar と重なる ・FAB が Navigation bar と重なる ・RecyclerView が Navigation bar と重なる (⼀番下までスクロールしたとき) WindowInsets を使って解消していく

Slide 55

Slide 55 text

重なりの解消 − Toolbar Toolbar の padding に Status barの⾼さ分を
 セットしてあげればよさそう

Slide 56

Slide 56 text

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

Slide 57

Slide 57 text

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

Slide 58

Slide 58 text

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

Slide 59

Slide 59 text

重なりの解消 − 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 }

Slide 60

Slide 60 text

重なりの解消 − Toolbar

Slide 61

Slide 61 text

重なりの解消 − Toolbar

Slide 62

Slide 62 text

FAB の margin に Navigation bar の⾼さ分を
 追加してあげればよさそう...? 重なりの解消 − FAB あらかじめ 16dp の bottom margin が
 セットされている

Slide 63

Slide 63 text

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

Slide 64

Slide 64 text

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

Slide 65

Slide 65 text

ViewCompat.setOnApplyWindowInsetsListener(binding.fab) { _, insets -> binding.fab.updateLayoutParams { leftMargin += insets.systemWindowInsets.left rightMargin += insets.systemWindowInsets.right bottomMargin += insets.systemWindowInsets.bottom } insets } 重なりの解消 − FAB 画⾯回転時には Left または Right に Status bar の⾼さが⼊る

Slide 66

Slide 66 text

重なりの解消 − FAB

Slide 67

Slide 67 text

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

Slide 68

Slide 68 text

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

Slide 69

Slide 69 text

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

Slide 70

Slide 70 text

重なりの解消 − 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 {a leftMargin = fabLeftMargin + insets.systemWindowInsets.left rightMargin = fabRightMargin + insets.systemWindowInsets.right bottomMargin = fabBottomMargin + insets.systemWindowInsets.bottom }anim insets }anim

Slide 71

Slide 71 text

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

Slide 72

Slide 72 text

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

Slide 73

Slide 73 text

重なりの解消 − FAB

Slide 74

Slide 74 text

重なりの解消 − FAB

Slide 75

Slide 75 text

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

Slide 76

Slide 76 text

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

Slide 77

Slide 77 text

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

Slide 78

Slide 78 text

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

Slide 79

Slide 79 text

重なりの解消 − Toolbar

Slide 80

Slide 80 text

No content

Slide 81

Slide 81 text

No content

Slide 82

Slide 82 text

2-Button Gesture

Slide 83

Slide 83 text

・Toolbar, FAB, RecyclerView といった
 画⾯の上下にくっついて表⽰される View は対応が必要
 ・すでにライブラリ側で対応されているものもある
 BottomNavigationView / Snackbar など 対応が必要な View com.google.android.material:material:1.2.0-alpha04

Slide 84

Slide 84 text

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

Slide 85

Slide 85 text

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 コンストラクタから呼ばれる

Slide 86

Slide 86 text

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 の⾼さを追加

Slide 87

Slide 87 text

BottomNavigationView

Slide 88

Slide 88 text

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 }); ...

Slide 89

Slide 89 text

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 を継承している

Slide 90

Slide 90 text

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 は画⾯回転時にも重ならないようにするため

Slide 91

Slide 91 text

Snackbar

Slide 92

Slide 92 text

WindowInsets のなかみ

Slide 93

Slide 93 text

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

Slide 94

Slide 94 text

ViewGroup ViewGroup ViewGroup View View View

Slide 95

Slide 95 text

ViewRootImpl DecorView ViewGroup ViewGroup ViewGroup View View View 実際にはこの間にいくつかの View が存在する ⚠

Slide 96

Slide 96 text

ViewRootImpl ・View 階層の最上位にあるクラス
 ・ViewParent を継承しているが
 これ⾃⾝は View ではない ViewRootImpl DecorView ViewGroup ViewGroup ViewGroup View View View View の親として振る舞うためのインタフェース
 この他に ViewGroup が実装している

Slide 97

Slide 97 text

DecorView ・すべての View のルートとなる View
 ・FrameLayout を継承している ViewRootImpl DecorView ViewGroup ViewGroup ViewGroup View View View ➡ ViewGroup ➡ View

Slide 98

Slide 98 text

WindowInsets の⽣成 ViewRootImpl DecorView ViewGroup ViewGroup ViewGroup View View View

Slide 99

Slide 99 text

WindowInsets の⽣成 android.view.ViewRootImpl private void performTraversals() { ... if (mFirst) { ... dispatchApplyInsets(host); } else { ... }anim }anim

Slide 100

Slide 100 text

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

Slide 101

Slide 101 text

private void performTraversals() { ... if (mFirst) { ... dispatchApplyInsets(host); } else { ... }anim }anim 初回のみ WindowInsets の処理を⾏う WindowInsets の⽣成 android.view.ViewRootImpl

Slide 102

Slide 102 text

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

Slide 103

Slide 103 text

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

Slide 104

Slide 104 text

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

Slide 105

Slide 105 text

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

Slide 106

Slide 106 text

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 を計算しているクラス

Slide 107

Slide 107 text

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

Slide 108

Slide 108 text

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

Slide 109

Slide 109 text

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

Slide 110

Slide 110 text

WindowInsets の⽣成 ViewRootImpl DecorView InsetsController InsetsState InsetsSource WindowInsets の計算 WindowInsets の伝達 View 階層

Slide 111

Slide 111 text

WindowInsets の伝達 ViewRootImpl DecorView ViewGroup ViewGroup ViewGroup View View View

Slide 112

Slide 112 text

void dispatchApplyInsets(View host) {
 ... WindowInsets insets = getWindowInsets(true /* forceConstruct */); ...
 host.dispatchApplyWindowInsets(insets); ... }anim DecorView に WindowInsets を渡す WindowInsets の伝達 android.view.ViewRootImpl

Slide 113

Slide 113 text

@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

Slide 114

Slide 114 text

@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

Slide 115

Slide 115 text

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

Slide 116

Slide 116 text

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

Slide 117

Slide 117 text

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

Slide 118

Slide 118 text

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

Slide 119

Slide 119 text

@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

Slide 120

Slide 120 text

@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

Slide 121

Slide 121 text

@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

Slide 122

Slide 122 text

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

Slide 123

Slide 123 text

(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

Slide 124

Slide 124 text

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

Slide 125

Slide 125 text

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

Slide 126

Slide 126 text

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

Slide 127

Slide 127 text

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

Slide 128

Slide 128 text

⼦ 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

Slide 129

Slide 129 text

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

Slide 130

Slide 130 text

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

Slide 131

Slide 131 text

WindowInsets の伝達 ViewGroup View dispatchApplyWindowInsets View ViewGroup dispatchApplyWindowInsets

Slide 132

Slide 132 text

@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

Slide 133

Slide 133 text

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

Slide 134

Slide 134 text

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

Slide 135

Slide 135 text

WindowInsets の内部 Broken とは...? public View(Context context) { ... sBrokenInsetsDispatch = ViewRootImpl.sNewInsetsMode != NEW_INSETS_MODE_FULL || targetSdkVersion < Build.VERSION_CODES.Q; ... } android.view.View

Slide 136

Slide 136 text

WindowInsets の内部 /** * Prior to Q, {@link #dispatchApplyWindowInsets} had some issues: *
    *
  • 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.
  • * *
  • 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 *
  • *
* * 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

Slide 137

Slide 137 text

/** * Prior to Q, {@link #dispatchApplyWindowInsets} had some issues: *
    *
  • 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.
  • * *
  • 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 *
  • *
* * 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

Slide 138

Slide 138 text

@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

Slide 139

Slide 139 text

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

Slide 140

Slide 140 text

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

Slide 141

Slide 141 text

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. *

* Only set this to 2 once the new insets system is productionized and the old APIs are * fully migrated over. *

* 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

Slide 142

Slide 142 text

/** * 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. *

* Only set this to 2 once the new insets system is productionized and the old APIs are * fully migrated over. *

* 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

Slide 143

Slide 143 text

/** * 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. *

* Only set this to 2 once the new insets system is productionized and the old APIs are * fully migrated over. *

* 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

Slide 144

Slide 144 text

WindowInsets の⽣成 ViewRootImpl DecorView InsetsController InsetsState InsetsSource WindowInsets の計算 WindowInsets の伝達 View 階層

Slide 145

Slide 145 text

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

Slide 146

Slide 146 text

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

Slide 147

Slide 147 text

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

Slide 148

Slide 148 text

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

Slide 149

Slide 149 text

sBrokenInsetsDispatch ͱ sNewInsetsMode ・将来的に Consume という概念がなくなることで
 これまでより扱いやすくなる ・⼀部の WindowInsets の計算⽅法が置き換わる
 (取得できる値は変わらない)

Slide 150

Slide 150 text

まとめ

Slide 151

Slide 151 text

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

Slide 152

Slide 152 text

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

Slide 153

Slide 153 text

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

Slide 154

Slide 154 text

No content

Slide 155

Slide 155 text

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

Slide 156

Slide 156 text

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

Slide 157

Slide 157 text

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

Slide 158

Slide 158 text

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

Slide 159

Slide 159 text

https://cs.android.com/

Slide 160

Slide 160 text

ありがとうございました @yt_hizi この資料は Apache License, Version 2.0 でライセンスされた The Android Open Source Project の成果物を含んでいます