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 系統隱藏和顯示虛擬按鍵的幾種方法
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章