Android 8.1平台SystemUI 导航栏加载流程解析

2024-08-22 06:38

本文主要是介绍Android 8.1平台SystemUI 导航栏加载流程解析,希望对大家解决编程问题提供一定的参考价值,需要的开发者们随着小编来一起学习吧!

需求

基于MTK8163 8.1平台定制导航栏部分,在左边增加音量减,右边增加音量加

思路

需求开始做之前,一定要研读SystemUI Navigation模块的代码流程!!!不要直接去网上copy别人改的需求代码,盲改的话很容易出现问题,然而无从解决。网上有老平台(8.0-)的讲解System UI的导航栏模块的博客,自行搜索。8.0对System UI还是做了不少细节上的改动,代码改动体现上也比较多,但是总体基本流程并没变。

源码阅读可以沿着一条线索去跟代码,不要过分在乎代码细节!例如我客制化这个需求,可以跟着导航栏的返回(back),桌面(home),最近任务(recent)中的一个功能跟代码流程,大体知道比如recen这个view是哪个方法调哪个方法最终加载出来,加载的关键代码在哪,点击事件怎么生成,而不在意里面的具体逻辑判断等等。

代码流程

1.SystemUI\src\com\android\systemui\statusbar\phone\StatusBar.java;

从状态栏入口开始看。

protected void makeStatusBarView() {final Context context = mContext;updateDisplaySize(); // populates mDisplayMetricsupdateResources();updateTheme();......try {boolean showNav = mWindowManagerService.hasNavigationBar();if (DEBUG) Log.v(TAG, "hasNavigationBar=" + showNav);if (showNav) {createNavigationBar();//创建导航栏}} catch (RemoteException ex) {}
}

2.进入 createNavigationBar 方法,发现主要是用 NavigationBarFragment 来管理.

protected void createNavigationBar() {mNavigationBarView = NavigationBarFragment.create(mContext, (tag, fragment) -> {mNavigationBar = (NavigationBarFragment) fragment;if (mLightBarController != null) {mNavigationBar.setLightBarController(mLightBarController);}mNavigationBar.setCurrentSysuiVisibility(mSystemUiVisibility);});
}

3.看 NavigationBarFragment 的create方法,终于知道,是WindowManager去addView了导航栏的布局,最终add了fragment的onCreateView加载的布局。(其实SystemUI所有的模块都是WindowManager来加载View)

public static View create(Context context, FragmentListener listener) {WindowManager.LayoutParams lp = new WindowManager.LayoutParams(LayoutParams.MATCH_PARENT, LayoutParams.MATCH_PARENT,WindowManager.LayoutParams.TYPE_NAVIGATION_BAR,WindowManager.LayoutParams.FLAG_TOUCHABLE_WHEN_WAKING| WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE| WindowManager.LayoutParams.FLAG_NOT_TOUCH_MODAL| WindowManager.LayoutParams.FLAG_WATCH_OUTSIDE_TOUCH| WindowManager.LayoutParams.FLAG_SPLIT_TOUCH| WindowManager.LayoutParams.FLAG_SLIPPERY,PixelFormat.TRANSLUCENT);lp.token = new Binder();lp.setTitle("NavigationBar");lp.windowAnimations = 0;View navigationBarView = LayoutInflater.from(context).inflate(R.layout.navigation_bar_window, null);if (DEBUG) Log.v(TAG, "addNavigationBar: about to add " + navigationBarView);if (navigationBarView == null) return null;context.getSystemService(WindowManager.class).addView(navigationBarView, lp);FragmentHostManager fragmentHost = FragmentHostManager.get(navigationBarView);NavigationBarFragment fragment = new NavigationBarFragment();fragmentHost.getFragmentManager().beginTransaction().replace(R.id.navigation_bar_frame, fragment, TAG) //注意!fragment里onCreateView加载的布局是add到这个Window属性的view里的。.commit();fragmentHost.addTagListener(TAG, listener);return navigationBarView;}
}

4.SystemUI\res\layout\navigation_bar_window.xml

来看WindowManager加载的这个view的布局:navigation_bar_window.xml,发现根布局是自定义的view类NavigationBarFrame.(其实SystemUI以及其他系统应用如Launcher,都是这种自定义view的方式,好多逻辑处理也都是在自定义view里,不能忽略)

<com.android.systemui.statusbar.phone.NavigationBarFramexmlns:android="http://schemas.android.com/apk/res/android"xmlns:systemui="http://schemas.android.com/apk/res-auto"android:id="@+id/navigation_bar_frame"android:layout_height="match_parent"android:layout_width="match_parent">	</com.android.systemui.statusbar.phone.NavigationBarFrame>

