android 虚拟按键流程分析


今天来说说android 的虚拟按键的源码流程。大家都知道,android 系统的状态栏,虚拟按键,下拉菜单,以及通知显示,keyguard 锁屏都是在framework 下的SystemUI中的。

1. 要说起虚拟按键,首先得说下虚拟按键的开关
frameworks\base\services\core\java\com\android\server\policy\PhoneWindowManager.java@Overridepublic 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#rangemForceDefaultOrientation = 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.javaprotected 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.javapublic 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 switcherfinal 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.KeyButtonViewxmlns: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.KeyButtonViewxmlns: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;}
    }
    
到此,虚拟按键的显示就介绍的这里。下一篇将介绍几种动态显示虚拟按键的方法。android 系统隐藏和显示虚拟按键的几种方法

android 虚拟按键源码流程分析相关推荐

  1. Android 9.0系统恢复出场设置源码流程分析

    前言 作为Framework层的开发人员,如果我们想让系统恢复出厂设置,一般有一下三种方式: 1.在[系统设置页面]进入[恢复出厂设置页面],点击[恢复出厂设置]按钮. 2.直接通过adb发送恢复出厂 ...

  2. 技术宝典 | WebRTC ADM 源码流程分析

    导读: 本文主要基于 WebRTC release-72 源码及云信音视频团队积累的相关经验而成,主要分析以下问题: ADM(Audio Device Manager)的架构如何?ADM(Audio ...

  3. WebRTC ADM 源码流程分析

    导读: 本文主要基于 WebRTC release-72 源码及云信音视频团队积累的相关经验而成,主要分析以下问题: ADM(Audio Device Manager)的架构如何?ADM(Audio ...

  4. AQS 源码流程分析

    导读: 我们日常开发中,经常会碰到并发的场景,在 Java 中语言体系里,我们会想到 ReentrantLock.CountDownLatch.Semaphore 等工具,但你是否清楚它们内部的实现原 ...

  5. springcloud ribbon @LoadBalance负载均衡源码流程分析

    一.编写示例 1.服务端 pom.xml <properties><java.version>1.8</java.version><spring-cloud. ...

  6. Fabric源码流程分析之Orderer篇

    导言: 本文使用fabric1.1版本,此时有小朋友会问了,fabric都出1.4.2了你怎么还在看1.1呢!首先fabric自1.0以后大的架构基本没有变化,小版本升级只是功能性上更加丰满了,当然最 ...

  7. eureka源码流程分析

    这是euraka官网的架构图 从上面图中可以看到eureka的功能 服务注册 服务续约 服务同步 服务下线 远程调用 一.服务注册 这个服务提供者需需要把自己的实例注册到注册中心中(就相当于相亲时把自 ...

  8. MyBatis源码流程分析

    mybatis核心流程三大阶段 Mybatis的初始化  建造者模式 建造者模式(Builder Pattern)使用多个简单的对象一步一步构建成一个复杂的对象.这种类型的设计模式属于创建型模式,它提 ...

  9. 【Android 插件化】Hook 插件化框架 ( 从源码角度分析加载资源流程 | Hook 点选择 | 资源冲突解决方案 )

    Android 插件化系列文章目录 [Android 插件化]插件化简介 ( 组件化与插件化 ) [Android 插件化]插件化原理 ( JVM 内存数据 | 类加载流程 ) [Android 插件 ...

最新文章

  1. FPGA逻辑设计回顾(8)单比特信号的CDC处理方式之Toggle同步器
  2. 5分钟了解 Python 中的super函数是如何实现继承的
  3. 宅福利-宅家抗疫,你我同在2020-01-30
  4. php sql 中文编码,php sql如何设置编码
  5. 前端工程师如何与设计师合作能提高效率
  6. quartus||仿真图
  7. sns.load_dataset报错解决
  8. 常用软件写网页html,新手用什么软件写html网页比较靠谱
  9. 计算机网络国家标准,计算机网络教室建设标准本标准参考国家标准GB50174并结合金州.doc...
  10. atikmpag.sys 导致蓝屏
  11. 华为RH2288 V3安装 linux 龙蜥anolis系统安装
  12. 新疆大盘鸡的标准做法
  13. matlab元胞数组的创建和显示
  14. 用python表白代码_python浪漫表白源码
  15. 重新认识java(十一)---- java中的数组
  16. 正易原文(木版本影印)
  17. 怎样快速学会ZBrush 中的移动笔刷的运用
  18. 黑客入侵“在线影院”全过程1
  19. steam创建账号一直验证人工操作_steam手机版官网版-steam手机版下载
  20. 利用梯度下降法实现线性回归--python实现

热门文章

  1. 如何在ChemDraw中输入℃温度符号
  2. LATEX之对文章排版的相关设置
  3. exponential backoff algorithm
  4. android m是什么版本号,Android M版本号确定,并不是Android 6.0
  5. 新手小白怎么学抖音运营?抖音运营5大技巧
  6. Oracle数据库:排序order by语句,select from where order by的执行先后顺序,各种样例
  7. c# 数字转大写中文
  8. 区块链要去中心化么?
  9. 怎样批量解析手机端头条视频和封面图片并保存
  10. linux startx无效_startx命令_Linux startx 命令用法详解:用来启动X Window