今天来说说android 的虚拟按键的源码流程。大家都知道,android 系统的状态栏,虚拟按键,下拉菜单,以及通知显示,keyguard 锁屏都是在framework 下的SystemUI中的。
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"));
}
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 添加的。
这里有三个重要的类,一个是上面提到的NavigationBarFragment,另外就是NavigationBarView和NavigationBarInflaterView
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);
}
// left[.5W],back[1WC];home;recent[1WC],right[.5W]
// 接下里就是对从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;
}
}