5.SystemUI\src\com\android\systemui\statusbar\phone\NavigationBarFrame.java

我们进入NavigationBarFrame类。发现类里并不是我们的预期,就是一个FrameLayout,对DeadZone功能下的touch事件做了手脚,不管了。

6.再回来看看NavigationBarFragment的生命周期呢。onCreateView()里,导航栏的真正的rootView。

@Override
public View onCreateView(LayoutInflater inflater, @Nullable ViewGroup container,Bundle savedInstanceState) {return inflater.inflate(R.layout.navigation_bar, container, false);
}

进入导航栏的真正根布局:navigation_bar.xml,好吧又是自定义view,NavigationBarView 和 NavigationBarInflaterView 都要仔细研读。

<com.android.systemui.statusbar.phone.NavigationBarViewxmlns:android="http://schemas.android.com/apk/res/android"xmlns:systemui="http://schemas.android.com/apk/res-auto"android:layout_height="match_parent"android:layout_width="match_parent"android:background="@drawable/system_bar_background"><com.android.systemui.statusbar.phone.NavigationBarInflaterViewandroid:id="@+id/navigation_inflater"android:layout_width="match_parent"android:layout_height="match_parent" /></com.android.systemui.statusbar.phone.NavigationBarView>

7.SystemUI\src\com\android\systemui\statusbar\phone\NavigationBarInflaterView.java;继承自FrameLayout

先看构造方法,因为加载xml布局首先走的是初始化

public NavigationBarInflaterView(Context context, AttributeSet attrs) {super(context, attrs);createInflaters();//根据屏幕旋转角度创建子view(单个back home or recent)的父布局Display display = ((WindowManager)context.getSystemService(Context.WINDOW_SERVICE)).getDefaultDisplay();Mode displayMode = display.getMode();isRot0Landscape = displayMode.getPhysicalWidth() > displayMode.getPhysicalHeight();
}
private void inflateChildren() {removeAllViews();mRot0 = (FrameLayout) mLayoutInflater.inflate(R.layout.navigation_layout, this, false);mRot0.setId(R.id.rot0);addView(mRot0);mRot90 = (FrameLayout) mLayoutInflater.inflate(R.layout.navigation_layout_rot90, this, false);mRot90.setId(R.id.rot90);addView(mRot90);updateAlternativeOrder();
}

再看onFinishInflate()方法,这是view的生命周期,每个view被inflate之后都会回调。

@Override
protected void onFinishInflate() {super.onFinishInflate();inflateChildren();//进去看无关紧要 忽略clearViews();//进去看无关紧要 忽略inflateLayout(getDefaultLayout());//关键方法:加载了 back.home.recent三个按钮的layout
}

看inflateLayout():里面的newLayout参数很重要!!!根据上一个方法看到getDefaultLayout(),他return了一个在xml写死的字符串。再看inflateLayout方法,他解析分割了xml里配置的字符串,并传给了inflateButtons方法

protected void inflateLayout(String newLayout) {mCurrentLayout = newLayout;if (newLayout == null) {newLayout = getDefaultLayout();}String[] sets = newLayout.split(GRAVITY_SEPARATOR, 3);//根据“;”号分割成长度为3的数组String[] start = sets[0].split(BUTTON_SEPARATOR);//根据“,”号分割,包含 left[.5W]和back[1WC]String[] center = sets[1].split(BUTTON_SEPARATOR);//包含homeString[] end = sets[2].split(BUTTON_SEPARATOR);//包含recent[1WC]和right[.5W]// Inflate these in start to end order or accessibility traversal will be messed up.inflateButtons(start, mRot0.findViewById(R.id.ends_group), isRot0Landscape, true);inflateButtons(start, mRot90.findViewById(R.id.ends_group), !isRot0Landscape, true);inflateButtons(center, mRot0.findViewById(R.id.center_group), isRot0Landscape, false);inflateButtons(center, mRot90.findViewById(R.id.center_group), !isRot0Landscape, false);addGravitySpacer(mRot0.findViewById(R.id.ends_group));addGravitySpacer(mRot90.findViewById(R.id.ends_group));inflateButtons(end, mRot0.findViewById(R.id.ends_group), isRot0Landscape, false);inflateButtons(end, mRot90.findViewById(R.id.ends_group), !isRot0Landscape, false);
}protected String getDefaultLayout() {return mContext.getString(R.string.config_navBarLayout);
}

SystemUI\res\values\config.xml

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

再看inflateButtons()方法,遍历加载inflateButton:

