android framework13-status bar[03NavigationBar]

1,629 阅读11分钟

1.简介

  • 导航栏就是屏幕下方或者两侧几个按键,以前是物理按键,现在都虚拟的。
  • 常见的就是后退键,home键,recents键。
  • 当然了,如果有输入法的显示的话,可能会有个箭头按钮用来隐藏输入法。
  • 现在还支持手势操作,就是滑屏关闭页面,这时候导航栏可能就显示一个按钮线条。
  • 手机模式用的是SystemUi下的NavigationBar,本篇学习的就是这个
  • 如果是平板的话,那么导航按钮是在launcher3工程里,有个quickstep目录,里边有个taskbar目录下就是相关的导航功能了。

1.1.创建

>1.createNavigationBar

还是在CentralSurfacesImpl.java里边,通过Controller来创建的

    protected void createNavigationBar(@Nullable RegisterStatusBarResult result) {
    //参考2.1
        mNavigationBarController.createNavigationBars(true /* includeDefaultDisplay */, result);
    }

2.NavigationBarController

2.1.createNavigationBars

首先判断是否需要显示导航栏,需要的话再创建

        public void createNavigationBars(final boolean includeDefaultDisplay,
                RegisterStatusBarResult result) {
            updateAccessibilityButtonModeIfNeeded();

            // Don't need to create nav bar on the default display if we initialize TaskBar.
            //taskbar和navitaionbar是二选一的,如果已经初始化了taskbar,这里就不创建navigationBar了
            final boolean shouldCreateDefaultNavbar = includeDefaultDisplay
                    && !initializeTaskbarIfNecessary();//参考补充1
            Display[] displays = mDisplayManager.getDisplays();
            for (Display display : displays) {
                if (shouldCreateDefaultNavbar || display.getDisplayId() != DEFAULT_DISPLAY) {
                //参考补充2
                    createNavigationBar(display, null /* savedState */, result);
                }
            }
        }

>1.initializeTaskbarIfNecessary

  • 是否有必要初始化taskbar,也就是launcher3里的,我们这里的是navigationbar
    private boolean initializeTaskbarIfNecessary() {
        //大屏的话用taskbar
        boolean taskbarEnabled = mIsLargeScreen || mFeatureFlags.isEnabled(
                Flags.HIDE_NAVBAR_WINDOW);

        if (taskbarEnabled) {
            final int displayId = mContext.getDisplayId();
            // Hint to NavBarHelper if we are replacing an existing bar to skip extra work
            mNavBarHelper.setTogglingNavbarTaskbar(mNavigationBars.contains(displayId));
            //移除NavigationBar,如果存在的话
            removeNavigationBar(displayId);
            //初始化taskbar
            mTaskbarDelegate.init(displayId);
            mNavBarHelper.setTogglingNavbarTaskbar(false);
        } else {
            mTaskbarDelegate.destroy();
        }
        return taskbarEnabled;
    }

>2.createNavigationBar

            void createNavigationBar(Display display, Bundle savedState, RegisterStatusBarResult result) {
    //...
                final int displayId = display.getDisplayId();
                final boolean isOnDefaultDisplay = displayId == DEFAULT_DISPLAY;

                //默认设备,已经创建了taskbar的,就不要创建navigationBar了
                if (isOnDefaultDisplay && initializeTaskbarIfNecessary()) {
                    return;
                }

                final IWindowManager wms = WindowManagerGlobal.getWindowManagerService();

                try {
                    if (!wms.hasNavigationBar(displayId)) {
                        return;
                    }
                } catch (RemoteException e) {
                    return;
                }
    //..
                NavigationBarComponent component = mNavigationBarComponentFactory.create(
                        context, savedState);
                //navBar是通过注解生成的
                NavigationBar navBar = component.getNavigationBar();
                navBar.init();
                mNavigationBars.put(displayId, navBar);

                navBar.addOnAttachStateChangeListener(new View.OnAttachStateChangeListener() {
                    @Override
                    public void onViewAttachedToWindow(View v) {
                        if (result != null) {
                            navBar.setImeWindowStatus(display.getDisplayId(), result.mImeToken,
                                    result.mImeWindowVis, result.mImeBackDisposition,
                                    result.mShowImeSwitcher);
                        }
                    }

                    @Override
                    public void onViewDetachedFromWindow(View v) {
                        v.removeOnAttachStateChangeListener(this);
                    }
                });
            }

2.2NavigationBar实例化

