当前位置: 首页 > article >正文

SystemUI中NavigationBar分析

需求

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组件如下:

// SystemUI\src\com\android\systemui\navigationbar\NavigationBarComponent.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();
}

// SystemUI\src\com\android\systemui\navigationbar\NavigationBarModule.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\res\layout\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>

3.NavigationBarView

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

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

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

// SystemUI\src\com\android\systemui\navigationbar\NavigationBarView.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。部分代码如下:

// SystemUI\src\com\android\systemui\navigationbar\NavigationBarInflaterView.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

// SystemUI\src\com\android\systemui\navigationbar\buttons\KeyButtonView.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

http://www.kler.cn/a/536812.html

相关文章:

  • LSSVM最小二乘支持向量机多变量多步光伏功率预测(Matlab)
  • 智能门铃市场:开启智能家居新时代
  • 【AI大模型】Ubuntu18.04安装deepseek-r1模型+服务器部署+内网访问
  • OpenBMC:通过qemu-system-arm运行编译好的image
  • 机器学习之数学基础:线性代数、微积分、概率论 | PyTorch 深度学习实战
  • 使用 Ollama 在 Windows 环境部署 DeepSeek 大模型实战指南
  • Vue笔记(二)
  • 通过代理模式理解Java注解的实现原理
  • (2024|CVPR,MLLM 幻觉)OPERA:通过过度信任惩罚和回顾分配缓解多模态大型语言模型中的幻觉
  • 【深度学习】关于模型训练的一些基本概念
  • openai库 及LangChain 跟ChatGPT对话的主要接口
  • java虚拟机JVM简单介绍(可用于面试)
  • 微信小程序longpress以及touchend的bug,touchend不触发,touchend不执行
  • Java - 在Linux系统上使用OpenCV和Tesseract
  • Gitee AI上线:开启免费DeepSeek模型新时代
  • Nginx部署Umi React前端项目标准配置
  • 1.1 学习代理(Agent)分为几步?
  • 2025 CCF BDCI|“基于TPU平台的OCR模型性能优化”一等奖作品
  • Ubuntu 下 nginx-1.24.0 源码分析 - ngx_strerror 函数
  • 【LeetCode 刷题】贪心算法(4)-区间问题
  • Ubuntu Server 部署网页 (如果无域名 可局域网访问)
  • 【每天学点AI】实战仿射变换在人工智能图像处理中的应用
  • CTRL: 一种用于可控生成的条件Transformer语言模型
  • ROS2从入门到精通3-2:详解xacro语法并优化封装urdf
  • 如何在Docker中运行MySQL容器?
  • 电脑右下角小喇叭没反应怎么回事,快速解决方案