private void inflateButtons(String[] buttons, ViewGroup parent, boolean landscape,boolean start) {for (int i = 0; i < buttons.length; i++) {inflateButton(buttons[i], parent, landscape, start);}
}@Nullable
protected View inflateButton(String buttonSpec, ViewGroup parent, boolean landscape,boolean start) {LayoutInflater inflater = landscape ? mLandscapeInflater : mLayoutInflater;View v = createView(buttonSpec, parent, inflater);//创建viewif (v == null) return null;v = applySize(v, buttonSpec, landscape, start);parent.addView(v);//addView到父布局addToDispatchers(v);View lastView = landscape ? mLastLandscape : mLastPortrait;View accessibilityView = v;if (v instanceof ReverseFrameLayout) {accessibilityView = ((ReverseFrameLayout) v).getChildAt(0);}if (lastView != null) {accessibilityView.setAccessibilityTraversalAfter(lastView.getId());}if (landscape) {mLastLandscape = accessibilityView;} else {mLastPortrait = accessibilityView;}return v;
}

我们来看createView()方法:以home按键为例,加载了home的button,其实是加载了 R.layout.home 的layout布局

private View createView(String buttonSpec, ViewGroup parent, LayoutInflater inflater) {View v = null;......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.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);} ......return v;
}//SystemUI\res\layout\home.xml 
//这里布局里没有src显示home的icon,肯定是在代码里设置了
//这里也是自定义view:KeyButtonView
<com.android.systemui.statusbar.policy.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"//引用了dimens.xml里的navigation_key_width
android:layout_height="match_parent"
android:layout_weight="0"
systemui:keyCode="3"//systemui自定义的属性
android:scaleType="fitCenter"
android:contentDescription="@string/accessibility_home"
android:paddingTop="@dimen/home_padding"
android:paddingBottom="@dimen/home_padding"
android:paddingStart="@dimen/navigation_key_padding"
android:paddingEnd="@dimen/navigation_key_padding"/>

8.SystemUI\src\com\android\systemui\statusbar\policy\KeyButtonView.java

先来看KeyButtonView的构造方法:我们之前xml的systemui:keyCode=”3”方法在这里获取。再来看Touch事件,通过sendEvent()方法可以看出,back等view的点击touch事件不是自己处理的,而是交由系统以实体按键(keycode)的形式处理的.

当然KeyButtonView类还处理了支持长按的button,按键的响声等,这里忽略。

至此,导航栏按键事件我们梳理完毕。

