Android P刘海屏适配及实现原理

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

3. 小结

由开发者选项的模拟刘海作为切入口,分析Android P刘海屏的实现原理,其中涉及到3个进程内容,分别是Settings进程、SystemUI进程以及最重要的System_server进程,三个进程各司其职

你可能感兴趣的:(Android)