需求
基於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 mDisplayMetrics
updateResources();
updateTheme();
...
...
try {
boolean showNav = mWindowManagerService.hasNavigationBar();
if (DEBUG) Log.v(TAG, "hasNavigationBar=" + showNav);
if (showNav) {
createNavigationBar();//建立導航欄
}
} catch (RemoteException ex) {
}
}
複製程式碼
2.SystemUI\src\com\android\systemui\statusbar\phone\StatusBar.java;
進入createNavigationBar方法,發現主要是用NavigationBarFragment來管理.
protected void createNavigationBar() {
mNavigationBarView = NavigationBarFragment.create(mContext, (tag, fragment) -> {
mNavigationBar = (NavigationBarFragment) fragment;
if (mLightBarController != null) {
mNavigationBar.setLightBarController(mLightBarController);
}
mNavigationBar.setCurrentSysuiVisibility(mSystemUiVisibility);
});
}
複製程式碼
3.SystemUI\src\com\android\systemui\statusbar\phone\NavigationBarFragment.java;
看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.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。
@Override
public View onCreateView(LayoutInflater inflater, @Nullable ViewGroup container,
Bundle savedInstanceState) {
return inflater.inflate(R.layout.navigation_bar, container, false);
}
複製程式碼
7.SystemUI\res\layout\navigation_bar.xml;
進入導航欄的真正根佈局:navigation_bar.xml,好吧又是自定義view,NavigationBarView和NavigationBarInflaterView都要仔細研讀。
<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>
複製程式碼
8.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);//包含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
<!-- 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);//建立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佈局
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"
/>
複製程式碼
9.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);
}
複製程式碼
10.還遺留一個問題:設定圖片的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);
...
...
}
複製程式碼
11.從第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的虛擬導航欄模組程式碼流程結束。
總結
- 建立一個window屬性的父view
- 通過讀取解析xml裡config的配置,addView需要的icon,或者調換順序
- src圖片資源通過程式碼設定亮色和暗色
- touch事件以keycode方式交由系統處理
下面一節介紹音量加減的定製步驟。