android 虚拟按键源码流程分析
16lz
2021-01-23
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); } //
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; }}
到此,虚拟按键的显示就介绍的这里。下一篇将介绍几种动态显示虚拟按键的方法。android 系统隐藏和显示虚拟按键的几种方法
更多相关文章
- adb通过wifi连接android设备的方法
- 在 android 上运行 python 的方法
- Android开发基础-系统结构
- 饭后Android 第一餐-NavigationView+Toolbar(NavigationView使用
- 知识储备:Android系统架构
- 初识Android系统平台
- Android jni 常用方法备忘