您现在的位置是:首页 >其他 >SystemUI中NavigationBar分析网站首页其他

SystemUI中NavigationBar分析

dongpingwang 2025-02-14 12:01:03
简介navigationbarview

需求

SystemUI是一个与系统组件显示紧密相关的应用,包含快捷中心、消息通知、状态栏、导航栏、任务中心等诸多模块,本文介绍NavigationBar模块。SystemUI源码位于/frameworks/base/packages/SystemUI,Android13平台。NavigationBar显示如下:

关键类

  • NavigationBarComponent.java:NavigationBar组件类,采用Dagger进行依赖注入
  • NavigationBar.java:将导航栏view添加到window
  • navigation_bar.xml:NavigationBar布局文件
  • NavigationBarView.java:设置导航栏图标
  • NavigationBarInflaterView:解析config中导航栏排布信息,创建对应的view
  • home.xml/back.xml:导航栏按钮对应的布局
  • KeyButtonView.java:导航栏图标的View,如果设置了keycode,则将点击事件touch以keycode方式交由系统处理

代码流程

1. NavigationBar模块启动

Android13平台的SystemUI代码较旧平台变化比较大,各个组件采用了Dagger进行依赖注入(DI)。在SystemUIApplication启动的时候进行了组件的初始化,NavigationBar组件如下:

// SystemUIsrccomandroidsystemui
avigationbarNavigationBarComponent.java
@Subcomponent(modules = { NavigationBarModule.class })
@NavigationBarComponent.NavigationBarScope
public interface NavigationBarComponent {
    @Subcomponent.Factory
    interface Factory {
        NavigationBarComponent create(
                @BindsInstance @DisplayId Context context,
                @BindsInstance @Nullable Bundle savedState);
    }
    NavigationBar getNavigationBar();
}

// SystemUIsrccomandroidsystemui
avigationbarNavigationBarModule.java
@Module
public interface NavigationBarModule {
    @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);
    }
}

从上面可以看到navigation_bar是布局文件,NavigationBarView是具体的view,NavigationBar中实现导航栏view添加到window。

2.布局文件navigation_bar.xml

NavigationBarView和NavigationBarInflaterView实际上都是Framelayout

// SystemUI
eslayout
avigation_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>

3.NavigationBarView

我们接着看NavigationBarView,主要做了下面几件事情:

  • 在构造方法中创建了返回、主页等ButtonDispatcher。
  • 布局加载完成时,找到了子view(NavigationInflaterView),并将ButtonDispatcher设置给了NavigationInflaterView
  • onAttachedToWindow()时,将对应的图标设置给返回、主页等view

我们发现NavigationBarView中并没有创建返回、主页等对应的view,将返回、主页等对应的view添加到ViewGroup的操作在NavigationInflaterView中

// SystemUIsrccomandroidsystemui
avigationbarNavigationBarView.java
// 创建ButtonDispatcher
public NavigationBarView(Context context, AttributeSet attrs) {
        mButtonDispatchers.put(R.id.back, new ButtonDispatcher(R.id.back));
        mButtonDispatchers.put(R.id.home, new ButtonDispatcher(R.id.home));
        mButtonDispatchers.put(R.id.recent_apps, new ButtonDispatcher(R.id.recent_apps));
}
// 布局加载完成
public void onFinishInflate() {
    super.onFinishInflate();
    mNavigationInflaterView = findViewById(R.id.navigation_inflater);
    mNavigationInflaterView.setButtonDispatchers(mButtonDispatchers);
    reloadNavIcons();// reloadNavIcons()中调用了updateIcons()
}
// 获取图标
private void updateIcons(Configuration oldConfig) {
    final boolean orientationChange = oldConfig.orientation != mConfiguration.orientation;
    final boolean densityChange = oldConfig.densityDpi != mConfiguration.densityDpi;
    final boolean dirChange = oldConfig.getLayoutDirection() != mConfiguration.getLayoutDirectin();
    // 获取返回按钮、主页、按钮图标drawable
    if (orientationChange || densityChange) {
        mDockedIcon = getDrawable(R.drawable.ic_sysbar_docked);
        mHomeDefaultIcon = getHomeDrawable();
    }
    if (densityChange || dirChange) {
        mRecentIcon = getDrawable(R.drawable.ic_sysbar_recent);
        mContextualButtonGroup.updateIcons(mLightIconColor, mDarkIconColor);
    }
    if (orientationChange || densityChange || dirChange) {
        mBackIcon = getBackDrawable();
    }
}

