Share
Embed
DroidKaigi 2020: System UIをコントロールして、 画面を最大限に生かしたアプリを構築する / Build apps that make the best uses of device screens by controlling System UI
端末の画面を最大限に活かし、より良い体験を提供するために、System UI(Status Bar、Navigation Bar)を考慮することは非常に重要です。
しかし、Android Q、Q以前などでのバージョンごとの差異、ノッチ対応、画面構成によっては、WindowInsetsを取得する必要があるなど、考慮しなければならない点が多くあります。
この発表では、最初にSystem UIをコントロールするために抑えておくべき要素を、実際の挙動を見ながら、1つ1つ理解していきます。
具体的には、次のことを説明します。
- fitsSystemWindowsの挙動について
- ThemeからSystem UIの設定をする
- Navigation Component(Single Activity)との兼ね合い
- WindowInsetsを取り、動的にSystem UIを調整する
- FloatingActionButtonとの兼ね合い
- systemUiVisibilityについて
- ノッチ端末(cutout)への対応
最後に、実践的な例として、Google I/OなどのアプリのSystem UIをどのようにすれば構築出来るかを説明します。
System UIの基礎 〜 実践までを理解することで、画面を最大限に生かした没入感のあるアプリを実装出来るようになることを目指します。
Sato Shun
April 02, 2020
Transcript
194 slides
-
DroidKaigi 2020 Sato Shun GitHub: satoshun Twitter: @stsn_jp CyberAgent, Inc.
System UIΛίϯτϩʔϧͯ͠ɺ ը໘Λ࠷େݶʹੜ͔ͨ͠ΞϓϦΛߏங͢Δ -
• Edge to Edgeͱ? • Edge to EdgeΛͳͥΔ͔? • Edge
to EdgeΛͲͷΑ͏ʹΔ͔ʁ • ΧελϜView͔ΒֶͿ • ଞͷΞϓϦ͔ΒֶͿ -
SystemUiVisibility ͱ? • System UIʢstatus bar / navigation barʣͷݟͨɺৼΔ͍ Λมߋ͢Δ
• Immersive modeʢೖϞʔυʣ -
SystemUiVisibility ͱ? • System UIʢstatus bar / navigation barʣͷݟͨɺৼΔ͍ Λมߋ͢Δ
• Immersive modeʢೖϞʔυʣ -
SystemUiVisibility ͱ? • System UIʢstatus bar / navigation barʣͷݟͨɺৼΔ͍ Λมߋ͢Δ
• Immersive modeʢೖϞʔυʣ -
SystemUiVisibility ͱ? • System UIʢstatus bar / navigation barʣͷݟͨɺৼΔ͍ Λมߋ͢Δ
• Immersive modeʢೖϞʔυʣ • status bar / navigation barͷഎޙʹඳը -
SystemUiVisibility ͱ? • System UIʢstatus bar / navigation barʣͷݟͨɺৼΔ͍ Λมߋ͢Δ
• Immersive modeʢೖϞʔυʣ • status bar / navigation barͷഎޙʹඳը -
SystemUiVisibility·ͱΊ • SYSTEM_UI_FLAG_LAYOUT_FULLSCREENɺ SYSTEM_UI_FLAG_LAYOUT_HIDE_NAVIGATION • 2ͭΛΈ߹ΘͤΔ͜ͱͰɺmarginɺpaddingΛ0ʹઃఆ͢Δ͜ͱ ͕ग़དྷΔ • ίϯςϯπΛSystem UIͷഎޙʹඳը͢Δ͜ͱ͕ग़དྷΔ
• SYSTEM_UI_FLAG_LAYOUT_STABLE͚ͨ΄͏͕ྑ͍ • ৄ͘͠ɺDroidKaigi2020 ৄղ WindowInsetsΛݟ͍ͯͩ͘͞ -
API23+ API23ະຬͰɺ Lightʢ໌Δ͍ʣ৭Λstatus barͷ എܠ৭ʹऔΔͱࢹೝੑ͕ஶ͘͠ Լ͢Δ͜ͱΛҙຯ͢Δ view.systemUiVisibility = ... View.SYSTEM_UI_FLAG_LIGHT_STATUS_BAR
<item name="android:windowLightStatusBar">true</item> -
API27+ API27ະຬͰɺ Lightʢ໌Δ͍ʣ৭Λnavigationͷ എܠ৭ʹऔΔͱࢹೝੑ͕ஶ͘͠ Լ͢Δ͜ͱΛҙຯ͢Δ view.systemUiVisibility = ... View.SYSTEM_UI_FLAG_LIGHT_NAVIGATION_BAR <item
name="android:windowLightNavigationBar">true</item> -
System UIഎܠ৭ͷ·ͱΊ • API23+: windowLightStatusBarͷՃ • API27+: windowLightNavigationBarͷՃ • API29+:
enforceNavigationBarContrastɺ enforceStatusBarContrastͷՃ • transparentͷͱ͖ɺ͍͍ײ͡ʹഎܠ৭Λௐͯ͘͠ΕΔ -
<LinearLayout android:fitsSystemWindows> <AppBarLayout android:fitsSystemWindows> <MaterialToolbar /> </AppBarLayout> <Button /> </LinearLayout>
͜ͷfitsSystemWindowsແࢹ͞ΕΔ fitsSystemWindowsσϑΥϧτͰɺ ਂ͞༏ઌ୳ࡧΛ͠ɺݟ͔ͭͬͨ࠷ॳͷΈ༗ޮ -
<LinearLayout android:fitsSystemWindows> <AppBarLayout android:fitsSystemWindows> <MaterialToolbar /> </AppBarLayout> <Button /> </LinearLayout>
͜ͷfitsSystemWindowsແࢹ͞ΕΔ fitsSystemWindowsσϑΥϧτͰɺ ਂ͞༏ઌ୳ࡧΛ͠ɺݟ͔ͭͬͨ࠷ॳͷΈ༗ޮ View View View View View -
<LinearLayout android:fitsSystemWindows> <AppBarLayout android:fitsSystemWindows> <MaterialToolbar /> </AppBarLayout> <Button /> </LinearLayout>
͜ͷfitsSystemWindowsແࢹ͞ΕΔ fitsSystemWindowsσϑΥϧτͰɺ ਂ͞༏ઌ୳ࡧΛ͠ɺݟ͔ͭͬͨ࠷ॳͷΈ༗ޮ View View View View View -
<LinearLayout android:fitsSystemWindows> <AppBarLayout android:fitsSystemWindows> <MaterialToolbar /> </AppBarLayout> <Button /> </LinearLayout>
͜ͷfitsSystemWindowsແࢹ͞ΕΔ fitsSystemWindowsσϑΥϧτͰɺ ਂ͞༏ઌ୳ࡧΛ͠ɺݟ͔ͭͬͨ࠷ॳͷΈ༗ޮ View View View View View -
<LinearLayout android:fitsSystemWindows> <AppBarLayout android:fitsSystemWindows> <MaterialToolbar /> </AppBarLayout> <Button /> </LinearLayout>
͜ͷfitsSystemWindowsແࢹ͞ΕΔ fitsSystemWindowsσϑΥϧτͰɺ ਂ͞༏ઌ୳ࡧΛ͠ɺݟ͔ͭͬͨ࠷ॳͷΈ༗ޮ View View View View View -
<LinearLayout android:fitsSystemWindows> <AppBarLayout android:fitsSystemWindows> <MaterialToolbar /> </AppBarLayout> <Button /> </LinearLayout>
͜ͷfitsSystemWindowsແࢹ͞ΕΔ fitsSystemWindowsσϑΥϧτͰɺ ਂ͞༏ઌ୳ࡧΛ͠ɺݟ͔ͭͬͨ࠷ॳͷΈ༗ޮ View View View View View -
<LinearLayout android:fitsSystemWindows> <AppBarLayout android:fitsSystemWindows> <MaterialToolbar /> </AppBarLayout> <Button /> </LinearLayout>
͜ͷfitsSystemWindowsແࢹ͞ΕΔ fitsSystemWindowsσϑΥϧτͰɺ ਂ͞༏ઌ୳ࡧΛ͠ɺݟ͔ͭͬͨ࠷ॳͷΈ༗ޮ View View View View View -
<LinearLayout android:fitsSystemWindows> <AppBarLayout android:fitsSystemWindows> <MaterialToolbar /> </AppBarLayout> <Button /> </LinearLayout>
͜ͷfitsSystemWindowsແࢹ͞ΕΔ fitsSystemWindowsσϑΥϧτͰɺ ਂ͞༏ઌ୳ࡧΛ͠ɺݟ͔ͭͬͨ࠷ॳͷΈ༗ޮ -
<LinearLayout android:fitsSystemWindows> <AppBarLayout> <MaterialToolbar /> </AppBarLayout> <Button /> </LinearLayout> fitsSystemWindowsσϑΥϧτͰɺ
ਂ͞༏ઌ୳ࡧΛ͠ɺݟ͔ͭͬͨ࠷ॳͷΈ༗ޮ ͜ͷfitsSystemWindowsແࢹ͞ΕΔ -
view.setOnApplyWindowInsetsListener { v, insets -> v.updatePadding( top = insets.systemWindowInsetTop, bottom
= insets.systemWindowInsetBottom ) insets } -
view.setOnApplyWindowInsetsListener { v, insets -> v.updatePadding( top = insets.systemWindowInsetTop, bottom
= insets.systemWindowInsetBottom ) insets } -
view.setOnApplyWindowInsetsListener { v, insets -> v.updatePadding( top = insets.systemWindowInsetTop, bottom
= insets.systemWindowInsetBottom ) insets } -
view.setOnApplyWindowInsetsListener { v, insets -> v.updatePadding( top = insets.systemWindowInsetTop, bottom
= insets.systemWindowInsetBottom ) insets } class WindowInsets { Insets getSystemWindowInsets() Insets getStableInsets() Insets getSystemGestureInsets() Insets getMandatorySystemGestureInsets() Insets getTappableElementInsets() DisplayCutout getDisplayCutout() } -
view.setOnApplyWindowInsetsListener { v, insets -> v.updatePadding( top = insets.systemWindowInsetTop, bottom
= insets.systemWindowInsetBottom ) insets } -
view.setOnApplyWindowInsetsListener { v, insets -> v.updatePadding( top = insets.systemWindowInsetTop, bottom
= insets.systemWindowInsetBottom ) insets } child.setOnApplyWindowInsetsListener { v, insets -> v.updatePadding( top = insets.systemWindowInsetTop, bottom = insets.systemWindowInsetBottom ) insets } -
view.setOnApplyWindowInsetsListener { v, insets -> v.updatePadding( top = insets.systemWindowInsetTop, bottom
= insets.systemWindowInsetBottom ) insets } child.setOnApplyWindowInsetsListener { v, insets -> v.updatePadding( top = insets.systemWindowInsetTop, bottom = insets.systemWindowInsetBottom ) insets } -
view.setOnApplyWindowInsetsListener { v, insets -> v.updatePadding( top = insets.systemWindowInsetTop, bottom
= insets.systemWindowInsetBottom ) insets.consumeSystemWindowInsets() } child.setOnApplyWindowInsetsListener { v, insets -> v.updatePadding( top = insets.systemWindowInsetTop, bottom = insets.systemWindowInsetBottom ) insets } -
view.setOnApplyWindowInsetsListener { v, insets -> v.updatePadding( top = insets.systemWindowInsetTop, bottom
= insets.systemWindowInsetBottom ) insets.consumeSystemWindowInsets() } child.setOnApplyWindowInsetsListener { v, insets -> v.updatePadding( top = insets.systemWindowInsetTop, bottom = insets.systemWindowInsetBottom ) insets } -
view.setOnApplyWindowInsetsListener { v, insets -> v.updatePadding( top = insets.systemWindowInsetTop, bottom
= insets.systemWindowInsetBottom ) insets.consumeSystemWindowInsets() } child.setOnApplyWindowInsetsListener { v, insets -> v.updatePadding( top = insets.systemWindowInsetTop, bottom = insets.systemWindowInsetBottom ) insets } -
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; } } -
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; } } -
public WindowInsets onApplyWindowInsets(WindowInsets insets) { if ((mPrivateFlags3 & PFLAG3_FITTING_SYSTEM_WINDOWS) ==
0) { if (fitSystemWindows(insets.getSystemWindowInsetsAsRect())) { return insets.consumeSystemWindowInsets(); } } else { if (fitSystemWindowsInt(insets.getSystemWindowInsetsAsRect())) { return insets.consumeSystemWindowInsets(); } } return insets; } -
public WindowInsets onApplyWindowInsets(WindowInsets insets) { if ((mPrivateFlags3 & PFLAG3_FITTING_SYSTEM_WINDOWS) ==
0) { if (fitSystemWindows(insets.getSystemWindowInsetsAsRect())) { return insets.consumeSystemWindowInsets(); } } else { if (fitSystemWindowsInt(insets.getSystemWindowInsetsAsRect())) { return insets.consumeSystemWindowInsets(); } } return insets; } σϑΥϧτͷfitsSystemWindowsͷ߹ɺInsetsΛফඅ͢Δ -
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; } } -
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; } } -
public void setOnApplyWindowInsetsListener(OnApplyWindowInsetsListener listener) { getListenerInfo().mOnApplyWindowInsetsListener = listener; } Listener͕ొ͞Ε͍ͯΔͱɺ
fitsSystemWindowsͷಈ࡞ΛΧελϚΠζ͢Δ ͜ͱ͕ग़དྷΔ view.setOnApplyWindowInsetsListener { v, insets -> … } -
<CoordinatorLayout fitsSystemWindows> <AppBarLayout fitsSystemWindows> <MaterialToolbar /> </AppBarLayout> <BottomAppBar fitsSystemWindows />
</CoordinatorLayout> setOnApplyWindowListenerΛͬͯɺ σϑΥϧτͷڍಈΛม͍͑ͯΔ ·ͨɺWindowInsetsফඅ͍ͯ͠ͳ͍ -
private void setupForInsets() { if (ViewCompat.getFitsSystemWindows(this)) { if (mApplyWindowInsetsListener ==
null) { mApplyWindowInsetsListener = new androidx.core.view.OnApplyWindowInsetsListener() { @Override public WindowInsetsCompat onApplyWindowInsets(View v, WindowInsetsCompat insets) { return setWindowInsets(insets); } }; } // First apply the insets listener ViewCompat.setOnApplyWindowInsetsListener(this, mApplyWindowInsetsListener); -
private void setupForInsets() { if (ViewCompat.getFitsSystemWindows(this)) { if (mApplyWindowInsetsListener ==
null) { mApplyWindowInsetsListener = new androidx.core.view.OnApplyWindowInsetsListener() { @Override public WindowInsetsCompat onApplyWindowInsets(View v, WindowInsetsCompat insets) { return setWindowInsets(insets); } }; } // First apply the insets listener ViewCompat.setOnApplyWindowInsetsListener(this, mApplyWindowInsetsListener); -
private void setupForInsets() { if (ViewCompat.getFitsSystemWindows(this)) { if (mApplyWindowInsetsListener ==
null) { mApplyWindowInsetsListener = new androidx.core.view.OnApplyWindowInsetsListener() { @Override public WindowInsetsCompat onApplyWindowInsets(View v, WindowInsetsCompat insets) { return setWindowInsets(insets); } }; } // First apply the insets listener ViewCompat.setOnApplyWindowInsetsListener(this, mApplyWindowInsetsListener); -
private void setupForInsets() { if (ViewCompat.getFitsSystemWindows(this)) { if (mApplyWindowInsetsListener ==
null) { mApplyWindowInsetsListener = new androidx.core.view.OnApplyWindowInsetsListener() { @Override public WindowInsetsCompat onApplyWindowInsets(View v, WindowInsetsCompat insets) { return setWindowInsets(insets); } }; } // First apply the insets listener ViewCompat.setOnApplyWindowInsetsListener(this, mApplyWindowInsetsListener); -
final WindowInsetsCompat setWindowInsets(WindowInsetsCompat insets) { if (!ObjectsCompat.equals(mLastInsets, insets)) { mLastInsets
= insets; // Now dispatch to the Behaviors insets = dispatchApplyWindowInsetsToBehaviors(insets); requestLayout(); } return insets; } -
<CoordinatorLayout fitsSystemWindows> <AppBarLayout fitsSystemWindows> <MaterialToolbar /> </AppBarLayout> <BottomAppBar fitsSystemWindows />
</CoordinatorLayout> final WindowInsetsCompat setWindowInsets(WindowInsetsCompat insets) { if (!ObjectsCompat.equals(mLastInsets, insets)) { mLastInsets = insets; // Now dispatch to the Behaviors insets = dispatchApplyWindowInsetsToBehaviors(insets); requestLayout(); } return insets; } -
if (mLastInsets != null && ViewCompat.getFitsSystemWindows(this) && !ViewCompat.getFitsSystemWindows(child)) { parent.left
+= mLastInsets.getSystemWindowInsetLeft(); parent.top += mLastInsets.getSystemWindowInsetTop(); parent.right -= mLastInsets.getSystemWindowInsetRight(); parent.bottom -= mLastInsets.getSystemWindowInsetBottom(); } final Rect out = acquireTempRect(); GravityCompat.apply(resolveGravity(lp.gravity), child.getMeasuredWidth(), child.getMeasuredHeight(), parent, out, layoutDirection); child.layout(out.left, out.top, out.right, out.bottom); private void layoutChild(…) -
if (mLastInsets != null && ViewCompat.getFitsSystemWindows(this) && !ViewCompat.getFitsSystemWindows(child)) { parent.left
+= mLastInsets.getSystemWindowInsetLeft(); parent.top += mLastInsets.getSystemWindowInsetTop(); parent.right -= mLastInsets.getSystemWindowInsetRight(); parent.bottom -= mLastInsets.getSystemWindowInsetBottom(); } final Rect out = acquireTempRect(); GravityCompat.apply(resolveGravity(lp.gravity), child.getMeasuredWidth(), child.getMeasuredHeight(), parent, out, layoutDirection); child.layout(out.left, out.top, out.right, out.bottom); private void layoutChild(…) -
if (mLastInsets != null && ViewCompat.getFitsSystemWindows(this) && !ViewCompat.getFitsSystemWindows(child)) { parent.left
+= mLastInsets.getSystemWindowInsetLeft(); parent.top += mLastInsets.getSystemWindowInsetTop(); parent.right -= mLastInsets.getSystemWindowInsetRight(); parent.bottom -= mLastInsets.getSystemWindowInsetBottom(); } final Rect out = acquireTempRect(); GravityCompat.apply(resolveGravity(lp.gravity), child.getMeasuredWidth(), child.getMeasuredHeight(), parent, out, layoutDirection); child.layout(out.left, out.top, out.right, out.bottom); <CoordinatorLayout> <AppBarLayout> <MaterialToolbar /> </AppBarLayout> <BottomAppBar /> </CoordinatorLayout> -
if (mLastInsets != null && ViewCompat.getFitsSystemWindows(this) && !ViewCompat.getFitsSystemWindows(child)) { parent.left
+= mLastInsets.getSystemWindowInsetLeft(); parent.top += mLastInsets.getSystemWindowInsetTop(); parent.right -= mLastInsets.getSystemWindowInsetRight(); parent.bottom -= mLastInsets.getSystemWindowInsetBottom(); } final Rect out = acquireTempRect(); GravityCompat.apply(resolveGravity(lp.gravity), child.getMeasuredWidth(), child.getMeasuredHeight(), parent, out, layoutDirection); child.layout(out.left, out.top, out.right, out.bottom); <CoordinatorLayout fitsSystemWindows> <AppBarLayout> <MaterialToolbar /> </AppBarLayout> <BottomAppBar /> </CoordinatorLayout> -
ViewCompat.setOnApplyWindowInsetsListener( this, new androidx.core.view.OnApplyWindowInsetsListener() { @Override public WindowInsetsCompat onApplyWindowInsets(View v,
WindowInsetsCompat insets) { return onWindowInsetChanged(insets); } }); class AppBarLayout -
ViewCompat.setOnApplyWindowInsetsListener( this, new androidx.core.view.OnApplyWindowInsetsListener() { @Override public WindowInsetsCompat onApplyWindowInsets(View v,
WindowInsetsCompat insets) { return onWindowInsetChanged(insets); } }); -
WindowInsetsCompat onWindowInsetChanged(final WindowInsetsCompat insets) { WindowInsetsCompat newInsets = null; if
(ViewCompat.getFitsSystemWindows(this)) { newInsets = insets; } … } -
WindowInsetsCompat onWindowInsetChanged(final WindowInsetsCompat insets) { WindowInsetsCompat newInsets = null; if
(ViewCompat.getFitsSystemWindows(this)) { newInsets = insets; } … } -
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { super.onMeasure(widthMeasureSpec, heightMeasureSpec); final
int heightMode = MeasureSpec.getMode(heightMeasureSpec); if (heightMode != MeasureSpec.EXACTLY && ViewCompat.getFitsSystemWindows(this) && shouldOffsetFirstChild()) { int newHeight = getMeasuredHeight(); switch (heightMode) { case MeasureSpec.AT_MOST: newHeight = MathUtils.clamp( getMeasuredHeight() + getTopInset(), 0, MeasureSpec.getSize(heightMeasureSpec)); break; … -
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { super.onMeasure(widthMeasureSpec, heightMeasureSpec); final
int heightMode = MeasureSpec.getMode(heightMeasureSpec); if (heightMode != MeasureSpec.EXACTLY && ViewCompat.getFitsSystemWindows(this) && shouldOffsetFirstChild()) { int newHeight = getMeasuredHeight(); switch (heightMode) { case MeasureSpec.AT_MOST: newHeight = MathUtils.clamp( getMeasuredHeight() + getTopInset(), 0, MeasureSpec.getSize(heightMeasureSpec)); break; … -
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { super.onMeasure(widthMeasureSpec, heightMeasureSpec); final
int heightMode = MeasureSpec.getMode(heightMeasureSpec); if (heightMode != MeasureSpec.EXACTLY && ViewCompat.getFitsSystemWindows(this) && shouldOffsetFirstChild()) { int newHeight = getMeasuredHeight(); switch (heightMode) { case MeasureSpec.AT_MOST: newHeight = MathUtils.clamp( getMeasuredHeight() + getTopInset(), 0, MeasureSpec.getSize(heightMeasureSpec)); break; … <CoordinatorLayout fitsSystemWindows> <AppBarLayout> <MaterialToolbar /> </AppBarLayout> <BottomAppBar /> </CoordinatorLayout> -
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { super.onMeasure(widthMeasureSpec, heightMeasureSpec); final
int heightMode = MeasureSpec.getMode(heightMeasureSpec); if (heightMode != MeasureSpec.EXACTLY && ViewCompat.getFitsSystemWindows(this) && shouldOffsetFirstChild()) { int newHeight = getMeasuredHeight(); switch (heightMode) { case MeasureSpec.AT_MOST: newHeight = MathUtils.clamp( getMeasuredHeight() + getTopInset(), 0, MeasureSpec.getSize(heightMeasureSpec)); break; … <CoordinatorLayout fitsSystemWindows> <AppBarLayout fitsSystemWindows> <MaterialToolbar /> </AppBarLayout> <BottomAppBar /> </CoordinatorLayout> -
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { super.onMeasure(widthMeasureSpec, heightMeasureSpec); final
int heightMode = MeasureSpec.getMode(heightMeasureSpec); if (heightMode != MeasureSpec.EXACTLY && ViewCompat.getFitsSystemWindows(this) && shouldOffsetFirstChild()) { int newHeight = getMeasuredHeight(); switch (heightMode) { case MeasureSpec.AT_MOST: newHeight = MathUtils.clamp( getMeasuredHeight() + getTopInset(), 0, MeasureSpec.getSize(heightMeasureSpec)); break; … <CoordinatorLayout fitsSystemWindows> <AppBarLayout fitsSystemWindows> <MaterialToolbar /> </AppBarLayout> <BottomAppBar /> </CoordinatorLayout> -
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { super.onMeasure(widthMeasureSpec, heightMeasureSpec); final
int heightMode = MeasureSpec.getMode(heightMeasureSpec); if (heightMode != MeasureSpec.EXACTLY && ViewCompat.getFitsSystemWindows(this) && shouldOffsetFirstChild()) { int newHeight = getMeasuredHeight(); switch (heightMode) { case MeasureSpec.AT_MOST: newHeight = MathUtils.clamp( getMeasuredHeight() + getTopInset(), 0, MeasureSpec.getSize(heightMeasureSpec)); break; … <CoordinatorLayout fitsSystemWindows> <AppBarLayout fitsSystemWindows> <MaterialToolbar /> </AppBarLayout> <BottomAppBar /> </CoordinatorLayout> ͱͱͷAppbarLayoutߴ͞ + status bar ͷߴ͞ -
APIϨϕϧΛߟྀ͢Δඞཁ͕͋Δ • API23+ • windowLightStatusBar • API27+ • windowLightNavigationBar •
API29+ • enforceStatusBarContrast • enforceNavigationBarContrast -
APIϨϕϧΛߟྀ͢Δඞཁ͕͋Δ • API23+ • windowLightStatusBar • API27+ • windowLightNavigationBar •
API29+ • enforceStatusBarContrast • enforceNavigationBarContrast 21 22 23 24 25 26 27 28 29 -
windowLightStatusBar ✗ / false windowLightNavigationBar ✗ / false statusBarColor transparent
navigationBarColor #B3000000 API21 ~ 28 -
windowLightStatusBar ✗ / false windowLightNavigationBar ✗ / false statusBarColor transparent
navigationBarColor transparent API29 -
<style name="Base.AppTheme" parent=“…”> <item name="android:windowLightStatusBar" tools:targetApi="m">@bool/ use_light_status</item> <item name="android:windowLightNavigationBar" tools:targetApi=“o_mr1">@bool/use_light_navigation</item>
</style> <resources> <bool name="use_light_status">true</bool> </resources> values-notnight-v23 -
<style name="Base.AppTheme" parent=“…”> <item name="android:windowLightStatusBar" tools:targetApi="m">@bool/ use_light_status</item> <item name="android:windowLightNavigationBar" tools:targetApi=“o_mr1">@bool/use_light_navigation</item>
</style> <resources> <bool name="use_light_status">true</bool> </resources> values-notnight-v23 <resources> <bool name="use_light_status">false</bool> </resources> values -
<item name="android:windowLightStatusBar" tools:targetApi="m">@bool/ use_light_status</item> <item name="android:windowLightNavigationBar" tools:targetApi=“o_mr1">@bool/use_light_navigation</item> </style> <resources> <bool
name="use_light_navigation">true</bool> </resources> values-notnight-v27 -
<resources> <bool name="use_light_navigation">true</bool> </resources> values-notnight-v27 <resources> <bool name="use_light_navigation">false</bool> </resources> values
<item name="android:windowLightStatusBar" tools:targetApi="m">@bool/ use_light_status</item> <item name="android:windowLightNavigationBar" tools:targetApi=“o_mr1">@bool/use_light_navigation</item> </style> -
<style name="AppTheme" parent="Base.AppTheme"> <item name="android:statusBarColor">@color/status_bar_scrim</item> <item name="android:navigationBarColor">@color/nav_bar_scrim</item> </style> values-night <color
name="status_bar_scrim">@color/transparent</color> values-notnight-v23 <color name="status_bar_scrim">@color/transparent</color> -
<style name="AppTheme" parent="Base.AppTheme"> <item name="android:statusBarColor">@color/status_bar_scrim</item> <item name="android:navigationBarColor">@color/nav_bar_scrim</item> </style> values-night <color
name="status_bar_scrim">@color/transparent</color> values-notnight-v23 <color name="status_bar_scrim">@color/transparent</color> values <color name="status_bar_scrim">@color/system_ui_scrim_black</color> -
ςʔϚͰߏங͢Δ·ͱΊ • σΟϨΫτϦΛΈ߹Θͤͯɺߏங͢Δ • values • values-night • values-notnight-v23 •
values-notnight-v27 • values-v29 • API29transparentΛࢦఆ͢Δͱɺ͍͍ײ͡ʹഎܠ৭Λௐͯ͘͠Ε Δ -
class StatusBarScrim override fun onApplyWindowInsets(insets: WindowInsets): WindowInsets { if (insets
!= lastWindowInsets) { lastWindowInsets = insets // Set this view as invisible if there isn't a top inset visibility = if (insets.systemWindowInsetTop > 0) VISIBLE else INVISIBLE // Request a layout to change size requestLayout() } return insets } -
class StatusBarScrim override fun onApplyWindowInsets(insets: WindowInsets): WindowInsets { if (insets
!= lastWindowInsets) { lastWindowInsets = insets // Set this view as invisible if there isn't a top inset visibility = if (insets.systemWindowInsetTop > 0) VISIBLE else INVISIBLE // Request a layout to change size requestLayout() } return insets } -
class StatusBarScrim override fun onApplyWindowInsets(insets: WindowInsets): WindowInsets { if (insets
!= lastWindowInsets) { lastWindowInsets = insets // Set this view as invisible if there isn't a top inset visibility = if (insets.systemWindowInsetTop > 0) VISIBLE else INVISIBLE // Request a layout to change size requestLayout() } return insets } -
InsetterϥΠϒϥϦ • WindowInsetsɺSystemUiVisibilityΛ؆୯ʹѻ͏͜ͱ͕ग़དྷΔ void setOnApplyInsetsListener(View view, OnApplyInsetsListener listener) public interface
OnApplyInsetsListener { void onApplyInsets( View view, WindowInsetsCompat insets, ViewState initialState ); } -
• WindowInsetsɺSystemUiVisibilityΛ؆୯ʹѻ͏͜ͱ͕ग़དྷΔ void setOnApplyInsetsListener(View view, OnApplyInsetsListener listener) public interface OnApplyInsetsListener
{ void onApplyInsets( View view, WindowInsetsCompat insets, ViewState initialState ); } view.updatePadding( top = view.paddingTop + insets.systemWindowInsetTop ) -
• WindowInsetsɺSystemUiVisibilityΛ؆୯ʹѻ͏͜ͱ͕ग़དྷΔ void setOnApplyInsetsListener(View view, OnApplyInsetsListener listener) public interface OnApplyInsetsListener
{ void onApplyInsets( View view, WindowInsetsCompat insets, ViewState initialState ); } view.updatePadding( top = view.paddingTop + insets.systemWindowInsetTop ) setOnApplyWindowInsetsListenerෳճίʔϧ͞ΕΔͷͰɺ͜Εͩͱμϝ -
• WindowInsetsɺSystemUiVisibilityΛ؆୯ʹѻ͏͜ͱ͕ग़དྷΔ void setOnApplyInsetsListener(View view, OnApplyInsetsListener listener) public interface OnApplyInsetsListener
{ void onApplyInsets( View view, WindowInsetsCompat insets, ViewState initialState ); } view.updatePadding( top = initialState.paddings.top + insets.systemWindowInsetTop ) -
ࢀߟ • https://github.com/google/iosched • https://medium.com/androiddevelopers/gesture-navigation- going-edge-to-edge-812f62e4e83e • https://medium.com/androiddevelopers/why-would-i-want- to-fitssystemwindows-4e26d9ce1eec •
https://speakerdeck.com/ytakahashi/xiang-jie-windowinsets • https://satoshun.github.io/2020/01/android- fitssystemwindows/ -
DroidKaigi 2020 System UIΛίϯτϩʔϧͯ͠ɺ ը໘Λ࠷େݶʹੜ͔ͨ͠ΞϓϦΛߏங͢Δ Sato Shun GitHub: satoshun Twitter:
@stsn_jp CyberAgent, Inc.