>1.构造方法

        @Inject
        NavigationBar(
                NavigationBarView navigationBarView,
                NavigationBarFrame navigationBarFrame,
                @Nullable Bundle savedState,
                //...
        super(navigationBarView);
        mFrame = navigationBarFrame;                

注解生成的,如下,

>2.NavigationBarComponent

    @Subcomponent(modules = { NavigationBarModule.class })
    @NavigationBarComponent.NavigationBarScope
    public interface NavigationBarComponent {

        /** Factory for {@link NavigationBarComponent}. */
        @Subcomponent.Factory
        interface Factory {
            NavigationBarComponent create(
                    @BindsInstance @DisplayId Context context,
                    @BindsInstance @Nullable Bundle savedState);
        }

        /** */
        NavigationBar getNavigationBar();

>3.NavigationBarModule

这里有NavigationBar构造方法里用到的一些view

/** Module for {@link com.android.systemui.navigationbar.NavigationBarComponent}. */
@Module
public interface NavigationBarModule {
    /** A Layout inflater specific to the display's context. */
    @Provides
    @NavigationBarScope
    @DisplayId
    static LayoutInflater provideLayoutInflater(@DisplayId Context context) {
        return LayoutInflater.from(context);
    }

    /** */
    @Provides
    @NavigationBarScope
    static NavigationBarFrame provideNavigationBarFrame(@DisplayId LayoutInflater layoutInflater) {
        return (NavigationBarFrame) layoutInflater.inflate(R.layout.navigation_bar_window, null);
    }

    /** */
    @Provides
    @NavigationBarScope
    static NavigationBarView provideNavigationBarview(
            @DisplayId LayoutInflater layoutInflater, NavigationBarFrame frame) {
        View barView = layoutInflater.inflate(R.layout.navigation_bar, frame);
        return barView.findViewById(R.id.navigation_bar_view);
    }

    /** */
    @Provides
    @NavigationBarScope
    static EdgeBackGestureHandler provideEdgeBackGestureHandler(
            EdgeBackGestureHandler.Factory factory, @DisplayId Context context) {
        return factory.create(context);
    }

    /** A WindowManager specific to the display's context. */
    @Provides
    @NavigationBarScope
    @DisplayId
    static WindowManager provideWindowManager(@DisplayId Context context) {
        return context.getSystemService(WindowManager.class);
    }
}

相关的两个布局如下

>4.navigation_bar_window.xml

<com.android.systemui.navigationbar.NavigationBarFrame
    android:id="@+id/navigation_bar_frame"
    android:theme="@style/Theme.SystemUI"
    android:layout_height="match_parent"
    android:layout_width="match_parent">

</com.android.systemui.navigationbar.NavigationBarFrame>

>5.navigation_bar.xml

<com.android.systemui.navigationbar.NavigationBarView
    android:id="@+id/navigation_bar_view"
    android:layout_height="match_parent"
    android:layout_width="match_parent"
    android:clipChildren="false"
    android:clipToPadding="false"
    android:background="@drawable/system_bar_background">

    <com.android.systemui.navigationbar.NavigationBarInflaterView
        android:id="@+id/navigation_inflater"
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        android:clipChildren="false"
        android:clipToPadding="false" />

</com.android.systemui.navigationbar.NavigationBarView>

2.3.onInit()

  • mView(NavigationBarView)是mFrame(NavigationBarFrame)的child,具体看上边Module里的加载过程以及相关的布局。
  • 导航栏里边是加载了两种线性布局的,一种是横向的,一种是竖的,根据旋转角度显示不同的布局
        @Override
        public void onInit() {
            mView.setBarTransitions(mNavigationBarTransitions);
            mView.setTouchHandler(mTouchHandler);
            setNavBarMode(mNavBarMode);//把mode传递给mView,mode是啥以及咋来的后边讲
            mEdgeBackGestureHandler.setStateChangeCallback(mView::updateStates);
            mNavigationBarTransitions.addListener(this::onBarTransition);
            mView.updateRotationButton();

            mView.setVisibility(
                    mStatusBarKeyguardViewManager.isNavBarVisible() ? View.VISIBLE : View.INVISIBLE);

         //添加导航栏的控件mFrame(NavigationBarFrame)
            mWindowManager.addView(mFrame,//根据旋转角度获取布局参数
                    getBarLayoutParams(mContext.getResources().getConfiguration().windowConfiguration
                            .getRotation()));//参考补充1
            mDisplayId = mContext.getDisplayId();
            mIsOnDefaultDisplay = mDisplayId == DEFAULT_DISPLAY;

            // Ensure we try to get currentSysuiState from navBarHelper before command queue callbacks
            // start firing, since the latter is source of truth
            parseCurrentSysuiState();
            mCommandQueue.addCallback(this);
            mLongPressHomeEnabled = mNavBarHelper.getLongPressHomeEnabled();
            mNavBarHelper.init();
            mHomeButtonLongPressDurationMs = Optional.of(mDeviceConfigProxy.getLong(
                    DeviceConfig.NAMESPACE_SYSTEMUI,
                    HOME_BUTTON_LONG_PRESS_DURATION_MS,
                    /* defaultValue = */ 0
            )).filter(duration -> duration != 0);
            mDeviceConfigProxy.addOnPropertiesChangedListener(
                    DeviceConfig.NAMESPACE_SYSTEMUI, mHandler::post, mOnPropertiesChangedListener);
    //...

            // Respect the latest disabled-flags.
            mCommandQueue.recomputeDisableFlags(mDisplayId, false);

            mNotificationShadeDepthController.addListener(mDepthListener);
        }

>1.getBarLayoutParams

        private WindowManager.LayoutParams getBarLayoutParams(int rotation) {
        //参考补充2
            WindowManager.LayoutParams lp = getBarLayoutParamsForRotation(rotation);
            //顺道保存了4种旋转角度对应的layoutParam
            lp.paramsForRotation = new WindowManager.LayoutParams[4];
            for (int rot = Surface.ROTATION_0; rot <= Surface.ROTATION_270; rot++) {
                lp.paramsForRotation[rot] = getBarLayoutParamsForRotation(rot);
            }
            return lp;
        }

>2.getBarLayoutParamsForRotation

  • config_navBarCanMove,values-sw600dp下是false,values下是true,所以对于手机来说应该是true
  • android 13里横屏的时候导航键是在左右显示的,不像以前在底部
            private WindowManager.LayoutParams getBarLayoutParamsForRotation(int rotation) {
                int width = WindowManager.LayoutParams.MATCH_PARENT;
                int height = WindowManager.LayoutParams.MATCH_PARENT;
                int insetsHeight = -1;
                int gravity = Gravity.BOTTOM;
                boolean navBarCanMove = true;
                final Context userContext = mUserContextProvider.createCurrentUserContext(mContext);
                if (mWindowManager != null && mWindowManager.getCurrentWindowMetrics() != null) {
                    Rect displaySize = mWindowManager.getCurrentWindowMetrics().getBounds();
                    //读取配置
                    navBarCanMove = displaySize.width() != displaySize.height()
                            && userContext.getResources().getBoolean(
                            com.android.internal.R.bool.config_navBarCanMove);
                }
                if (!navBarCanMove) {
                //读取配置里写好的高度,手机平板高度不一样
                    height = userContext.getResources().getDimensionPixelSize(
                            com.android.internal.R.dimen.navigation_bar_frame_height);
                    insetsHeight = userContext.getResources().getDimensionPixelSize(
                            com.android.internal.R.dimen.navigation_bar_height);
                } else {
                    switch (rotation) {
                        case ROTATION_UNDEFINED:
                        case Surface.ROTATION_0:
                        case Surface.ROTATION_180://默认的竖屏,导航栏在底部,所以限制的是高度
                            height = userContext.getResources().getDimensionPixelSize(
                                    com.android.internal.R.dimen.navigation_bar_frame_height);
                            insetsHeight = userContext.getResources().getDimensionPixelSize(
                                    com.android.internal.R.dimen.navigation_bar_height);
                            break;
                        case Surface.ROTATION_90://90/270导航栏在左右两侧,所以限制的是宽度
                            gravity = Gravity.RIGHT;
                            width = userContext.getResources().getDimensionPixelSize(
                                    com.android.internal.R.dimen.navigation_bar_width);
                            break;
                        case Surface.ROTATION_270:
                            gravity = Gravity.LEFT;
                            width = userContext.getResources().getDimensionPixelSize(
                                    com.android.internal.R.dimen.navigation_bar_width);
                            break;
                    }
                }
                WindowManager.LayoutParams lp = new WindowManager.LayoutParams(
                        width,
                        height,
                        WindowManager.LayoutParams.TYPE_NAVIGATION_BAR,
    //...
                        PixelFormat.TRANSLUCENT);
    //...
                return lp;
            }

2.4.init方法

父类ViewController里的

@NavigationBarScope
public class NavigationBar extends ViewController<NavigationBarView> implements Callbacks {
  • 给view添加attach,detach监听.

  • 根据子类构造方法里"super(navigationBarView);"可知,这里的view是navigationBarView

          protected ViewController(T view) {
              mView = view;
          }
              public void init() {
                  if (mInited) {
                      return;
                  }
                  onInit();//子类2.3实现
                  mInited = true;
    
                  if (isAttachedToWindow()) {
                      mOnAttachStateListener.onViewAttachedToWindow(mView);
                  }
                  addOnAttachStateChangeListener(mOnAttachStateListener);
              }
    

3.NavigationBarView

public class NavigationBarView extends FrameLayout {

    public void onFinishInflate() {
        super.onFinishInflate();
        mNavigationInflaterView = findViewById(R.id.navigation_inflater);
        mNavigationInflaterView.setButtonDispatchers(mButtonDispatchers);

        updateOrientationViews();
        reloadNavIcons();
    }
    

3.1.updateNavButtonIcons

    public void updateNavButtonIcons() {
        // We have to replace or restore the back and home button icons when exiting or entering
        // carmode, respectively. Recents are not available in CarMode in nav bar so change
        // to recent icon is not required.
        final boolean useAltBack =
                (mNavigationIconHints & StatusBarManager.NAVIGATION_HINT_BACK_ALT) != 0;
        KeyButtonDrawable backIcon = mBackIcon;
        orientBackButton(backIcon);
        KeyButtonDrawable homeIcon = mHomeDefaultIcon;
        if (!mUseCarModeUi) {
            orientHomeButton(homeIcon);
        }
        //设置图标
        getHomeButton().setImageDrawable(homeIcon);
        getBackButton().setImageDrawable(backIcon);

        updateRecentsIcon();

        // Update IME button visibility, a11y and rotate button always overrides the appearance
        boolean disableImeSwitcher =
                (mNavigationIconHints & StatusBarManager.NAVIGATION_HINT_IME_SWITCHER_SHOWN) == 0
                || isImeRenderingNavButtons();
        mContextualButtonGroup.setButtonVisibility(R.id.ime_switcher, !disableImeSwitcher);

        mBarTransitions.reapplyDarkIntensity();

        boolean disableHome = isGesturalMode(mNavBarMode)
                || ((mDisabledFlags & View.STATUS_BAR_DISABLE_HOME) != 0);

        // Always disable recents when alternate car mode UI is active and for secondary displays.
        boolean disableRecent = isRecentsButtonDisabled();

        // Disable the home handle if both hone and recents are disabled
        boolean disableHomeHandle = disableRecent
                && ((mDisabledFlags & View.STATUS_BAR_DISABLE_HOME) != 0);

        boolean disableBack = !useAltBack && (mEdgeBackGestureHandler.isHandlingGestures()
                || ((mDisabledFlags & View.STATUS_BAR_DISABLE_BACK) != 0))
                || isImeRenderingNavButtons();

        // When screen pinning, don't hide back and home when connected service or back and
        // recents buttons when disconnected from launcher service in screen pinning mode,
        // as they are used for exiting.
        final boolean pinningActive = ActivityManagerWrapper.getInstance().isScreenPinningActive();
        if (mOverviewProxyEnabled) {
            // Force disable recents when not in legacy mode
            disableRecent |= !QuickStepContract.isLegacyMode(mNavBarMode);
            if (pinningActive && !QuickStepContract.isGesturalMode(mNavBarMode)) {
                disableBack = disableHome = false;
            }
        } else if (pinningActive) {
            disableBack = disableRecent = false;
        }

        ViewGroup navButtons = getCurrentView().findViewById(R.id.nav_buttons);
        if (navButtons != null) {
            LayoutTransition lt = navButtons.getLayoutTransition();
            if (lt != null) {
                if (!lt.getTransitionListeners().contains(mTransitionListener)) {
                    lt.addTransitionListener(mTransitionListener);
                }
            }
        }

        getBackButton().setVisibility(disableBack       ? View.INVISIBLE : View.VISIBLE);
        getHomeButton().setVisibility(disableHome       ? View.INVISIBLE : View.VISIBLE);
        getRecentsButton().setVisibility(disableRecent  ? View.INVISIBLE : View.VISIBLE);
        getHomeHandle().setVisibility(disableHomeHandle ? View.INVISIBLE : View.VISIBLE);
        notifyActiveTouchRegions();
    }

4.NavigationBarInflaterView

    public class NavigationBarInflaterView extends FrameLayout
            implements NavigationModeController.ModeChangedListener {

4.1.onFinishInflate

        protected void onFinishInflate() {
            super.onFinishInflate();
            inflateChildren();//补充1
            clearViews();
            //配置数据参考4.2,布局加载参考4.3
            inflateLayout(getDefaultLayout());
        }

>1.inflateChildren

  • 添加了 横屏和竖屏两种布局,根据实际情况显示
        private void inflateChildren() {
            removeAllViews();
            mHorizontal = (FrameLayout) mLayoutInflater.inflate(R.layout.navigation_layout,
                    this /* root */, false /* attachToRoot */);
            addView(mHorizontal);
            mVertical = (FrameLayout) mLayoutInflater.inflate(R.layout.navigation_layout_vertical,
                    this /* root */, false /* attachToRoot */);
            addView(mVertical);
            updateAlternativeOrder();
        }        

>2.navigation_layout.xml

  • 里边是2个horizontal的线性布局,显示在底部的导航栏
<FrameLayout
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:layout_marginStart="@dimen/rounded_corner_content_padding"
    android:layout_marginEnd="@dimen/rounded_corner_content_padding"
    android:paddingStart="@dimen/nav_content_padding"
    android:paddingEnd="@dimen/nav_content_padding"
    android:clipChildren="false"
    android:clipToPadding="false"
    android:id="@+id/horizontal">

    <com.android.systemui.navigationbar.buttons.NearestTouchFrame
        android:id="@+id/nav_buttons"
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        android:clipChildren="false"
        android:clipToPadding="false"
        systemui:isVertical="false">

        <LinearLayout
            android:id="@+id/ends_group"
            android:layout_width="match_parent"
            android:layout_height="match_parent"
            android:orientation="horizontal"
            android:clipToPadding="false"
            android:clipChildren="false" />

        <LinearLayout
            android:id="@+id/center_group"
            android:layout_width="wrap_content"
            android:layout_height="match_parent"
            android:layout_gravity="center"
            android:gravity="center"
            android:orientation="horizontal"
            android:clipToPadding="false"
            android:clipChildren="false" />

    </com.android.systemui.navigationbar.buttons.NearestTouchFrame>

</FrameLayout>

>3.navigation_layout_vertical.xml

  • 里边是两个vertical的线性布局,显示在两侧的导航栏,横屏的时候
<FrameLayout
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:layout_marginTop="@dimen/rounded_corner_content_padding"
    android:layout_marginBottom="@dimen/rounded_corner_content_padding"
    android:paddingTop="@dimen/nav_content_padding"
    android:paddingBottom="@dimen/nav_content_padding"
    android:id="@+id/vertical">
        <!!--帧布局-->
    <com.android.systemui.navigationbar.buttons.NearestTouchFrame 
        android:id="@+id/nav_buttons"
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        android:clipChildren="false"
        android:clipToPadding="false"
        systemui:isVertical="true">

        <com.android.systemui.navigationbar.buttons.ReverseLinearLayout
            android:id="@+id/ends_group"
            android:layout_width="match_parent"
            android:layout_height="match_parent"
            android:orientation="vertical"
            android:clipToPadding="false"
            android:clipChildren="false" />

        <com.android.systemui.navigationbar.buttons.ReverseLinearLayout
            android:id="@+id/center_group"
            android:layout_width="match_parent"
            android:layout_height="match_parent"
            android:gravity="center"
            android:orientation="vertical"
            android:clipToPadding="false"
            android:clipChildren="false" />

    </com.android.systemui.navigationbar.buttons.NearestTouchFrame>

</FrameLayout>

4.2.getDefaultLayout

  • 导航栏都显示哪些按钮?读取配置string

      protected String getDefaultLayout() {
          final int defaultResource = QuickStepContract.isGesturalMode(mNavBarMode)
                  ? R.string.config_navBarLayoutHandle
                  : mOverviewProxyService.shouldShowSwipeUpUI()
                          ? R.string.config_navBarLayoutQuickstep
                          : R.string.config_navBarLayout;
          return getContext().getString(defaultResource);
      }
    

>1.config_navBarLayout

分号隔开左中右,然后逗号隔开,w表示比重,A表示绝对值,C表示居中,具体的的逻辑看代码如何解析这些字符串

    <!-- Nav bar button default ordering/layout -->
    <string name="config_navBarLayout" translatable="false">left[.5W],back[1WC];home;recent[1WC],right[.5W]</string>
    <string name="config_navBarLayoutQuickstep" translatable="false">back[1.7WC];home;contextual[1.7WC]</string>
    <string name="config_navBarLayoutHandle" translatable="false">back[70AC];home_handle;ime_switcher[70AC]</string>

4.3.inflateLayout

  • 解析配置里的字符串,加载相关的按钮。

  • start ,end相关的按钮记载在ends_group容器里,中间又额外加了个spacer(比重为1)

  • home 按钮是加载在center_group容器里的,这个容器是居中显示的。

        protected void inflateLayout(String newLayout) {
            mCurrentLayout = newLayout;
            if (newLayout == null) {
                newLayout = getDefaultLayout();
            }
            String[] sets = newLayout.split(GRAVITY_SEPARATOR, 3);
            if (sets.length != 3) {
                newLayout = getDefaultLayout();
                sets = newLayout.split(GRAVITY_SEPARATOR, 3);
            }
            String[] start = sets[0].split(BUTTON_SEPARATOR);
            String[] center = sets[1].split(BUTTON_SEPARATOR);
            String[] end = sets[2].split(BUTTON_SEPARATOR);
            // Inflate these in start to end order or accessibility traversal will be messed up.
            inflateButtons(start, mHorizontal.findViewById(R.id.ends_group),
                    false /* landscape */, true /* start */);
            inflateButtons(start, mVertical.findViewById(R.id.ends_group),
                    true /* landscape */, true /* start */);
    
            inflateButtons(center, mHorizontal.findViewById(R.id.center_group),
                    false /* landscape */, false /* start */);
            inflateButtons(center, mVertical.findViewById(R.id.center_group),
                    true /* landscape */, false /* start */);
    
            addGravitySpacer(mHorizontal.findViewById(R.id.ends_group));
            addGravitySpacer(mVertical.findViewById(R.id.ends_group));
    
            inflateButtons(end, mHorizontal.findViewById(R.id.ends_group),
                    false /* landscape */, false /* start */);
            inflateButtons(end, mVertical.findViewById(R.id.ends_group),
                    true /* landscape */, false /* start */);
    
            updateButtonDispatchersCurrentView();
        }
    

>1.inflateButton

    protected View inflateButton(String buttonSpec, ViewGroup parent, boolean landscape,
            boolean start) {
        LayoutInflater inflater = landscape ? mLandscapeInflater : mLayoutInflater;
        View v = createView(buttonSpec, parent, inflater);
        if (v == null) return null;
        //通过解析配置里的字符串,设置比重,宽度等尺寸
        v = applySize(v, buttonSpec, landscape, start);
        parent.addView(v);//添加到容器里
        addToDispatchers(v);//添加到dispachers里

>2.createView

  • 根据配置里的字段名字加载对应的按钮布局

  • left和right,最终是被替换成space 和 menu_ime了

      View createView(String buttonSpec, ViewGroup parent, LayoutInflater inflater) {
          View v = null;
          String button = extractButton(buttonSpec);
          if (LEFT.equals(button)) {
              button = extractButton(NAVSPACE);
          } else if (RIGHT.equals(button)) {
              button = extractButton(MENU_IME_ROTATE);
          }
          if (HOME.equals(button)) {
              v = inflater.inflate(R.layout.home, parent, false);
          } else if (BACK.equals(button)) {
              v = inflater.inflate(R.layout.back, parent, false);
          } else if (RECENT.equals(button)) {
              v = inflater.inflate(R.layout.recent_apps, parent, false);
          } else if (MENU_IME_ROTATE.equals(button)) {
              v = inflater.inflate(R.layout.menu_ime, parent, false);
          } else if (NAVSPACE.equals(button)) {
              v = inflater.inflate(R.layout.nav_key_space, parent, false);
          } else if (CLIPBOARD.equals(button)) {
              v = inflater.inflate(R.layout.clipboard, parent, false);
          } else if (CONTEXTUAL.equals(button)) {
              v = inflater.inflate(R.layout.contextual, parent, false);
          } else if (HOME_HANDLE.equals(button)) {
              v = inflater.inflate(R.layout.home_handle, parent, false);
          } else if (IME_SWITCHER.equals(button)) {
              v = inflater.inflate(R.layout.ime_switcher, parent, false);
          } else if (button.startsWith(KEY)) {
              String uri = extractImage(button);
              int code = extractKeycode(button);
              v = inflater.inflate(R.layout.custom_key, parent, false);
              ((KeyButtonView) v).setCode(code);
              if (uri != null) {
                  if (uri.contains(":")) {
                      ((KeyButtonView) v).loadAsync(Icon.createWithContentUri(uri));
                  } else if (uri.contains("/")) {
                      int index = uri.indexOf('/');
                      String pkg = uri.substring(0, index);
                      int id = Integer.parseInt(uri.substring(index + 1));
                      ((KeyButtonView) v).loadAsync(Icon.createWithResource(pkg, id));
                  }
              }
          }
          return v;
      }
    

4.5.back,home,recents布局

>1.back.xml

<com.android.systemui.navigationbar.buttons.KeyButtonView
    android:id="@+id/back"
    android:layout_width="@dimen/navigation_key_width"
    android:layout_height="match_parent"
    android:layout_weight="0"
    systemui:keyCode="4"
    android:scaleType="center"
    android:contentDescription="@string/accessibility_back"
    android:paddingStart="@dimen/navigation_key_padding"
    android:paddingEnd="@dimen/navigation_key_padding"
    />

>2.home.xml

<com.android.systemui.navigationbar.buttons.KeyButtonView
    android:id="@+id/home"
    android:layout_width="@dimen/navigation_key_width"
    android:layout_height="match_parent"
    android:layout_weight="0"
    systemui:keyCode="3"
    android:scaleType="center"
    android:contentDescription="@string/accessibility_home"
    android:paddingStart="@dimen/navigation_key_padding"
    android:paddingEnd="@dimen/navigation_key_padding"
    />

>3.home_handle.xml

<com.android.systemui.navigationbar.gestural.NavigationHandle
    android:id="@+id/home_handle"
    android:layout_width="@dimen/navigation_home_handle_width"
    android:layout_height="match_parent"
    android:layout_weight="0"
    android:contentDescription="@string/accessibility_home"
    android:paddingStart="@dimen/navigation_key_padding"
    android:paddingEnd="@dimen/navigation_key_padding"
    />

>4.recent_apps.xml

<com.android.systemui.navigationbar.buttons.KeyButtonView
    android:id="@+id/recent_apps"
    android:layout_width="@dimen/navigation_key_width"
    android:layout_height="match_parent"
    android:layout_weight="0"
    android:scaleType="center"
    android:contentDescription="@string/accessibility_recent"
    android:paddingStart="@dimen/navigation_key_padding"
    android:paddingEnd="@dimen/navigation_key_padding"
    />

4.6.KeyButtonView

public class KeyButtonView extends ImageView implements ButtonInterface {
//...
public KeyButtonView(Context context, AttributeSet attrs, int defStyle, InputManager manager,
        UiEventLogger uiEventLogger) {
    super(context, attrs);
//... 这里有个code的属性
    mCode = a.getInteger(R.styleable.KeyButtonView_keyCode, KEYCODE_UNKNOWN);
    //...

    setClickable(true);
    //...
}

>1.onTouchEvent

从上边的布局可以知道,back,home是有keyCode值的

    public boolean onTouchEvent(MotionEvent ev) {
        final int action = ev.getAction();
        int x, y;
    //...

        switch (action) {
            case MotionEvent.ACTION_DOWN:
                mDownTime = SystemClock.uptimeMillis();
                mLongClicked = false;
                setPressed(true);

                mTouchDownX = (int) ev.getX();
                mTouchDownY = (int) ev.getY();
                //有code的view会把keyEvent发送出去
                if (mCode != KEYCODE_UNKNOWN) {
                    sendEvent(KeyEvent.ACTION_DOWN, 0, mDownTime);
                } else {
                    // Provide the same haptic feedback that the system offers for virtual keys.
                    performHapticFeedback(HapticFeedbackConstants.VIRTUAL_KEY);
                }

                removeCallbacks(mCheckLongPress);
                postDelayed(mCheckLongPress, ViewConfiguration.getLongPressTimeout());
                break;
            case MotionEvent.ACTION_MOVE:
            //...我们只关心点击事件,所以这个就不看了
                break;
            case MotionEvent.ACTION_CANCEL:
                setPressed(false);
                //同样发送keyEvent
                if (mCode != KEYCODE_UNKNOWN) {
                    sendEvent(KeyEvent.ACTION_UP, KeyEvent.FLAG_CANCELED);
                }
                removeCallbacks(mCheckLongPress);
                break;
            case MotionEvent.ACTION_UP:
                final boolean doIt = isPressed() && !mLongClicked;
                setPressed(false);
                final boolean doHapticFeedback = (SystemClock.uptimeMillis() - mDownTime) > 150;
                //...继续发送keyEvent
                if (mCode != KEYCODE_UNKNOWN) {
                    if (doIt) {
                        sendEvent(KeyEvent.ACTION_UP, 0);
                        sendAccessibilityEvent(AccessibilityEvent.TYPE_VIEW_CLICKED);
                    } else {
                        sendEvent(KeyEvent.ACTION_UP, KeyEvent.FLAG_CANCELED);
                    }
                } else {
                    // 没有设置code的view,看下有没有设置clickListener,有的话对应处理
                    if (doIt && mOnClickListener != null) {
                        mOnClickListener.onClick(this);
                        sendAccessibilityEvent(AccessibilityEvent.TYPE_VIEW_CLICKED);
                    }
                }
                removeCallbacks(mCheckLongPress);
                break;
        }

        return true;
    }

>2.sendEvent

    private void sendEvent(int action, int flags, long when) {

        final int repeatCount = (flags & KeyEvent.FLAG_LONG_PRESS) != 0 ? 1 : 0;
        final KeyEvent ev = new KeyEvent(mDownTime, when, action, mCode, repeatCount,
                0, KeyCharacterMap.VIRTUAL_KEYBOARD, 0,
                flags | KeyEvent.FLAG_FROM_SYSTEM | KeyEvent.FLAG_VIRTUAL_HARD_KEY,
                InputDevice.SOURCE_KEYBOARD);

        int displayId = INVALID_DISPLAY;

        // Make KeyEvent work on multi-display environment
        if (getDisplay() != null) {
            displayId = getDisplay().getDisplayId();
        }
        if (displayId != INVALID_DISPLAY) {
            ev.setDisplayId(displayId);
        }
        上边实例化一个keyEvent,这里发送出去,交给系统处理(PhoneWindowManager)
        mInputManager.injectInputEvent(ev, InputManager.INJECT_INPUT_EVENT_MODE_ASYNC);
    }

4.7.按钮的点击事件

>1.PhoneWindowManager.java

back和home的点击事件交给系统处理了,具体见上边keyButtonView的touch事件,下边简单看下系统key的处理逻辑

    public int interceptKeyBeforeQueueing(KeyEvent event, int policyFlags) {
        final int keyCode = event.getKeyCode();
        final boolean down = event.getAction() == KeyEvent.ACTION_DOWN;
        //...
    // Handle special keys.
    switch (keyCode) {
        case KeyEvent.KEYCODE_BACK: {
            if (down) {
                mBackKeyHandled = false;
            } else {
                if (!hasLongPressOnBackBehavior()) {
                    mBackKeyHandled |= backKeyPress();//处理back事件
                }
                // Don't pass back press to app if we've already handled it via long press
                if (mBackKeyHandled) {
                    result &= ~ACTION_PASS_TO_USER;
                }
            }
            break;
        }

        case KeyEvent.KEYCODE_VOLUME_DOWN:
        case KeyEvent.KEYCODE_VOLUME_UP:
        
//如果你打算拦击某种keyCode,这里的result返回0即可       
    return result;
}
    public long interceptKeyBeforeDispatching(IBinder focusedToken, KeyEvent event,
            int policyFlags) {
        final boolean keyguardOn = keyguardOn();
        final int keyCode = event.getKeyCode();
    //...    
    switch(keyCode) {
        case KeyEvent.KEYCODE_HOME:
            DisplayHomeButtonHandler handler = mDisplayHomeButtonHandlers.get(displayId);
            if (handler == null) {
                handler = new DisplayHomeButtonHandler(displayId);
                mDisplayHomeButtonHandlers.put(displayId, handler);
            }
            return handler.handleHomeButton(focusedToken, event);
            //...这个是recents
        case KeyEvent.KEYCODE_APP_SWITCH:
            if (!keyguardOn) {
                if (down && repeatCount == 0) {
                    preloadRecentApps();
                } else if (!down) {
                    toggleRecentApps();
                }
            }
            return key_consumed;

toggleRecentApps

    private void toggleRecentApps() {
        mPreloadedRecentApps = false; // preloading no longer needs to be canceled
        StatusBarManagerInternal statusbar = getStatusBarManagerInternal();
        if (statusbar != null) {
            statusbar.toggleRecentApps();
        }
    }

>2.adb shell

  • keycode的值: back【4】,home【3】,recents【187】,可以利用adb命令模拟按键操作
    adb shell input keyevent 4

>3.NavigationBar.java

recents的点击事件是下边添加的。

    private void prepareNavigationBarView() {
        mView.reorient();

        ButtonDispatcher recentsButton = mView.getRecentsButton();
        recentsButton.setOnClickListener(this::onRecentsClick); //recent这里有设置点击事件
        recentsButton.setOnTouchListener(this::onRecentsTouch);

        ButtonDispatcher homeButton = mView.getHomeButton();
        homeButton.setOnTouchListener(this::onHomeTouch);

        reconfigureHomeLongClick();

        ButtonDispatcher accessibilityButton = mView.getAccessibilityButton();
        accessibilityButton.setOnClickListener(this::onAccessibilityClick);
        accessibilityButton.setOnLongClickListener(this::onAccessibilityLongClick);
        updateAccessibilityStateFlags();

        ButtonDispatcher imeSwitcherButton = mView.getImeSwitchButton();
        imeSwitcherButton.setOnClickListener(this::onImeSwitcherClick);

        updateScreenPinningGestures();
    }

onRecentsClick

    private void onRecentsClick(View v) {
        if (LatencyTracker.isEnabled(mContext)) {
            LatencyTracker.getInstance(mContext).onActionStart(
                    LatencyTracker.ACTION_TOGGLE_RECENTS);
        }
        mCentralSurfacesOptionalLazy.get().ifPresent(CentralSurfaces::awakenDreams);
        mCommandQueue.toggleRecentApps();
    }

>4.Recents.java

上边的recent事件最终走的都是CommandQueue.toggleRecentApps()方法,而这个方法里调用的又是一堆callback,具体的recent相关的类是Recents.java

public Recents(Context context, RecentsImplementation impl, CommandQueue commandQueue) {
    mContext = context;
    mImpl = impl;//交给它处理了
    mCommandQueue = commandQueue;
}

@Override
public void start() {
    mCommandQueue.addCallback(this);// 添加到CommandQueue的callbacks里了
    mImpl.onStart(mContext);
}
    public void preloadRecentApps() {
        //...
        mImpl.preloadRecentApps();
    }

RecentsImplementation的实现类

public class OverviewProxyRecentsImpl implements RecentsImplementation {
//...
public void toggleRecentApps() {
    // If connected to launcher service, let it handle the toggle logic
    IOverviewProxy overviewProxy = mOverviewProxyService.getProxy();
    if (overviewProxy != null) {
        final Runnable toggleRecents = () -> {
            try {
                if (mOverviewProxyService.getProxy() != null) {
                //其实是这行代码调用才会显示recents内容的
                    mOverviewProxyService.getProxy().onOverviewToggle();
                    //我开始以为交给它处理了,后来发现不是这个。这个是平板模式taskbar用到的
                    mOverviewProxyService.notifyToggleRecentApps();
                }

        };

继续看OverviewProxyService.java,构造方法或者user变化的时候会调用下边的方法绑定服务

private static final String ACTION_QUICKSTEP = "android.intent.action.QUICKSTEP_SERVICE";
    private void internalConnectToCurrentUser() {
        //...
        Intent launcherServiceIntent = new Intent(ACTION_QUICKSTEP)
                .setPackage(mRecentsComponentName.getPackageName());
        try {
            mBound = mContext.bindServiceAsUser(launcherServiceIntent,
                    mOverviewServiceConnection,
                    Context.BIND_AUTO_CREATE | Context.BIND_FOREGROUND_SERVICE_WHILE_AWAKE,
                    UserHandle.of(mUserTracker.getUserId()));
        } catch (SecurityException e) {
//...
    }

看下要bind的服务是啥

    com.android.systemui/.recents.RecentsActivity -->
    <string name="config_recentsComponentName" translatable="false"
            >com.android.launcher3/com.android.quickstep.RecentsActivity</string>

我们需要的mOverviewProxy如下

            mOverviewProxy = IOverviewProxy.Stub.asInterface(service);

这个服务在模拟器里是系统自带的桌面,具体的逻辑可以看后边launcher3相关的文章。

5.NavigationModeController.java

5.1.addListener

        public int addListener(ModeChangedListener listener) {
            mListeners.add(listener);
            return getCurrentInteractionMode(mCurrentUserContext);
        }

>1.getCurrentInteractionMode

private int getCurrentInteractionMode(Context context) {
                int mode = context.getResources().getInteger(
                        com.android.internal.R.integer.config_navBarInteractionMode);
                return mode;
            }

>2.mode

  • 如下三种,有3个按钮,2个按钮,以及1个按钮

      <!-- Controls the navigation bar interaction mode:
           0: 3 button mode (back, home, overview buttons)
           1: 2 button mode (back, home buttons + swipe up for overview)
           2: gestures only for back, home and overview -->
      <integer name="config_navBarInteractionMode">0</integer>
    

6.总结

这里简单整理了下导航栏的创建过程。

  • 是由NavigationBarController实例化NavigationBar
  • NavigationBar里又通过注解获取要用到的View,然后在onInit方法里通过WindowManager把View添加到窗口上
  • 显示几个按钮是配置里决定的,具体看mode的说明
  • 具体的按钮的添加逻辑都是在NavigationBarView,NavigationBarinflaterView里,至于按钮的显示大小啥的可以看getDefaultLayout里的配置string