android 虛擬按鍵流程分析
今天來說說android 的虛擬按鍵的源碼流程。大家都知道,android 系統的狀態欄,虛擬按鍵,下拉菜單,以及通知顯示,keyguard 鎖屏都是在framework 下的SystemUI中的。
1. 要說起虛擬按鍵,首先得說下虛擬按鍵的開關
frameworks\base\services\core\java\com\android\server\policy\PhoneWindowManager.java
@Override
public void setInitialDisplaySize(Display display, int width, int height, int density) {
...
// Allow the navigation bar to move on non-square small devices (phones).
mNavigationBarCanMove = width != height && shortSizeDp < 600;
mHasNavigationBar = res.getBoolean(com.android.internal.R.bool.config_showNavigationBar);
//這裏 mHasNavigationBar 變量決定android 系統是否有虛擬按鍵,想要android 系統默認顯示或者關閉虛擬按鍵,則可以在framework 下的config 文件中將config_showNavigationBar置爲true或者false
// Allow a system property to override this. Used by the emulator.
// See also hasNavigationBar().
String navBarOverride = SystemProperties.get("qemu.hw.mainkeys");
if ("1".equals(navBarOverride)) {
mHasNavigationBar = false;
} else if ("0".equals(navBarOverride)) {
mHasNavigationBar = true;
}
//這裏谷歌又給了一個開關,即 "qemu.hw,mainkeys"的值,一般來說,系統中是不對這個值處理的。這個是谷歌預留的,在有需求的情況下,可以使用這個開關是設置prop,動態的顯示或者隱藏虛擬按鍵
// For demo purposes, allow the rotation of the HDMI display to be controlled.
// By default, HDMI locks rotation to landscape.
if ("portrait".equals(SystemProperties.get("persist.demo.hdmirotation"))) {
mDemoHdmiRotation = mPortraitRotation;
} else {
mDemoHdmiRotation = mLandscapeRotation;
}
mDemoHdmiRotationLock = SystemProperties.getBoolean("persist.demo.hdmirotationlock", false);
// For demo purposes, allow the rotation of the remote display to be controlled.
// By default, remote display locks rotation to landscape.
if ("portrait".equals(SystemProperties.get("persist.demo.remoterotation"))) {
mDemoRotation = mPortraitRotation;
} else {
mDemoRotation = mLandscapeRotation;
}
mDemoRotationLock = SystemProperties.getBoolean(
"persist.demo.rotationlock", false);
// Only force the default orientation if the screen is xlarge, at least 960dp x 720dp, per
// http://developer.android.com/guide/practices/screens_support.html#range
mForceDefaultOrientation = longSizeDp >= 960 && shortSizeDp >= 720 &&
res.getBoolean(com.android.internal.R.bool.config_forceDefaultOrientation) &&
// For debug purposes the next line turns this feature off with:
// $ adb shell setprop config.override_forced_orient true
// $ adb shell wm size reset
!"true".equals(SystemProperties.get("config.override_forced_orient"));
}
2. SystemUI 中虛擬按鍵的創建
SystemUI\app\src\main\java\com\android\systemui\statusbar\phone\StatusBar.java
protected void makeStatusBarView() {
...
try {
boolean showNav = mWindowManagerService.hasNavigationBar(); //獲取上面虛擬按鍵的開關
if (DEBUG) Log.v(TAG, "hasNavigationBar=" + showNav);
if (showNav) {
createNavigationBar();// 創建虛擬按鍵
}
} catch (RemoteException ex) {
// no window manager? good luck with that
}
...
}
protected void createNavigationBar() {
mNavigationBarView = NavigationBarFragment.create(mContext, (tag, fragment) -> {
mNavigationBar = (NavigationBarFragment) fragment;
if (mLightBarController != null) {
mNavigationBar.setLightBarController(mLightBarController);
}
mNavigationBar.setCurrentSysuiVisibility(mSystemUiVisibility);
});
}
SystemUI\app\src\main\java\com\android\systemui\statusbar\phone\NavigationBarFragment.java
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)
.commit();
fragmentHost.addTagListener(TAG, listener);
return navigationBarView;
}
這裏可以看到,其實虛擬按鍵的view 是一個window,是通過addView 添加的。
3. 接下來說說虛擬按鍵view的創建和顯示
這裏有三個重要的類,一個是上面提到的NavigationBarFragment,另外就是NavigationBarView和NavigationBarInflaterView
- 現在來看看NavigationBarView ,這個類主要是將虛擬按鍵的幾個圖標和view關聯起來
public class NavigationBarView extends FrameLayout implements PluginListener<NavGesture> {
這個類主要是將各個虛擬按鍵的button加入ButtonDispatcher中,另外這裏要說一句,我們只知道一般虛擬按鍵只有三個(back,home,recent),其實看了下面,其實不止三個,其餘幾個都是隱藏的。另外,每一個虛擬按鍵的view都是一個layout。
public NavigationBarView(Context context, AttributeSet attrs) {
super(context, attrs);
mDisplay = ((WindowManager) context.getSystemService(
Context.WINDOW_SERVICE)).getDefaultDisplay();
mVertical = false;
mShowMenu = false;
mShowAccessibilityButton = false;
mLongClickableAccessibilityButton = false;
mConfiguration = new Configuration();
mConfiguration.updateFrom(context.getResources().getConfiguration());
updateIcons(context, Configuration.EMPTY, mConfiguration);
mBarTransitions = new NavigationBarTransitions(this);
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));
}
//這個方法主要是將虛擬按鍵的圖標和view,bind起來
private void updateIcons(Context ctx, Configuration oldConfig, Configuration newConfig) {
if (oldConfig.orientation != newConfig.orientation
|| oldConfig.densityDpi != newConfig.densityDpi) {
mDockedIcon = getDrawable(ctx,
R.drawable.ic_sysbar_docked, R.drawable.ic_sysbar_docked_dark);
}
if (oldConfig.densityDpi != newConfig.densityDpi
|| oldConfig.getLayoutDirection() != newConfig.getLayoutDirection()) {
mBackIcon = getDrawable(ctx, R.drawable.ic_sysbar_back, R.drawable.ic_sysbar_back_dark);
mBackLandIcon = mBackIcon;
mBackAltIcon = getDrawable(ctx,
R.drawable.ic_sysbar_back_ime, R.drawable.ic_sysbar_back_ime_dark);
mBackAltLandIcon = mBackAltIcon;
mHomeDefaultIcon = getDrawable(ctx,
R.drawable.ic_sysbar_home, R.drawable.ic_sysbar_home_dark);
mRecentIcon = getDrawable(ctx,
R.drawable.ic_sysbar_recent, R.drawable.ic_sysbar_recent_dark);
mMenuIcon = getDrawable(ctx, R.drawable.ic_sysbar_menu, R.drawable.ic_sysbar_menu_dark);
mAccessibilityIcon = getDrawable(ctx, R.drawable.ic_sysbar_accessibility_button,
R.drawable.ic_sysbar_accessibility_button_dark);
int dualToneDarkTheme = Utils.getThemeAttr(ctx, R.attr.darkIconTheme);
int dualToneLightTheme = Utils.getThemeAttr(ctx, R.attr.lightIconTheme);
Context darkContext = new ContextThemeWrapper(ctx, dualToneDarkTheme);
Context lightContext = new ContextThemeWrapper(ctx, dualToneLightTheme);
mImeIcon = getDrawable(darkContext, lightContext,
R.drawable.ic_ime_switcher_default, R.drawable.ic_ime_switcher_default);
if (ALTERNATE_CAR_MODE_UI) {
updateCarModeIcons(ctx);
}
}
}
// 這個方法其實就是來隱藏其餘的虛擬按鍵的。
public void setNavigationIconHints(int hints, boolean force) {
if (!force && hints == mNavigationIconHints) return;
final boolean backAlt = (hints & StatusBarManager.NAVIGATION_HINT_BACK_ALT) != 0;
if ((mNavigationIconHints & StatusBarManager.NAVIGATION_HINT_BACK_ALT) != 0 && !backAlt) {
mTransitionListener.onBackAltCleared();
}
if (DEBUG) {
android.widget.Toast.makeText(getContext(),
"Navigation icon hints = " + hints,
500).show();
}
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();
if (mUseCarModeUi) {
getHomeButton().setImageDrawable(mHomeCarModeIcon);
} else {
getHomeButton().setImageDrawable(mHomeDefaultIcon);
}
// The Accessibility button always overrides the appearance of the IME switcher
final boolean showImeButton =
!mShowAccessibilityButton && ((hints & StatusBarManager.NAVIGATION_HINT_IME_SHOWN)
!= 0);
getImeSwitchButton().setVisibility(showImeButton ? View.VISIBLE : View.INVISIBLE);
getImeSwitchButton().setImageDrawable(mImeIcon);
// Update menu button in case the IME state has changed.
setMenuVisibility(mShowMenu, true);
getMenuButton().setImageDrawable(mMenuIcon);
setAccessibilityButtonState(mShowAccessibilityButton, mLongClickableAccessibilityButton);
getAccessibilityButton().setImageDrawable(mAccessibilityIcon);
setDisabledFlags(mDisabledFlags, true);
mBarTransitions.reapplyDarkIntensity();
}
}
說到這,不妨再來看看每一個虛擬按鍵的layout是怎麼寫的:
SystemUI\app\src\main\res\layout\back.xml
<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/back"
android:layout_width="@dimen/navigation_key_width"
android:layout_height="match_parent"
android:layout_weight="0"
systemui:keyCode="4"
android:scaleType="fitCenter"
android:contentDescription="@string/accessibility_back"
android:paddingTop="15dp"
android:paddingBottom="15dp"
android:paddingStart="@dimen/navigation_key_padding"
android:paddingEnd="@dimen/navigation_key_padding"
/>
SystemUI\app\src\main\res\layout\home.xml
<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"
android:layout_height="match_parent"
android:layout_weight="0"
systemui:keyCode="3"
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"
/>
從上面我們可以知道,每一個虛擬按鍵都是一個單獨的layout。細心的同學應該會注意到,這個裏面有一個重要的元素,就是 systemui:keyCode=“3”。從這裏我們大概可以知道了,虛擬按鍵的點擊實現,實際上是通過模擬發送keycode來實現的。
-
再來看看NavigationBarFragment 類
public class NavigationBarFragment extends Fragment implements Callbacks { // 這個方法就是設置每一個虛擬按鍵的點擊長按事件的監聽的 private void prepareNavigationBarView() { mNavigationBarView.reorient(); ButtonDispatcher recentsButton = mNavigationBarView.getRecentsButton(); recentsButton.setOnClickListener(this::onRecentsClick); recentsButton.setOnTouchListener(this::onRecentsTouch); recentsButton.setLongClickable(true); recentsButton.setOnLongClickListener(this::onLongPressBackRecents); ButtonDispatcher backButton = mNavigationBarView.getBackButton(); backButton.setLongClickable(true); backButton.setOnLongClickListener(this::onLongPressBackRecents); ButtonDispatcher homeButton = mNavigationBarView.getHomeButton(); homeButton.setOnTouchListener(this::onHomeTouch); homeButton.setOnLongClickListener(this::onHomeLongClick); ButtonDispatcher accessibilityButton = mNavigationBarView.getAccessibilityButton(); accessibilityButton.setOnClickListener(this::onAccessibilityClick); accessibilityButton.setOnLongClickListener(this::onAccessibilityLongClick); updateAccessibilityServicesState(mAccessibilityManager); } // recent按鍵點擊時會加載recentapp, private boolean onRecentsTouch(View v, MotionEvent event) { int action = event.getAction() & MotionEvent.ACTION_MASK; if (action == MotionEvent.ACTION_DOWN) { mCommandQueue.preloadRecentApps(); } else if (action == MotionEvent.ACTION_CANCEL) { mCommandQueue.cancelPreloadRecentApps(); } else if (action == MotionEvent.ACTION_UP) { if (!v.isPressed()) { mCommandQueue.cancelPreloadRecentApps(); } } return false; } // 點擊後顯示 private void onRecentsClick(View v) { if (LatencyTracker.isEnabled(getContext())) { LatencyTracker.getInstance(getContext()).onActionStart( LatencyTracker.ACTION_TOGGLE_RECENTS); } mStatusBar.awakenDreams(); mCommandQueue.toggleRecentApps(); } }
-
NavigationBarInflaterView
SystemUI\app\src\main\java\com\android\systemui\statusbar\phone\NavigationBarInflaterView.java //這個類主要是設置虛擬按鍵的位置顯示相關的 public class NavigationBarInflaterView extends FrameLayout // 這裏是判斷加載方向的 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(); } // 這個方法用來將getDefaultLayout虛擬按鍵的layout string進行分解操作 protected void inflateLayout(String newLayout) { mCurrentLayout = newLayout; if (newLayout == null) { newLayout = getDefaultLayout(); } String[] 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, 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); } // 以下方法可知,虛擬按鍵的順序是由這個string來解決的,如果需要客製化改虛擬按鍵的顯示順序,可以改變這裏 protected String getDefaultLayout() { return mContext.getString(R.string.config_navBarLayout); } // <string name="config_navBarLayout" translatable="false">left[.5W],back[1WC];home;recent[1WC],right[.5W]</string> // 接下里就是對從string裏面拆分出來的view進行一個個的加載創建 private View createView(String buttonSpec, ViewGroup parent, LayoutInflater inflater) { View v = null; String button = extractButton(buttonSpec); if (LEFT.equals(button)) { String s = Dependency.get(TunerService.class).getValue(NAV_BAR_LEFT, NAVSPACE); button = extractButton(s); } else if (RIGHT.equals(button)) { String s = Dependency.get(TunerService.class).getValue(NAV_BAR_RIGHT, MENU_IME); button = extractButton(s); } // Let plugins go first so they can override a standard view if they want. for (NavBarButtonProvider provider : mPlugins) { v = provider.createView(buttonSpec, parent); if (v != null) return v; } 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); } else if (button.startsWith(KEY)) { String uri = extractImage(button); int code = extractKeycode(button); v = inflater.inflate(R.layout.custom_key, parent, false); ((KeyButtonView) v).setCode(code); if (uri != null) { if (uri.contains(":")) { ((KeyButtonView) v).loadAsync(Icon.createWithContentUri(uri)); } else if (uri.contains("/")) { int index = uri.indexOf('/'); String pkg = uri.substring(0, index); int id = Integer.parseInt(uri.substring(index + 1)); ((KeyButtonView) v).loadAsync(Icon.createWithResource(pkg, id)); } } } return v; } }