まえおき
iPhone Xに始まった↑のような欠陥デザインは、Android 9.0でも正式に対応されたようです。みんな「ノッチ」とか言っていますが、Androidでは display cutout
と呼ぶようです。
display cutoutは全画面表示するようなアプリの動作にも影響が出るため、開発者向けリファレンスにも、 https://developer.android.com/guide/topics/display-cutout/ こんな感じのドキュメントが公式に公開されています。
display cutoutにはいろんな種類がある
Android 9.0エミュレータで開発者オプションを見てみると
corner | double | tall |
---|---|---|
の3種類を選ぶことができるようになっています。
これってようするに、端末ベンダーが割と自由にdisplay cutoutできるようになってたりするんじゃない???
と気になったので、Androidフレームワークにどういう形でdisplay cutoutが定義されているのか調べてみました。
あらかじめ言っておきますが、アプリ開発者には多分なんの約にも立ちません。完全に自分用のメモです。
開発者オプションが何をやってるかを調べてみる
display cutoutの形の定義
まずdisplay cutoutのシミュレート部分になにかヒントが無いかみてみます。
の下に、
- DisplayCutoutEmulationCornerOverlay/
- DisplayCutoutEmulationDoubleOverlay/
- DisplayCutoutEmulationNarrowOverlay/
- DisplayCutoutEmulationTallOverlay/
- DisplayCutoutEmulationWideOverlay/
の5つのオーバーレイデータがあって、おそらくそれが使われているのでしょう。 res/values/config.xml
にはそれぞれ↓のようなSVGパス定義とステータスバー等の高さ定義などがあります。
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
<!-- The bounding path of the cutout region of the main built-in display.
Must either be empty if there is no cutout region, or a string that is parsable by
{@link android.util.PathParser}.
The path is assumed to be specified in display coordinates with pixel units and in
the display's native orientation, with the origin of the coordinate system at the
center top of the display.
To facilitate writing device-independent emulation overlays, the marker `@dp` can be
appended after the path string to interpret coordinates in dp instead of px units.
Note that a physical cutout should be configured in pixels for the best results.
-->
<string translatable="false" name="config_mainBuiltInDisplayCutout">
M 0,0
L -72, 0
L -69.9940446283, 20.0595537175
C -69.1582133885, 28.4178661152 -65.2, 32.0 -56.8, 32.0
L 56.8, 32.0
C 65.2, 32.0 69.1582133885, 28.4178661152 69.9940446283, 20.0595537175
L 72, 0
Z
@dp
</string>
<!-- Whether the display cutout region of the main built-in display should be forced to
black in software (to avoid aliasing or emulate a cutout that is not physically existent).
-->
<bool name="config_fillMainBuiltInDisplayCutout">true</bool>
<!-- Height of the status bar -->
<dimen name="status_bar_height_portrait">48dp</dimen>
<dimen name="status_bar_height_landscape">28dp</dimen>
<!-- Height of area above QQS where battery/time go (equal to status bar height if > 48dp) -->
<dimen name="quick_qs_offset_height">48dp</dimen>
<!-- Total height of QQS (quick_qs_offset_height + 128) -->
<dimen name="quick_qs_total_height">176dp</dimen>
</resources>
この定義が https://android.googlesource.com/platform/frameworks/base/+/master/core/java/android/view/DisplayCutout.java で読み込まれるらしい。
cutoutの形を適用するためのオペレーション
89 private boolean setEmulationOverlay(String packageName) {
90 OverlayInfo[] overlays = getOverlayInfos();
91 String currentPackageName = null;
92 for (OverlayInfo o : overlays) {
93 if (o.isEnabled()) {
94 currentPackageName = o.packageName;
95 }
96 }
97
98 if (TextUtils.isEmpty(packageName) && TextUtils.isEmpty(currentPackageName)
99 || TextUtils.equals(packageName, currentPackageName)) {
100 // Already set.
101 return true;
102 }
103
104 final boolean result;
105 if (TextUtils.isEmpty(packageName)) {
106 result = mOverlayManager.setEnabled(currentPackageName, false, USER_SYSTEM);
107 } else {
108 result = mOverlayManager.setEnabledExclusiveInCategory(packageName, USER_SYSTEM);
109 }
110 updateState(mPreference);
111 return result;
112 }
OverlayManagerとかいうAPIを叩いているだけですね・・。
私は全く知らなかったんですが、Android 8.0くらいからテーマ設定を実現するためにシステムサービスに追加されているもののようです。
https://android.googlesource.com/platform/frameworks/base/+/eabc9e95768e7ac9acc3b32dc9ac2edf99c9e2c5
OverlayManagerService
このクラスは、DisplayCutoutについては何も知らないクラスで、純粋にOverlayInfoに基づいてオーバーレイするだけっぽい?
DisplayCutoutはどうシステムに反映されるか
DisplayCutoutの利用箇所を見てみると、既存の DisplayInfoの1プロパティとして組み込まれていて、WindowManagerあたりでinsetsを計算するときに使われています。
- http://androidxref.com/9.0.0_r3/xref/frameworks/base/core/java/android/view/DisplayInfo.java#152
- http://androidxref.com/9.0.0_r3/xref/frameworks/base/services/core/java/com/android/server/wm/utils/WmDisplayCutout.java#44
ステータスバーとか各種コンポーネントは、空気を読んで( View#getRootWindowInsets()
とかいろいろ使って)描画位置をずらしているようです。
- http://androidxref.com/9.0.0_r3/xref/frameworks/base/packages/SystemUI/src/com/android/systemui/statusbar/phone/KeyguardStatusBarView.java#261
- http://androidxref.com/9.0.0_r3/xref/frameworks/base/packages/SystemUI/src/com/android/systemui/statusbar/HeadsUpStatusBarView.java#198
開発者オプションで指定がない場合のDisplayCutoutはどこからやってくるのか
DisplayInfoはDisplayManagerServiceというシステムサービスが持ってるはずなので、そのあたりを見てみます。
http://androidxref.com/9.0.0_r3/xref/frameworks/base/services/core/java/com/android/server/display/DisplayManagerService.java
見た感じだとDisplayDeviceの情報を、起動時に addLogicalDisplayLocked
でメモリ上にキャッシュしてるだけですね。
デフォルトのディスプレイは↓のあたりで定義されている。
397 if (mBuiltInDisplayId == SurfaceControl.BUILT_IN_DISPLAY_ID_MAIN) {
398 mInfo.name = res.getString(
399 com.android.internal.R.string.display_manager_built_in_display_name);
400 mInfo.flags |= DisplayDeviceInfo.FLAG_DEFAULT_DISPLAY
401 | DisplayDeviceInfo.FLAG_ROTATES_WITH_CONTENT;
402 if (res.getBoolean(com.android.internal.R.bool.config_mainBuiltInDisplayIsRound)
403 || (Build.IS_EMULATOR
404 && SystemProperties.getBoolean(PROPERTY_EMULATOR_CIRCULAR, false))) {
405 mInfo.flags |= DisplayDeviceInfo.FLAG_ROUND;
406 }
407 mInfo.displayCutout = DisplayCutout.fromResources(res, mInfo.width,
408 mInfo.height);
409 mInfo.type = Display.TYPE_BUILT_IN;
410 mInfo.densityDpi = (int)(phys.density * 160 + 0.5f);
411 mInfo.xDpi = phys.xDpi;
412 mInfo.yDpi = phys.yDpi;
413 mInfo.touch = DisplayDeviceInfo.TOUCH_INTERNAL;
- display_manager_built_in_display_name
- config_mainBuiltInDisplayIsRound
とかいろいろ面白そうなプロパティあるんだなー。へー。(どうでもいいw
361 public static DisplayCutout fromResources(Resources res, int displayWidth, int displayHeight) {
362 return fromSpec(res.getString(R.string.config_mainBuiltInDisplayCutout),
363 displayWidth, displayHeight, DENSITY_DEVICE_STABLE / (float) DENSITY_DEFAULT);
364 }
やはりここに行き着いた。
2913 <!-- The bounding path of the cutout region of the main built-in display.
2914 Must either be empty if there is no cutout region, or a string that is parsable by
2915 {@link android.util.PathParser}.
2916
2917 The path is assumed to be specified in display coordinates with pixel units and in
2918 the display's native orientation, with the origin of the coordinate system at the
2919 center top of the display.
2920
2921 To facilitate writing device-independent emulation overlays, the marker `@dp` can be
2922 appended after the path string to interpret coordinates in dp instead of px units.
2923 Note that a physical cutout should be configured in pixels for the best results.
2924
2925 Example for a 10px x 10px square top-center cutout:
2926 <string ...>M -5,0 L -5,10 L 5,10 L 5,0 Z</string>
2927 Example for a 10dp x 10dp square top-center cutout:
2928 <string ...>M -5,0 L -5,10 L 5,10 L 5,0 Z @dp</string>
2929
2930 @see https://www.w3.org/TR/SVG/paths.html#PathData
2931 -->
2932 <string translatable="false" name="config_mainBuiltInDisplayCutout"></string>
基本的にはカラの実装だけど、ベンダーが device/hoge_vendor/fugafuga/overlay/...
オーバーレイしてねーってことなんだろう。
現状、ぐぐってでてくるのは、カスタムロム製作者と思われる人が公開している
https://github.com/phhusson/vendor_hardware_overlay/blob/master/Asus/ZenFone5Z/res/values/notch.xml
このくらいだけど、まぁSVGで好きな形を定義できるんだろう。きっと。
改めてパス定義をみてみる
<string translatable="false" name="config_mainBuiltInDisplayCutout">
M 0,0
L -48, 0
C -48,48 -48,48 0,48
Z
@dp
@right
</string>
<string translatable="false" name="config_mainBuiltInDisplayCutout">
M 0,0
L -72, 0
L -69.9940446283, 20.0595537175
C -69.1582133885, 28.4178661152 -65.2, 32.0 -56.8, 32.0
L 56.8, 32.0
C 65.2, 32.0 69.1582133885, 28.4178661152 69.9940446283, 20.0595537175
L 72, 0
Z
@bottom
M 0,0
L -72, 0
L -69.9940446283, -20.0595537175
C -69.1582133885, -28.4178661152 -65.2, -32.0 -56.8, -32.0
L 56.8, -32.0
C 65.2, -32.0 69.1582133885, -28.4178661152 69.9940446283, -20.0595537175
L 72, 0
Z
@dp
</string>
なるほどわかった。
余談: HuaweiはAndroid 8.xでもnotch対応しているらしい
なんか力ずくで頑張って実装したっぽいです。すごい。
https://developer.huawei.com/consumer/en/devservice/doc/30210
まとめ
端末ベンダーは framework-res/res/values/config.xml の config_mainBuiltInDisplayCutout
をoverlay定義することで、好きな形のdisplay cutoutにすることができるよ!!