NavigationBar左侧布局方案探索一


前言

随着android大屏化趋势,原生的StatusBar和NavigationBar的布局位置已经不能满足现如今的需求。在车载领域,越来越多车载娱乐将android的NavigationBar布局做了调整,比较明显的一种布局方式如下图所示:将NavigationBar设计摆放在屏幕左侧。
首页展示
我们这边的最终效果如上图所示,从图中我们可以拆解下Navigation Bar的修改需求

  1. Navigation bar布局在左侧
  2. Navigation Bar独占了屏幕一块区域,即应用区域在Navigation Bar的右侧区域
  3. Navigation Bar上边距调整

本章主要介绍Navigation Bar框架层修改的方案探索。主要分两部分介绍:

  1. WindowManager窗口简述:主要介绍修改点涉及到的一些关键类和关键方法。
  2. 方案实现细节概述:主要介绍我们的方案修改点。

一、WindowManager窗口布局简述

文章InputManagerService源码分析三:事件分发中我们了解了Window的创建过程以及ViewRootImpl的addToDisplay()方法,现在我们从ViewRootImpl的另一个方法relayoutWindow()了解下Window的测量过程。这个过程这里主要分两部分介绍,第一部分主要介绍流程进入DIsplayContent之前,第二部分主要介绍流程进入DisplayContent。

1.1 流程进入DisplayContent前

代码这里暂不一步步分析,直接粘贴出部分时序图,如下图所示。
流程进入DiaplayContent前

该流程图主要描述了从客户进程中ViewRootImpl类的relayouWindow()方法,掉到DisplayContent类的applySurfaceChangesTransaction()方法为止。

1.2 流程进入DiaplayContent

下面的流程图主要介绍了调用DisplayContent类的applySurfaceChangesTransaction()方法。
流程进入DisplayContent
其中标注了实现我们需求所涉及到的几个关键方法:

  1. layoutStatusBar():状态栏布局位置测算方法。
  2. layoutNavigationBar():导航栏布局位置测算方法。
  3. layoutWindowLw():一般Window布局位置测试方法。
    后面一节我们会介绍如何修改这几个方法,完成我们的设计需求。

二、修改步骤

拿到设计需求,工程师分析后一般会提出如下两种开发方案,各有优劣

  1. 各个应用自行适配即各个应用布局添加Margin距离
    这种设计不足之处:增加了各个应用的开发成本,不利于三方应用的移植
    这种设计的优点:应用层修改,不容易出错,风险可控
  2. Framework窗口根据设计需求裁剪Window窗口大小:
    这种设计不足之处:Framework修改,风险难控
    这种设计的优点:应用层无需修改,减少了应用层开发成本,便于三方app移植

本文主要介绍的是第二种方法,即从Framework层出发对Navigation Bar的位置,Window大小进行裁剪。

1. Navigation Bar左侧布局

android中Status Bar和Navigation Bar的布局位置是由WindowManager负责管理的,应用端(一般指SystemUI)是无法调整Bar的上下、左右位置的。
android中Navigation布局是会根据配置的参数设置Navigation Bar的位置,代码如下

//frameworks\base\services\core\java\com\android\server\wm\DisplayPolicy.java
private boolean layoutNavigationBar(DisplayFrames displayFrames, int uiMode, boolean navVisible,
            boolean navTranslucent, boolean navAllowedHidden,
            boolean statusBarForcesShowingNavigation) {
        if (mNavigationBar == null) {
            return false;
        }
        ...
        mNavigationBarPosition = navigationBarPosition(displayWidth, displayHeight, rotation);
        ...
        //根据配置位置参数设置Navigation Bar的Window的位置参数
        if (mNavigationBarPosition == NAV_BAR_BOTTOM) {
            ...
        } else if (mNavigationBarPosition == NAV_BAR_RIGHT) {
            ...
        } else if (mNavigationBarPosition == NAV_BAR_LEFT) {
            ...
        }

        ...
        mNavigationBar.getWindowFrames().setFrames(navigationFrame /* parentFrame */,
                navigationFrame /* displayFrame */, navigationFrame /* overscanFrame */,
                displayFrames.mDisplayCutoutSafe /* contentFrame */,
                navigationFrame /* visibleFrame */, sTmpRect /* decorFrame */,
                navigationFrame /* stableFrame */,
                displayFrames.mDisplayCutoutSafe /* outsetFrame */);
        mNavigationBar.getWindowFrames().setDisplayCutout(displayFrames.mDisplayCutout);
        mNavigationBar.computeFrameLw();
        mNavigationBarController.setContentFrame(mNavigationBar.getContentFrameLw());
        return mNavigationBarController.checkHiddenLw();
    }
