需求
基于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;
从状态栏入口开始看。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
|
protected void makeStatusBarView() { final Context context = mContext; updateDisplaySize(); // populates mDisplayMetrics updateResources(); updateTheme(); ... ... try { boolean showNav = mWindowManagerService.hasNavigationBar(); if (DEBUG) Log.v(TAG, "hasNavigationBar=" + showNav); if (showNav) { createNavigationBar(); //创建导航栏 } } catch (RemoteException ex) { } } |
2.进入 createNavigationBar 方法,发现主要是用 NavigationBarFragment 来管理.
1
2
3
4
5
6
7
8
9
|
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)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
|
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里,不能忽略)
1
2
3
4
5
6
7
8
|
<com.android.systemui.statusbar.phone.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: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。
1
2
3
4
5
|
@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 都要仔细研读。
1
2
3
4
5
6
7
8
9
10
11
12
|
<com.android.systemui.statusbar.phone.NavigationBarView xmlns: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.NavigationBarInflaterView android: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布局首先走的是初始化
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
|
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之后都会回调。
1
2
3
4
5
6
7
|
@Override protected void onFinishInflate() { super .onFinishInflate(); inflateChildren(); //进去看无关紧要 忽略 clearViews(); //进去看无关紧要 忽略 inflateLayout(getDefaultLayout()); //关键方法:加载了 back.home.recent三个按钮的layout } |
看inflateLayout():里面的newLayout参数很重要!!!根据上一个方法看到getDefaultLayout(),他return了一个在xml写死的字符串。再看inflateLayout方法,他解析分割了xml里配置的字符串,并传给了inflateButtons方法
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
|
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); //包含home String[] 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
1
2
|
<!-- Nav bar button default ordering/layout --> <string name= "config_navBarLayout" translatable= "false" >left[.5W],back[1WC];home;recent[1WC],right[.5W]</string> |
再看inflateButtons()方法,遍历加载inflateButton:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
|
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); //创建view if (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布局
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
|
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,按键的响声等,这里忽略。
至此,导航栏按键事件我们梳理完毕。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
|
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类里,找到构造方法。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
|
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就可知道,即反推看调用流程。
1
2
3
4
|
private void updateRecentsIcon() { getRecentsButton().setImageDrawable(mDockedStackExists ? mDockedIcon : mRecentIcon); mBarTransitions.reapplyDarkIntensity(); } |
updateRecentsIcon这个方法设置了recent图片的资源,再看谁调用了updateRecentsIcon方法:onConfigurationChanged屏幕旋转会重新设置资源图片
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
|
@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()方法:
1
2
3
4
5
6
|
public void reorient() { updateCurrentView(); ... setNavigationIconHints(mNavigationIconHints, true ); getHomeButton().setVertical(mVertical); } |
再朝上推,最终追溯到NavigationBarFragment的onConfigurationChanged()方法 和 NavigationBarView的onAttachedToWindow()和onSizeChanged()方法。也就是说,在NavigationBarView导航栏这个布局加载的时候就会设置图片资源,和长度改变,屏幕旋转都有可能引起重新设置
至此,SystemUI的虚拟导航栏模块代码流程结束。
总结
- 创建一个window属性的父view
- 通过读取解析xml里config的配置,addView需要的icon,或者调换顺序
- src图片资源通过代码设置亮色和暗色
- touch事件以keycode方式交由系统处理
以上所述是小编给大家介绍的Android 8.1平台SystemUI 导航栏加载流程,希望对大家有所帮助,如果大家有任何疑问请给我留言,小编会及时回复大家的。在此也非常感谢大家对服务器之家网站的支持!
如果你觉得本文对你有帮助,欢迎转载,烦请注明出处,谢谢!
原文链接:https://blog.csdn.net/u012932409/article/details/89498082