// 返回按钮图标,KeyButtonDrawable实际上是一个Drawable
public KeyButtonDrawable getBackDrawable() {
    KeyButtonDrawable drawable = getDrawable(getBackDrawableRes());
    orientBackButton(drawable);
    return drawable;
}

// 设置图标
protected void onAttachedToWindow() {
    super.onAttachedToWindow();
    requestApplyInsets();
    reorient();
    updateNavButtonIcons();
}

4.NavigationBarInflaterView

NavigationBarInflaterView是真正创建返回、主页按钮view的地方,先解析config中设置config_navBarLayout排列信息,然后通过对应layout创建KeyButtonView。部分代码如下:

// SystemUIsrccomandroidsystemui
avigationbarNavigationBarInflaterView.java
// 布局加载完成
protected void onFinishInflate() {
    super.onFinishInflate();
    inflateChildren(); // 加载布局
    clearViews();// 清空传递过来的ButtonDispatcher中保存的view
    inflateLayout(getDefaultLayout()); // 关键点:加载布局,创建view
}

// getDefaultLayout()是获取按钮排布信息,从config.xml中获取,如:<string name="config_navBarLayout" translatable="false">left[.5W],back[1WC];home;recent[1WC],right[.5W]</string>
// 解析newLayout创建view
protected void inflateLayout(String 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(
                    com.android.internal.R.id.input_method_nav_ends_group),
            false /* landscape */, true /* start */);
    inflateButtons(center, mHorizontal.findViewById(
                    com.android.internal.R.id.input_method_nav_center_group),
            false /* landscape */, false /* start */);
    addGravitySpacer(mHorizontal.findViewById(
            com.android.internal.R.id.input_method_nav_ends_group));
    inflateButtons(end, mHorizontal.findViewById(
                    com.android.internal.R.id.input_method_nav_ends_group),
            false /* landscape */, false /* start */);
    updateButtonDispatchersCurrentView();
}

// 创建view并添加到viewgroup
protected View inflateButton(String buttonSpec, ViewGroup parent, boolean landscape,
            boolean start) {
    LayoutInflater inflater = landscape ? mLandscapeInflater : mLayoutInflater;
    View v = createView(buttonSpec, parent, inflater); // 关键点:创建view
    if (v == null) return null;
    v = applySize(v, buttonSpec, landscape, start);
    parent.addView(v);
    addToDispatchers(v);
    View lastView = landscape ? mLastLandscape : mLastPortrait;
    View accessibilityView = v;
    if (v instanceof ReverseRelativeLayout) {
        accessibilityView = ((ReverseRelativeLayout) v).getChildAt(0);
    }
    if (lastView != null) {
        accessibilityView.setAccessibilityTraversalAfter(lastView.getId());
    }
    if (landscape) {
        mLastLandscape = accessibilityView;
    } else {
        mLastPortrait = accessibilityView;
    }
    return v;
}

// 通过对应的布局创建view,实际上创建的是KeyButtonView
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);
    }
    return v;
}

5.KeyButtonView

如上一步back按钮的布局文件如下。

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

KeyButtonView是一个ImageView,重写了onTouchEvent,设置了keyCode,则点击后给系统发送对应的keyevent

// SystemUIsrccomandroidsystemui
avigationbaruttonsKeyButtonView.java
public boolean onTouchEvent(MotionEvent ev) {
 ...
 switch (action) {
     case MotionEvent.ACTION_DOWN:
         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);
         }
     }
}
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;

    if (getDisplay() != null) {
        displayId = getDisplay().getDisplayId();
    }
    if (displayId != INVALID_DISPLAY) {
        ev.setDisplayId(displayId);
    }
    mInputManager.injectInputEvent(ev, InputManager.INJECT_INPUT_EVENT_MODE_ASYNC);
}   

总结

  • 将导航栏View添加到Window进行显示
  • 通过读取解析xml里config的图标排布信息,来创建对应的view
  • 如果设置了keycode,则将点击事件touch以keycode方式交由系统处理

参考

  • Dagger/Hilt依赖注入使用:https://developer.android.com/training/dependency-injection?hl=zh-cn
  • 解析Android 8.1平台SystemUI 导航栏加载流程:https://www.jb51.net/article/174313.htm
风语者!平时喜欢研究各种技术,目前在从事后端开发工作,热爱生活、热爱工作。