//frameworks\base\services\core\java\com\android\server\wm\DisplayPolicy.java
int navigationBarPosition(int displayWidth, int displayHeight, int displayRotation) {
        //获取Navigation bar的配置参数
        String isCustomSystemUI = SystemProperties.get("persist.systemui.custom");
        if ("1".equals(isCustomSystemUI)) {
            return NAV_BAR_LEFT;
        }
        if (navigationBarCanMove() && displayWidth > displayHeight) {
            if (displayRotation == Surface.ROTATION_270) {
                return NAV_BAR_LEFT;
            } else if (displayRotation == Surface.ROTATION_90) {
                return NAV_BAR_RIGHT;
            }
        }
        return NAV_BAR_BOTTOM;
    }

设置Navigation Bar的位置主要是通过调用layoutNavigationBar()方法设置的,其中获取Navigation Bar布局位置的方法为navigationBarPosition()方法。可以看到位置主要配置于persist.systemui.custom中。本地模拟可以通过脚本测试调整结果

adb shell setprop persist.systemui.custom 1

集成版本可以在device.mk中配置位置参数,配置如下:

具体涉及的代码位置依据平台、项目而定.这里粘贴下我们平台配置代码位置,不具有参考性
//device\semidrive\x9h\common\DeviceCommmon.mk
PRODUCT_PROPERTY_OVERRIDES += \
      ro.adb.secure=1 \
      persist.systemui.custom=1

2. Navigation Bar独占屏幕区域

修改Navigation Bar的位置后会发现,有两个与需求不符:

  1. Activity的左侧被Navigation挡住,无法看清左侧内容。
  2. Navigation Bar占据了Status Bar的位置,没有按照需求留有一定距离的上边距离

这里我们通过layoutWindowLw()方法,分析NavigationBar布局对应用Activity边衬距离的影响。