public KeyButtonView(Context context, AttributeSet attrs, int defStyle) {super(context, attrs);TypedArray a = context.obtainStyledAttributes(attrs, R.styleable.KeyButtonView,defStyle, 0);mCode = a.getInteger(R.styleable.KeyButtonView_keyCode, 0);mSupportsLongpress = a.getBoolean(R.styleable.KeyButtonView_keyRepeat, true);mPlaySounds = a.getBoolean(R.styleable.KeyButtonView_playSound, true);TypedValue value = new TypedValue();if (a.getValue(R.styleable.KeyButtonView_android_contentDescription, value)) {mContentDescriptionRes = value.resourceId;}a.recycle();setClickable(true);mTouchSlop = ViewConfiguration.get(context).getScaledTouchSlop();mAudioManager = (AudioManager) context.getSystemService(Context.AUDIO_SERVICE);mRipple = new KeyButtonRipple(context, this);setBackground(mRipple);
}...
...public boolean onTouchEvent(MotionEvent ev) {...switch (action) {case MotionEvent.ACTION_DOWN:mDownTime = SystemClock.uptimeMillis();mLongClicked = false;setPressed(true);if (mCode != 0) {sendEvent(KeyEvent.ACTION_DOWN, 0, mDownTime);//关键方法} else {// Provide the same haptic feedback that the system offers for virtual keys.performHapticFeedback(HapticFeedbackConstants.VIRTUAL_KEY);}playSoundEffect(SoundEffectConstants.CLICK);removeCallbacks(mCheckLongPress);postDelayed(mCheckLongPress, ViewConfiguration.getLongPressTimeout());break;......}return true;
}void sendEvent(int action, int flags, long when) {mMetricsLogger.write(new LogMaker(MetricsEvent.ACTION_NAV_BUTTON_EVENT).setType(MetricsEvent.TYPE_ACTION).setSubtype(mCode).addTaggedData(MetricsEvent.FIELD_NAV_ACTION, action).addTaggedData(MetricsEvent.FIELD_FLAGS, flags));final int repeatCount = (flags & KeyEvent.FLAG_LONG_PRESS) != 0 ? 1 : 0;//这里根据mCode new了一个KeyEvent事件,通过injectInputEvent使事件生效。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);InputManager.getInstance().injectInputEvent(ev,InputManager.INJECT_INPUT_EVENT_MODE_ASYNC);
}

9.还遗留一个问题:设置图片的icon到底在哪?我们之前一直阅读的是NavigationBarInflaterView,根据布局我们还有一个类没有看,NavigationBarView.java

SystemUI\src\com\android\systemui\statusbar\phone\NavigationBarView.java

进入NavigationBarView类里,找到构造方法。

public NavigationBarView(Context context, AttributeSet attrs) {super(context, attrs);mDisplay = ((WindowManager) context.getSystemService(Context.WINDOW_SERVICE)).getDefaultDisplay();......updateIcons(context, Configuration.EMPTY, mConfiguration);//关键方法mBarTransitions = new NavigationBarTransitions(this);//mButtonDispatchers 是维护这些home back recent图标view的管理类,会传递到他的child,NavigationBarInflaterView类中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));mButtonDispatchers.put(R.id.menu, new ButtonDispatcher(R.id.menu));mButtonDispatchers.put(R.id.ime_switcher, new ButtonDispatcher(R.id.ime_switcher));mButtonDispatchers.put(R.id.accessibility_button,new ButtonDispatcher(R.id.accessibility_button));}private void updateIcons(Context ctx, Configuration oldConfig, Configuration newConfig) {...iconLight = mNavBarPlugin.getHomeImage(ctx.getDrawable(R.drawable.ic_sysbar_home));iconDark = mNavBarPlugin.getHomeImage(ctx.getDrawable(R.drawable.ic_sysbar_home_dark));//mHomeDefaultIcon = getDrawable(ctx,//        R.drawable.ic_sysbar_home, R.drawable.ic_sysbar_home_dark);mHomeDefaultIcon = getDrawable(iconLight,iconDark);//亮色的icon资源iconLight = mNavBarPlugin.getRecentImage(ctx.getDrawable(R.drawable.ic_sysbar_recent));//暗色的icon资源iconDark = mNavBarPlugin.getRecentImage(ctx.getDrawable(R.drawable.ic_sysbar_recent_dark));//mRecentIcon = getDrawable(ctx,//        R.drawable.ic_sysbar_recent, R.drawable.ic_sysbar_recent_dark);mRecentIcon = getDrawable(iconLight,iconDark);mMenuIcon = getDrawable(ctx, R.drawable.ic_sysbar_menu,R.drawable.ic_sysbar_menu_dark);......}

10.从第10可以看到,以recent为例,在初始化时得到了mRecentIcon的资源,再看谁调用了了mRecentIcon就可知道,即反推看调用流程。

private void updateRecentsIcon() {getRecentsButton().setImageDrawable(mDockedStackExists ? mDockedIcon : mRecentIcon);mBarTransitions.reapplyDarkIntensity();
}

updateRecentsIcon这个方法设置了recent图片的资源,再看谁调用了updateRecentsIcon方法:onConfigurationChanged屏幕旋转会重新设置资源图片

@Override
protected void onConfigurationChanged(Configuration newConfig) {super.onConfigurationChanged(newConfig);boolean uiCarModeChanged = updateCarMode(newConfig);updateTaskSwitchHelper();updateIcons(getContext(), mConfiguration, newConfig);updateRecentsIcon();if (uiCarModeChanged || mConfiguration.densityDpi != newConfig.densityDpi|| mConfiguration.getLayoutDirection() != newConfig.getLayoutDirection()) {// If car mode or density changes, we need to reset the icons.setNavigationIconHints(mNavigationIconHints, true);}mConfiguration.updateFrom(newConfig);
}public void setNavigationIconHints(int hints, boolean force) {......mNavigationIconHints = hints;// 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.KeyButtonDrawable backIcon = (backAlt)? getBackIconWithAlt(mUseCarModeUi, mVertical): getBackIcon(mUseCarModeUi, mVertical);getBackButton().setImageDrawable(backIcon);updateRecentsIcon();......}

reorient()也调用了setNavigationIconHints()方法:

public void reorient() {updateCurrentView();...setNavigationIconHints(mNavigationIconHints, true);getHomeButton().setVertical(mVertical);
}

再朝上推,最终追溯到NavigationBarFragment的onConfigurationChanged()方法 和 NavigationBarView的onAttachedToWindow()和onSizeChanged()方法。也就是说,在NavigationBarView导航栏这个布局加载的时候就会设置图片资源,和长度改变,屏幕旋转都有可能引起重新设置

至此,SystemUI的虚拟导航栏模块代码流程结束。

总结

  1. 创建一个window属性的父view
  2. 通过读取解析xml里config的配置,addView需要的icon,或者调换顺序
  3. src图片资源通过代码设置亮色和暗色
  4. touch事件以keycode方式交由系统处理

文章转自 https://blog.csdn.net/danxinzhicheng/article/details/80019902

这篇关于Android 8.1平台SystemUI 导航栏加载流程解析的文章就介绍到这儿,希望我们推荐的文章对编程师们有所帮助!



http://www.chinasem.cn/article/1095519

相关文章

线上Java OOM问题定位与解决方案超详细解析

《线上JavaOOM问题定位与解决方案超详细解析》OOM是JVM抛出的错误,表示内存分配失败,:本文主要介绍线上JavaOOM问题定位与解决方案的相关资料,文中通过代码介绍的非常详细,需要的朋... 目录一、OOM问题核心认知1.1 OOM定义与技术定位1.2 OOM常见类型及技术特征二、OOM问题定位工具

通过Docker容器部署Python环境的全流程

《通过Docker容器部署Python环境的全流程》在现代化开发流程中,Docker因其轻量化、环境隔离和跨平台一致性的特性,已成为部署Python应用的标准工具,本文将详细演示如何通过Docker容... 目录引言一、docker与python的协同优势二、核心步骤详解三、进阶配置技巧四、生产环境最佳实践

MyBatis分页查询实战案例完整流程

《MyBatis分页查询实战案例完整流程》MyBatis是一个强大的Java持久层框架,支持自定义SQL和高级映射,本案例以员工工资信息管理为例,详细讲解如何在IDEA中使用MyBatis结合Page... 目录1. MyBATis框架简介2. 分页查询原理与应用场景2.1 分页查询的基本原理2.1.1 分

深度解析Python中递归下降解析器的原理与实现

《深度解析Python中递归下降解析器的原理与实现》在编译器设计、配置文件处理和数据转换领域,递归下降解析器是最常用且最直观的解析技术,本文将详细介绍递归下降解析器的原理与实现,感兴趣的小伙伴可以跟随... 目录引言:解析器的核心价值一、递归下降解析器基础1.1 核心概念解析1.2 基本架构二、简单算术表达

深度解析Java @Serial 注解及常见错误案例

《深度解析Java@Serial注解及常见错误案例》Java14引入@Serial注解,用于编译时校验序列化成员,替代传统方式解决运行时错误,适用于Serializable类的方法/字段,需注意签... 目录Java @Serial 注解深度解析1. 注解本质2. 核心作用(1) 主要用途(2) 适用位置3

Java MCP 的鉴权深度解析

《JavaMCP的鉴权深度解析》文章介绍JavaMCP鉴权的实现方式,指出客户端可通过queryString、header或env传递鉴权信息,服务器端支持工具单独鉴权、过滤器集中鉴权及启动时鉴权... 目录一、MCP Client 侧(负责传递,比较简单)(1)常见的 mcpServers json 配置

redis-sentinel基础概念及部署流程

《redis-sentinel基础概念及部署流程》RedisSentinel是Redis的高可用解决方案,通过监控主从节点、自动故障转移、通知机制及配置提供,实现集群故障恢复与服务持续可用,核心组件包... 目录一. 引言二. 核心功能三. 核心组件四. 故障转移流程五. 服务部署六. sentinel部署

从原理到实战解析Java Stream 的并行流性能优化

《从原理到实战解析JavaStream的并行流性能优化》本文给大家介绍JavaStream的并行流性能优化:从原理到实战的全攻略,本文通过实例代码给大家介绍的非常详细,对大家的学习或工作具有一定的... 目录一、并行流的核心原理与适用场景二、性能优化的核心策略1. 合理设置并行度:打破默认阈值2. 避免装箱

Maven中生命周期深度解析与实战指南

《Maven中生命周期深度解析与实战指南》这篇文章主要为大家详细介绍了Maven生命周期实战指南,包含核心概念、阶段详解、SpringBoot特化场景及企业级实践建议,希望对大家有一定的帮助... 目录一、Maven 生命周期哲学二、default生命周期核心阶段详解(高频使用)三、clean生命周期核心阶

SpringBoot集成XXL-JOB实现任务管理全流程

《SpringBoot集成XXL-JOB实现任务管理全流程》XXL-JOB是一款轻量级分布式任务调度平台,功能丰富、界面简洁、易于扩展,本文介绍如何通过SpringBoot项目,使用RestTempl... 目录一、前言二、项目结构简述三、Maven 依赖四、Controller 代码详解五、Service