介绍:
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() 返回缺口位置的一个列表,表明可能存在多个缺口区域(一般只有一个),其中列表中顺序分别为:上缺口参数、左缺口参数、下右缺口参数
应用适配
全屏窗口:
WindowManager.LayoutParams lp = getWindow().getAttributes();
lp.layoutInDisplayCutoutMode = WindowManager.LayoutParams.LAYOUT_IN_DISPLAY_CUTOUT_MODE_NEVER;
WindowManager.LayoutParams lp = getWindow().getAttributes();
lp.layoutInDisplayCutoutMode = WindowManager.LayoutParams.LAYOUT_IN_DISPLAY_CUTOUT_MODE_SHORT_EDGES;
非全屏窗口(状态栏显示):不用适配
相关类
显示刘海缺口窗口
设置–开发者选项–模拟“刘海屏”
选择不同的刘海屏选项后会回调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 pathAndDisplayCutoutFromSpec(String spec,
int displayWidth, int displayHeight, float density) {
.....
final Pair 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);
}
......
}
由开发者选项的模拟刘海作为切入口,分析Android P刘海屏的实现原理,其中涉及到3个进程内容,分别是Settings进程、SystemUI进程以及最重要的System_server进程,三个进程各司其职