android framework13-status bar[03NavigationBar]

866 阅读9分钟

导航栏就是屏幕下方或者两侧几个按键,以前是物理按键,现在都虚拟的。常见的就是后退键,home键,recents键。 当然了,如果有输入法的显示的话,可能会有个箭头按钮用来隐藏输入法。现在还支持手势操作,就是滑屏关闭页面,这时候导航栏可能就显示一个按钮线条。

补充说明

目前文章都是以手机模式来研究代码的,这里说明一下,如果是平板的话,那么导航按钮是在launcher3工程里,有个quickstep目录,里边有个taskbar目录下就是相关的导航功能了。

NavigationBarController.java 下边代码可以看到,只有手机模式才会创建navigationBar。

    private boolean updateNavbarForTaskbar() {
        boolean taskbarShown = initializeTaskbarIfNecessary();
        if (!taskbarShown && mNavigationBars.get(mContext.getDisplayId()) == null) {
        //非平板模式走这里
            createNavigationBar(mContext.getDisplay(), null, null);
        }
        return taskbarShown;
    }

    /** 平板模式这里返回true, */
    private boolean initializeTaskbarIfNecessary() {
        // Enable for tablet or (phone AND flag is set); assuming phone = !mIsTablet
        boolean taskbarEnabled = mIsTablet || mFeatureFlags.isEnabled(Flags.HIDE_NAVBAR_WINDOW);

        if (taskbarEnabled) {
        //平板模式,移除手机模式的navBar
            removeNavigationBar(mContext.getDisplayId());
            mTaskbarDelegate.init(mContext.getDisplayId());
        } else {
            mTaskbarDelegate.destroy();
        }
        return taskbarEnabled;
    }

1.创建

创建还是在CentralSurfacesImpl.java里边

    // TODO(b/117478341): This was left such that CarStatusBar can override this method.
    // Try to remove this.
    protected void createNavigationBar(@Nullable RegisterStatusBarResult result) {
        mNavigationBarController.createNavigationBars(true /* includeDefaultDisplay */, result);
    }

可以看到是通过Controller来创建的,那就具体看下。

2.NavigationBarController

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

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

        // Don't need to create nav bar on the default display if we initialize TaskBar.
        final boolean shouldCreateDefaultNavbar = includeDefaultDisplay
                && !initializeTaskbarIfNecessary();
        Display[] displays = mDisplayManager.getDisplays();
        for (Display display : displays) {
            if (shouldCreateDefaultNavbar || display.getDisplayId() != DEFAULT_DISPLAY) {
                createNavigationBar(display, null /* savedState */, result);
            }
        }
    }
        void createNavigationBar(Display display, Bundle savedState, RegisterStatusBarResult result) {
//...
            final int displayId = display.getDisplayId();
            final boolean isOnDefaultDisplay = displayId == DEFAULT_DISPLAY;

            // We may show TaskBar on the default display for large screen device. Don't need to create
            // navigation bar for this case.
            if (isOnDefaultDisplay && initializeTaskbarIfNecessary()) {
                return;
            }

            final IWindowManager wms = WindowManagerGlobal.getWindowManagerService();

            try {
                if (!wms.hasNavigationBar(displayId)) {
                    return;
                }
            } catch (RemoteException e) {
                // Cannot get wms, just return with warning message.
                Log.w(TAG, "Cannot get WindowManager.");
                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.1NavigationBar实例化

>构造方法

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

注解生成的,如下,

>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();

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

相关的两个布局如下

>>navigation_bar_window.xml

<com.android.systemui.navigationbar.NavigationBarFrame
    xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:systemui="http://schemas.android.com/apk/res-auto"
    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>

>>navigation_bar.xml

<com.android.systemui.navigationbar.NavigationBarView
    xmlns:android="http://schemas.android.com/apk/res/android"
    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.2init方法

init方法是父类ViewController里的

@NavigationBarScope
public class NavigationBar extends ViewController<NavigationBarView> implements Callbacks {

ViewController.java调用了onInit方法,给view添加attach,detach监听.

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

        protected ViewController(T view) {
            mView = view;
        }
            public void init() {
                if (mInited) {
                    return;
                }
                onInit();
                mInited = true;

                if (isAttachedToWindow()) {
                    mOnAttachStateListener.onViewAttachedToWindow(mView);
                }
                addOnAttachStateChangeListener(mOnAttachStateListener);
            }

2.3onInit()

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

>getBarLayoutParams

        private WindowManager.LayoutParams getBarLayoutParams(int rotation) {
            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;
        }

>getBarLayoutParamsForRotation

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

说明:

  • config_navBarCanMove: 查了下values-sw600dp下是false,values下是true,所以对于手机来说应该是true了
  • android 13里横屏的时候导航键是在左右显示的,不像以前在底部
  • 导航栏里边是加载了两种线性布局的,一种是横向的,一种是竖的,根据旋转角度显示不同的布局

3.NavigationBarView

public class NavigationBarView extends FrameLayout {

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

        updateOrientationViews();
        reloadNavIcons();
    }
    

4.NavigationBarInflaterView

public class NavigationBarInflaterView extends FrameLayout
        implements NavigationModeController.ModeChangedListener {
        
    protected void onFinishInflate() {
        super.onFinishInflate();
        inflateChildren();//手动添加children容器控件
        clearViews();
        inflateLayout(getDefaultLayout());//手动加载按钮
    }
//添加了 横屏和竖屏两种布局,根据实际情况显示
    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();
    }        

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

分号隔开左中右,然后逗号隔开,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>

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

4.2.navigation_layout.xml

里边是2个horizontal的线性布局,显示在底部的导航栏

<FrameLayout
    xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:systemui="http://schemas.android.com/apk/res-auto"
    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>

4.3.navigation_layout_vertical.xml

里边是两个vertical的线性布局,显示在两侧的导航栏,横屏的时候

<FrameLayout
    xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:systemui="http://schemas.android.com/apk/res-auto"
    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.4.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) {
              Log.d(TAG, "Invalid layout.");
              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();
      }
    

>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里

>createView

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

    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布局

>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"
    />

>home.xml

<com.android.systemui.navigationbar.buttons.KeyButtonView
    xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:systemui="http://schemas.android.com/apk/res-auto"
    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"
    />

>home_handle.xml

<com.android.systemui.navigationbar.gestural.NavigationHandle
    xmlns:android="http://schemas.android.com/apk/res/android"
    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"
    />

>recent_apps.xml

<com.android.systemui.navigationbar.buttons.KeyButtonView
    xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:systemui="http://schemas.android.com/apk/res-auto"
    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);
    //...
}

>onTouchEvent

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

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

>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.按钮的点击事件

>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:
            // First we always handle the home key here, so applications
            // can never break it, although if keyguard is on, we do let
            // it handle it, because that gives us the correct 5 second
            // timeout.
            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();
        }
    }

>adb shell

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

adb shell input keyevent 4

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

>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();
                    //我开始以为交给它处理了,后来发现不是这个。
                    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

>获取mode

    public int addListener(ModeChangedListener listener) {
        mListeners.add(listener);
        return getCurrentInteractionMode(mCurrentUserContext);
    }
        private int getCurrentInteractionMode(Context context) {
            int mode = context.getResources().getInteger(
                    com.android.internal.R.integer.config_navBarInteractionMode);
            return mode;
        }

>mode

model有如下三种,可以看到可以有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