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 系统隐藏和显示虚拟按键的几种方法

更多相关文章

  1. adb通过wifi连接android设备的方法
  2. 在 android 上运行 python 的方法
  3. Android开发基础-系统结构
  4. 饭后Android 第一餐-NavigationView+Toolbar(NavigationView使用
  5. 知识储备:Android系统架构
  6. 初识Android系统平台
  7. Android jni 常用方法备忘

随机推荐

  1. android调节系统音量
  2. Android(安卓)获取通讯录联系人,打开通讯
  3. android ListView内数据的动态添加与删除
  4. Android Studio 不显示logcat
  5. Android在子线程中更新UI(一)
  6. Android 设置圆角按下改变颜色按钮
  7. 锦囊篇|一文摸懂SharedPreferences和MMKV(
  8. Android实现定时器的方法
  9. Android 获取设备各种信息
  10. android中gridView中点击 item为选中状态