//frameworks\base\services\core\java\com\android\server\wm\DisplayPolicy.java
public void layoutWindowLw(WindowState win, WindowState attached, DisplayFrames displayFrames) {
     ...
     //Window各种位置信息,其中我们关心的是边衬距离
     final Rect pf = windowFrames.mParentFrame;
     final Rect df = windowFrames.mDisplayFrame;
     final Rect of = windowFrames.mOverscanFrame;
     final Rect cf = windowFrames.mContentFrame;
     final Rect vf = windowFrames.mVisibleFrame;
     final Rect dcf = windowFrames.mDecorFrame;
     final Rect sf = windowFrames.mStableFrame;
     dcf.setEmpty();
     windowFrames.setParentFrameWasClippedByDisplayCutout(false);
     windowFrames.setDisplayCutout(displayFrames.mDisplayCutout);

    ...

     sf.set(displayFrames.mStable);

     if (type == TYPE_INPUT_METHOD) {
         ...
     } else if (type == TYPE_VOICE_INTERACTION) {
         ...
     } else if (type == TYPE_WALLPAPER) {
         layoutWallpaper(displayFrames, pf, df, of, cf);
     } else if (win == mStatusBar) {
         of.set(displayFrames.mUnrestricted);
         df.set(displayFrames.mUnrestricted);
         pf.set(displayFrames.mUnrestricted);
         cf.set(displayFrames.mStable);
         vf.set(displayFrames.mStable);

         if (adjust == SOFT_INPUT_ADJUST_RESIZE) {
             cf.bottom = displayFrames.mContent.bottom;
         } else {
             cf.bottom = displayFrames.mDock.bottom;
             vf.bottom = displayFrames.mContent.bottom;
         }
     } else {
         ...
         
         if (layoutInScreen && layoutInsetDecor) {
           //Activiy走这个流程
             ...
         } else if (layoutInScreen || (sysUiFl
                 & (View.SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN
                 | SYSTEM_UI_FLAG_LAYOUT_HIDE_NAVIGATION)) != 0) {
             if (DEBUG_LAYOUT) Slog.v(TAG, "layoutWindowLw(" + attrs.getTitle()
                     + "): IN_SCREEN");
             // A window that has requested to fill the entire screen just
             // gets everything, period.
             if (type == TYPE_STATUS_BAR_PANEL || type == TYPE_STATUS_BAR_SUB_PANEL) {
               ...
             } else if (type == TYPE_NAVIGATION_BAR || type == TYPE_NAVIGATION_BAR_PANEL) {
                ...
             } else if ((type == TYPE_SECURE_SYSTEM_OVERLAY || type == TYPE_SCREENSHOT)
                     && ((fl & FLAG_FULLSCREEN) != 0)) {
               ...
             } else if (type == TYPE_BOOT_PROGRESS) {
               ...
             } else if ((fl & FLAG_LAYOUT_IN_OVERSCAN) != 0
                     && type >= FIRST_APPLICATION_WINDOW && type <= LAST_SUB_WINDOW) {
               ...
             } else if ((sysUiFl & SYSTEM_UI_FLAG_LAYOUT_HIDE_NAVIGATION) != 0
                     && (type == TYPE_STATUS_BAR
                     || type == TYPE_TOAST
                     || type == TYPE_DOCK_DIVIDER
                     || type == TYPE_VOICE_INTERACTION_STARTING
                     || (type >= FIRST_APPLICATION_WINDOW && type <= LAST_SUB_WINDOW))) {
                ... 
             } else if ((sysUiFl & View.SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN) != 0) {
                 ...
             } else {
                 cf.set(displayFrames.mRestricted);
                 of.set(displayFrames.mRestricted);
                 df.set(displayFrames.mRestricted);
                 pf.set(displayFrames.mRestricted);
             }

             applyStableConstraints(sysUiFl, fl, cf, displayFrames);

             if (adjust != SOFT_INPUT_ADJUST_NOTHING) {
                 vf.set(displayFrames.mCurrent);
             } else {
                 vf.set(cf);
             }
         } else if (attached != null) {
            ...
         } else {
            ...
         }
     }
     ...
     //may occur errors for window's region
     cf.left = getNavigationBarWidth(displayFrames.mRotation, mService.mPolicy.getUiMode());
     if (DEBUG_LAYOUT) Slog.v(TAG, "Compute frame " + attrs.getTitle()
             + ": sim=#" + Integer.toHexString(sim)
             + " attach=" + attached + " type=" + type
             + String.format(" flags=0x%08x", fl)
             + " pf=" + pf.toShortString() + " df=" + df.toShortString()
             + " of=" + of.toShortString()
             + " of=" + of.toShortString()
             + " cf=" + cf.toShortString() + " vf=" + vf.toShortString()
             + " dcf=" + dcf.toShortString()
             + " sf=" + sf.toShortString()
             + " osf=" + windowFrames.mOutsetFrame.toShortString() + " " + win);

     if (!sTmpLastParentFrame.equals(pf)) {
         windowFrames.setContentChanged(true);
     }

     win.computeFrameLw();
     // Dock windows carve out the bottom of the screen, so normal windows
     // can't appear underneath them.
     if (type == TYPE_INPUT_METHOD && win.isVisibleLw()
             && !win.getGivenInsetsPendingLw()) {
         offsetInputMethodWindowLw(win, displayFrames);
     }
     if (type == TYPE_VOICE_INTERACTION && win.isVisibleLw()
             && !win.getGivenInsetsPendingLw()) {
         offsetVoiceInputWindowLw(win, displayFrames);
     }
     
}

从代码中可以看出WindowManager会根据各个Window的属性计算各个Window的位置信息,这些位置信息会影响我们应用Window的位置,Window的边衬距离。这里我们主要通过修改如下一句代码实现Navigation Bar独占屏幕左侧,Activity左边添加Margin。

//frameworks\base\services\core\java\com\android\server\wm\DisplayPolicy.java
 cf.left = getNavigationBarWidth(displayFrames.mRotation, mService.mPolicy.getUiMode());

关于验证:
我们主要通过如下三种方式验证我们修改方式的正确性:
1.车机应用以Activity为载体的页面显示正常;
2.三方游戏(隐藏状态栏)无左边距,亦显示正常;
3.三方其他应用出现过问题,可以调整Activity的样式解决。

关于第3点出现的问题和修改方式,因为涉及到平台化的问题,比较重要,我们单独拉出一章节来介绍。
这种修改比较粗糙,为全局性修改,实际情况我们只需修改Activity的边衬距离即可。后续出现问题,我们即可以按照这种思路来解决问题。

3. Navigation Bar边距调节

上一节主要通过layoutWindowLw()我们了解了普通Window位置信息的处理流程。本节以layoutNavigationBar()方法为了解下修改Navigation Bar上面距的方法。layoutNavigationBar()方法如下:

private boolean layoutNavigationBar(DisplayFrames displayFrames, int uiMode, boolean navVisible,
          boolean navTranslucent, boolean navAllowedHidden,
          boolean statusBarForcesShowingNavigation) {
      if (mNavigationBar == null) {
          return false;
      }

      final Rect navigationFrame = sTmpNavFrame;
      boolean transientNavBarShowing = mNavigationBarController.isTransientShowing();
      // Force the navigation bar to its appropriate place and size. We need to do this directly,
      // instead of relying on it to bubble up from the nav bar, because this needs to change
      // atomically with screen rotations.
      final int rotation = displayFrames.mRotation;
      final int displayHeight = displayFrames.mDisplayHeight;
      final int displayWidth = displayFrames.mDisplayWidth;
      final Rect dockFrame = displayFrames.mDock;
      mNavigationBarPosition = navigationBarPosition(displayWidth, displayHeight, rotation);

      final Rect cutoutSafeUnrestricted = sTmpRect;
      cutoutSafeUnrestricted.set(displayFrames.mUnrestricted);
      cutoutSafeUnrestricted.intersectUnchecked(displayFrames.mDisplayCutoutSafe);

      if (mNavigationBarPosition == NAV_BAR_BOTTOM) {
          ...
      } else if (mNavigationBarPosition == NAV_BAR_RIGHT) {
         ...
      } else if (mNavigationBarPosition == NAV_BAR_LEFT) {
          // Seascape screen; nav bar goes to the left.
          final int right = cutoutSafeUnrestricted.left
                  + getNavigationBarWidth(rotation, uiMode);
          //这里修改Navigation Bar的上边距
          boolean isStatusBarHide = !mStatusBar.isVisibleLw();
          int marginTop = isStatusBarHide?0:getStatusBarHeight(displayFrames);
          navigationFrame.set(displayFrames.mUnrestricted.left, marginTop, right,displayHeight);
          displayFrames.mStable.left = displayFrames.mStableFullscreen.left = right;
          if (transientNavBarShowing) {
              mNavigationBarController.setBarShowingLw(true);
          } else if (navVisible) {
              mNavigationBarController.setBarShowingLw(true);
              dockFrame.left = displayFrames.mRestricted.left =
                      displayFrames.mRestrictedOverscan.left = right;
          } else {
              // We currently want to hide the navigation UI - unless we expanded the status bar.
              mNavigationBarController.setBarShowingLw(statusBarForcesShowingNavigation);
          }
          if (navVisible && !navTranslucent && !navAllowedHidden
                  && !mNavigationBar.isAnimatingLw()
                  && !mNavigationBarController.wasRecentlyTranslucent()) {
              // If the nav bar is currently requested to be visible, and not in the process of
              // animating on or off, then we can tell the app that it is covered by it.
              displayFrames.mSystem.left = right;
          }
      }

      // Make sure the content and current rectangles are updated to account for the restrictions
      // from the navigation bar.
      displayFrames.mCurrent.set(dockFrame);
      displayFrames.mVoiceContent.set(dockFrame);
      displayFrames.mContent.set(dockFrame);
      // And compute the final frame.
      sTmpRect.setEmpty();
      mNavigationBar.getWindowFrames().setFrames(navigationFrame /* parentFrame */,
              navigationFrame /* displayFrame */, navigationFrame /* overscanFrame */,
              displayFrames.mDisplayCutoutSafe /* contentFrame */,
              navigationFrame /* visibleFrame */, sTmpRect /* decorFrame */,
              navigationFrame /* stableFrame */,
              displayFrames.mDisplayCutoutSafe /* outsetFrame */);
      mNavigationBar.getWindowFrames().setDisplayCutout(displayFrames.mDisplayCutout);
      mNavigationBar.computeFrameLw();
      mNavigationBarController.setContentFrame(mNavigationBar.getContentFrameLw());

      if (DEBUG_LAYOUT) Slog.i(TAG, "mNavigationBar frame: " + navigationFrame);
      return mNavigationBarController.checkHiddenLw();
  }

我们通过如下代码修改NaviationBar的上边距,我们需要根据状态栏的显示与否动态更新Navigation上边距。

 boolean isStatusBarHide = !mStatusBar.isVisibleLw();
 int marginTop = isStatusBarHide?0:getStatusBarHeight(displayFrames);
 navigationFrame.set(displayFrames.mUnrestricted.left, marginTop, right, displayHeight);

三、注意事项

原生的android应用端可以设置Navigation Bar透明,经测试会与我们修改策略冲突,表现为:应用被Navigation Bar遮挡,即我们修改的策略不生效。需要做如下两种修改:

  1. 代码不能将Navigation设置为透明;
  2. styles.xml样式不能设置透明,即android:windowTranslucentNavigation为false.
<item name="android:windowTranslucentNavigation">false</item>

总结

我们Navigation Bar左侧布局方案介绍就到这里,方案只涉及到一部分WindowManager的管理,后续我们会详细介绍下WIndowManagerService。

评论 4
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值