android 虚拟按键源码流程分析

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;
        }
    }
    
到此,虚拟按键的显示就介绍的这里。下一篇将介绍几种动态显示虚拟按键的方法。android 系统隐藏和显示虚拟按键的几种方法
  • 2
    点赞
  • 12
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

假装多好123

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值