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; } }