1. Android P 刘海屏的适配
-
介绍:
Android P 新增了刘海屏的支持,以下内容摘录至Google Android Developer官网:Android 9 支持最新的全面屏,其中包含为摄像头和扬声器预留空间的屏幕缺口。 通过 DisplayCutout 类可确定非功能区域的位置和形状,这些区域不应显示内容。 要确定这些屏幕缺口区域是否存在及其位置,请使用 getDisplayCutout() 函数。
全新的窗口布局属性 layoutInDisplayCutoutMode 让您的应用可以为设备屏幕缺口周围的内容进行布局。 您可以将此属性设为下列值之一:LAYOUT_IN_DISPLAY_CUTOUT_MODE_DEFAULT
LAYOUT_IN_DISPLAY_CUTOUT_MODE_SHORT_EDGES
LAYOUT_IN_DISPLAY_CUTOUT_MODE_NEVER
可以按以下方法在任何运行 Android 9 的设备或模拟器上模拟屏幕缺口:启用开发者选项。
在 Developer options 屏幕中,向下滚动至 Drawing 部分并选择 Simulate a display with a cutout。
选择屏幕缺口的大小。
注:我们建议您通过使用运行 Android 9 的设备或模拟器测试屏幕缺口周围的内容显示。官方链接:https://developer.android.com/about/versions/pie/android-9.0#cutout
-
属性以及接口介绍
-
新增的窗口属性:
-
[->WindowManager.java]
@LayoutInDisplayCutoutMode public int layoutInDisplayCutoutMode = LAYOUT_IN_DISPLAY_CUTOUT_MODE_DEFAULT; public static final int LAYOUT_IN_DISPLAY_CUTOUT_MODE_DEFAULT = 0; /** * @deprecated use {@link #LAYOUT_IN_DISPLAY_CUTOUT_MODE_SHORT_EDGES} * @hide */ @Deprecated public static final int LAYOUT_IN_DISPLAY_CUTOUT_MODE_ALWAYS = 1; public static final int LAYOUT_IN_DISPLAY_CUTOUT_MODE_SHORT_EDGES = 1; public static final int LAYOUT_IN_DISPLAY_CUTOUT_MODE_NEVER = 2;
-
其中每个参数值的含义如下:
LAYOUT_IN_DISPLAY_CUTOUT_MODE_DEFAULT : 只有当DisplayCutout完全包含在系统栏中时,才允许窗口延伸到DisplayCutout区域。 否则,窗口布局不与DisplayCutout区域重叠。 (有状态栏时,不下压;没有状态栏全屏显示时,下压);
LAYOUT_IN_DISPLAY_CUTOUT_MODE_NEVER : 该窗口决不允许与DisplayCutout区域重叠。 (强制下压);
LAYOUT_IN_DISPLAY_CUTOUT_MODE_SHORT_EDGES : 该窗口始终允许延伸到屏幕短边上的DisplayCutout区域。 (不处理);
LAYOUT_IN_DISPLAY_CUTOUT_MODE_ALWAYS : 不再使用,与LAYOUT_IN_DISPLAY_CUTOUT_MODE_SHORT_EDGES;
-
-
刘海参数
-
获取DisplayCutout对象:
方法一: private final DisplayInfo mInfo = new DisplayInfo(); getDisplay().getDisplayInfo(mInfo); mInfo.displayCutout; 方法二: final View decorView = getWindow().getDecorView(); DisplayCutout displayCutout = decorView.getRootWindowInsets().getDisplayCutout();
-
获取缺口位置和安全区域位置:
getBoundingRects ():返回Rects的列表,每个Rects都是显示屏上非功能区域的边界矩形。 getSafeInsetXXX ():返回安全区域的距离屏幕的距离(XXX表示Left,Right等)
需要注意的是,其中getBoundingRects() 返回缺口位置的一个列表,表明可能存在多个缺口区域(一般只有一个),其中列表中顺序分别为:上缺口参数、左缺口参数、下右缺口参数
-
-
-
应用适配
-
全屏窗口:
- 若想自身内容不被遮挡,可配置窗口属性LAYOUT_IN_DISPLAY_CUTOUT_MODE_NEVER进行强制下压,或不做任何配置直接使用默认值;然后获取刘海的参数应用内部适配
WindowManager.LayoutParams lp = getWindow().getAttributes(); lp.layoutInDisplayCutoutMode = WindowManager.LayoutParams.LAYOUT_IN_DISPLAY_CUTOUT_MODE_NEVER;
- 若想自身内容显示在刘海区域下,可配置窗口属性LAYOUT_IN_DISPLAY_CUTOUT_MODE_SHORT_EDGES,系统将不做任何处理;
WindowManager.LayoutParams lp = getWindow().getAttributes(); lp.layoutInDisplayCutoutMode = WindowManager.LayoutParams.LAYOUT_IN_DISPLAY_CUTOUT_MODE_SHORT_EDGES;
-
非全屏窗口(状态栏显示):不用适配
-
2. 实现的原理
-
相关类
- 设置:
EmulateDisplayCutoutPreferenceController.java
OverlayManagerWrapper.java - Framework:
OverlayManagerService.java
OverlayManagerServiceImpl.java
DisplayManagerService.java
LocalDisplayAdapter.java
DisplayAdapter.java
DisplayCutout.java
PhoneWindowManager.java
WindowManager.java - Overlay的资源应用:
DisplayCuoutEmulationTallOverlay;
DisplayCuoutEmulationWideOverlay;
DisplayCuoutEmulationCornerOverlay;
DisplayCuoutEmulationDoubleOverlay;
DisplayCuoutEmulationNarrowOverlay; - SystemUI:
ScreenDecorations.java
- 设置:
-
显示刘海缺口窗口
-
设置–开发者选项–模拟“刘海屏”
-
选择不同的刘海屏选项后会回调EmulateDisplayCutoutPreferenceController.setEmulationOverlay(); 然后执行mOverlayManager中的方法
[->EmulateDisplayCutoutPreferenceController.java]
private boolean setEmulationOverlay(String packageName) { .... final boolean result; if (TextUtils.isEmpty(packageName)) { result = mOverlayManager.setEnabled(currentPackageName, false, USER_SYSTEM); } else { result = mOverlayManager.setEnabledExclusiveInCategory(packageName, USER_SYSTEM); } ..... }
-
最终会执行OverlayManagerServiceImpl.setEnabledExclusive(),通过enalbe和diable 不同资源的Overlay应用,实现动态资源替换(Overlay):
[->OverlayManagerServiceImpl.java]boolean setEnabledExclusive(@NonNull final String packageName, boolean withinCategory, final int userId) { .... // Disable the overlay. modified |= mSettings.setEnabled(disabledOverlayPackageName, userId, false); modified |= updateState(targetPackageName, disabledOverlayPackageName, userId, 0); .... // Enable the selected overlay. modified |= mSettings.setEnabled(packageName, userId, true); modified |= updateState(targetPackageName, packageName, userId, 0); ..... if (modified) { mListener.onOverlaysChanged(targetPackageName, userId); } return true; ...... }
-
通过mListener.onOverlaysChanged()通知Overlay资源变换,触发DisplayManagerService.LocalService.onOverlayChanged()的回调,然后更新DisplayCutout的mBounds和mSafeInsets 数据;
-
-
更新DisplayCutout数据
-
如上所述,DisplayManagerService.LocalService.onOverlayChanged()的回调 会触发LocalDisplayAdapter.LocalDisplayDevice.requestDisplayModesLocked()的执行,然后updateDeviceInfoLocked()–>sendDisplayDeviceEventLocked():
[->LocalDisplayAdapter.java]
.... @Override public void requestDisplayModesLocked(int colorMode, int modeId) { if (requestModeLocked(modeId) || requestColorModeLocked(colorMode)) { updateDeviceInfoLocked(); } } .... sendDisplayDeviceEventLocked(this, DISPLAY_DEVICE_EVENT_CHANGED); ..... /** * Sends a display device event to the display adapter listener asynchronously. */ protected final void sendDisplayDeviceEventLocked( final DisplayDevice device, final int event) { mHandler.post(new Runnable() { @Override public void run() { mListener.onDisplayDeviceEvent(device, event); } }); }
-
其中sendDisplayDeviceEventLocked(), 又会回调DisplayManagerService.DisplayAdapterListener.onDisplayDeviceEvent, 其中handleDisplayDeviceChanged(device)–>device.getDisplayDeviceInfoLocked():
[->DisplayManagerService.java]
private final class DisplayAdapterListener implements DisplayAdapter.Listener { @Override public void onDisplayDeviceEvent(DisplayDevice device, int event) { switch (event) { case DisplayAdapter.DISPLAY_DEVICE_EVENT_ADDED: handleDisplayDeviceAdded(device); break; case DisplayAdapter.DISPLAY_DEVICE_EVENT_CHANGED: handleDisplayDeviceChanged(device); break; case DisplayAdapter.DISPLAY_DEVICE_EVENT_REMOVED: handleDisplayDeviceRemoved(device); break; } } ...... } ...... private void handleDisplayDeviceChanged(DisplayDevice device) { synchronized (mSyncRoot) { DisplayDeviceInfo info = device.getDisplayDeviceInfoLocked(); if (!mDisplayDevices.contains(device)) { Slog.w(TAG, "Attempted to change non-existent display device: " + info); return; } int diff = device.mDebugLastLoggedDeviceInfo.diff(info); if (diff == DisplayDeviceInfo.DIFF_STATE) { Slog.i(TAG, "Display device changed state: \"" + info.name + "\", " + Display.stateToString(info.state)); } else if (diff != 0) { Slog.i(TAG, "Display device changed: " + info); } if ((diff & DisplayDeviceInfo.DIFF_COLOR_MODE) != 0) { try { mPersistentDataStore.setColorMode(device, info.colorMode); } finally { mPersistentDataStore.saveIfNeeded(); } } device.mDebugLastLoggedDeviceInfo = info; device.applyPendingDisplayDeviceInfoChangesLocked(); if (updateLogicalDisplaysLocked()) { scheduleTraversalLocked(false); } } }
-
最终又回到LocalDisplayAdapter.getDisplayDeviceInfoLocked(),
DisplayCutout.fromResources(res, mInfo.width, mInfo.height)方法会被调用;public DisplayDeviceInfo getDisplayDeviceInfoLocked() { .... mInfo.displayCutout = DisplayCutout.fromResources(res, mInfo.width, mInfo.height); ..... }
-
-
确定刘海的缺口形状和大小
-
刘海缺口的形状通过path来确定,而path数据则有上面各个overlay app中存储,R.string.config_mainBuiltInDisplayCutout
[-> DisplayCutout.java]
/** * Creates the bounding path according to @android:string/config_mainBuiltInDisplayCutout. * * @hide */ public static DisplayCutout fromResources(Resources res, int displayWidth, int displayHeight) { return fromSpec(res.getString(R.string.config_mainBuiltInDisplayCutout), displayWidth, displayHeight, DENSITY_DEVICE_STABLE / (float) DENSITY_DEFAULT); } private static Pair<Path, DisplayCutout> pathAndDisplayCutoutFromSpec(String spec, int displayWidth, int displayHeight, float density) { ..... final Pair<Path, DisplayCutout> result = new Pair<>(p, fromBounds(p)); ...... return result; } .... /** * Creates an instance from a bounding {@link Path}. * * @hide */ public static DisplayCutout fromBounds(Path path) { RectF clipRect = new RectF(); path.computeBounds(clipRect, false /* unused */); Region clipRegion = Region.obtain(); clipRegion.set((int) clipRect.left, (int) clipRect.top, (int) clipRect.right, (int) clipRect.bottom); Region bounds = new Region(); bounds.setPath(path, clipRegion); clipRegion.recycle(); return new DisplayCutout(ZERO_RECT, bounds, false /* copyArguments */); }
-
-
显示刘海缺口
-
显示刘海缺口在SystemUI中的ScreenDecorations,通过注册监听Display的变化来触发更新, 最终显示到屏幕上;
[->ScreenDecorations.java]
@Override protected void onAttachedToWindow() { super.onAttachedToWindow(); mContext.getSystemService(DisplayManager.class).registerDisplayListener(this, getHandler()); update(); } @Override protected void onDetachedFromWindow() { super.onDetachedFromWindow(); mContext.getSystemService(DisplayManager.class).unregisterDisplayListener(this); } ..... @Override public void onDisplayChanged(int displayId) { if (displayId == getDisplay().getDisplayId()) { update(); } } ..... private void update() { requestLayout(); getDisplay().getDisplayInfo(mInfo); mBounds.setEmpty(); mBoundingRect.setEmpty(); mBoundingPath.reset(); int newVisible; if (shouldDrawCutout(getContext()) && hasCutout()) { mBounds.set(mInfo.displayCutout.getBounds()); localBounds(mBoundingRect); updateBoundingPath(); invalidate(); newVisible = VISIBLE; } else { newVisible = GONE; } if (newVisible != getVisibility()) { setVisibility(newVisible); mVisibilityChangedListener.run(); } } .....
-
-
-
刘海下压
-
PhoneWindowManager 利用DisplayCutout数据,根据窗口的类型和属性等条件来判断是否下压窗口
[->PhoneWindowManager.java]
/** {@inheritDoc} */ @Override public void layoutWindowLw(WindowState win, WindowState attached, DisplayFrames displayFrames) { .... // Ensure that windows with a DEFAULT or NEVER display cutout mode are laid out in // the cutout safe zone. if (cutoutMode != LAYOUT_IN_DISPLAY_CUTOUT_MODE_ALWAYS) { final Rect displayCutoutSafeExceptMaybeBars = mTmpDisplayCutoutSafeExceptMaybeBarsRect; displayCutoutSafeExceptMaybeBars.set(displayFrames.mDisplayCutoutSafe); if (layoutInScreen && layoutInsetDecor && !requestedFullscreen && cutoutMode == LAYOUT_IN_DISPLAY_CUTOUT_MODE_DEFAULT) { // At the top we have the status bar, so apps that are // LAYOUT_IN_SCREEN | LAYOUT_INSET_DECOR but not FULLSCREEN // already expect that there's an inset there and we don't need to exclude // the window from that area. displayCutoutSafeExceptMaybeBars.top = Integer.MIN_VALUE; } if (layoutInScreen && layoutInsetDecor && !requestedHideNavigation && cutoutMode == LAYOUT_IN_DISPLAY_CUTOUT_MODE_DEFAULT) { // Same for the navigation bar. switch (mNavigationBarPosition) { case NAV_BAR_BOTTOM: displayCutoutSafeExceptMaybeBars.bottom = Integer.MAX_VALUE; break; case NAV_BAR_RIGHT: displayCutoutSafeExceptMaybeBars.right = Integer.MAX_VALUE; break; case NAV_BAR_LEFT: displayCutoutSafeExceptMaybeBars.left = Integer.MIN_VALUE; break; } } if (type == TYPE_INPUT_METHOD && mNavigationBarPosition == NAV_BAR_BOTTOM) { // The IME can always extend under the bottom cutout if the navbar is there. displayCutoutSafeExceptMaybeBars.bottom = Integer.MAX_VALUE; } // Windows that are attached to a parent and laid out in said parent already avoid // the cutout according to that parent and don't need to be further constrained. // Floating IN_SCREEN windows get what they ask for and lay out in the full screen. // They will later be cropped or shifted using the displayFrame in WindowState, // which prevents overlap with the DisplayCutout. if (!attachedInParent && !floatingInScreenWindow) { mTmpRect.set(pf); pf.intersectUnchecked(displayCutoutSafeExceptMaybeBars); parentFrameWasClippedByDisplayCutout |= !mTmpRect.equals(pf); } // Make sure that NO_LIMITS windows clipped to the display don't extend under the // cutout. df.intersectUnchecked(displayCutoutSafeExceptMaybeBars); } ...... }
-
3. 小结
由开发者选项的模拟刘海作为切入口,分析Android P刘海屏的实现原理,其中涉及到3个进程内容,分别是Settings进程、SystemUI进程以及最重要的System_server进程,三个进程各司其职