一步步淺析Android系統導航欄(NavigationBar)
Android手機可分為有導航欄以及沒導航欄兩種,一般有物理按鍵的機器不會帶有導航欄,而沒有物理按鍵的機器則基本會帶,比如華為的手機基本都是帶導航欄的。
導航欄是如何載入到桌面上?是如何實現與物理按鍵相同的功能的呢?帶著種種疑問,我們來read the fucking source code。
導航欄是屬於系統介面的一部分,也就是SystemUI的一部分。在SystemUI中導航欄實質上是一個繼承LinearLayout的ViewGroup:NavigationBarView,在系統介面初始化的時候在PhoneStatusBar.java的makeStatusBarView方法中通過以下程式碼初始化中:
try {
boolean showNav = mWindowManagerService.hasNavigationBar();
if (DEBUG) Log.v(TAG, "hasNavigationBar=" + showNav);
if (showNav) {
/// M: add for multi window @{
int layoutId = R.layout.navigation_bar;
if (MultiWindowProxy.isSupported()) {
layoutId = R.layout.navigation_bar_float_window;
}
mNavigationBarView = (NavigationBarView) View.inflate(context,
/*R.layout.navigation_bar*/layoutId, null);
/// @}
mNavigationBarView.setDisabledFlags(mDisabled1);
mNavigationBarView.setBar(this );
mNavigationBarView.setOnVerticalChangedListener(
new NavigationBarView.OnVerticalChangedListener() {
@Override
public void onVerticalChanged(boolean isVertical) {
if (mAssistManager != null) {
mAssistManager.onConfigurationChanged();
}
mNotificationPanel.setQsScrimEnabled(!isVertical);
}
});
mNavigationBarView.setOnTouchListener(new View.OnTouchListener() {
@Override
public boolean onTouch(View v, MotionEvent event) {
checkUserAutohide(v, event);
return false;
}});
}
} catch (RemoteException ex) {
// no window manager? good luck with that
}
首先在第2行通過mWindowManagerService.hasNavigationBar();
方法判斷是否應該載入導航欄,如果應該載入,則在第10行呼叫View.inflate()方法將佈局加載出來,並賦值給NavigationBarView的引用mNavigationBarView,然後對mNavigationBarView進行各種初始化操作。
NavigationBarView的佈局檔案比較長,我就只貼一部分:
......
<View
android:layout_width="@dimen/navigation_side_padding"
android:layout_height="match_parent"
android:layout_weight="0"
android:visibility="invisible"
/>
<com.android.systemui.statusbar.policy.KeyButtonView android:id="@+id/back"
android:layout_width="@dimen/navigation_key_width"
android:layout_height="match_parent"
android:src="@drawable/ic_sysbar_back"
systemui:keyCode="4"
android:layout_weight="0"
android:scaleType="center"
android:contentDescription="@string/accessibility_back"
/>
<View
android:layout_width="0dp"
android:layout_height="match_parent"
android:layout_weight="1"
android:visibility="invisible"
/>
<com.android.systemui.statusbar.policy.KeyButtonView android:id="@+id/home"
android:layout_width="@dimen/navigation_key_width"
android:layout_height="match_parent"
android:src="@drawable/ic_sysbar_home"
systemui:keyCode="3"
systemui:keyRepeat="false"
android:layout_weight="0"
android:scaleType="center"
android:contentDescription="@string/accessibility_home"
/>
<View
android:layout_width="0dp"
android:layout_height="match_parent"
android:layout_weight="1"
android:visibility="invisible"
/>
<com.android.systemui.statusbar.policy.KeyButtonView android:id="@+id/recent_apps"
android:layout_width="@dimen/navigation_key_width"
android:layout_height="match_parent"
android:src="@drawable/ic_sysbar_recent"
android:layout_weight="0"
android:scaleType="center"
android:contentDescription="@string/accessibility_recent"
/>
......
我們主要關注在第10、25和41行中id分別為back、home、和recent_apps的3個KeyButtonView。如大家所料,這3個View分別對應著返回鍵,home鍵以及最近活動鍵(不是MENU鍵)。至於其它View的主要作用是為了佈局的整齊,讓三個按鍵平分導航欄的空間。
接下來我們看下KeyButtonView是個什麼東東:
public class KeyButtonView extends ImageView {
......
}
可見KeyButtonView是繼承自ImageView的。接下來我們去了解下它是如何處理點選事件的:
public boolean onTouchEvent(MotionEvent ev) {
final int action = ev.getAction();
int x, y;
if (action == MotionEvent.ACTION_DOWN) {
mGestureAborted = false;
}
if (mGestureAborted) {
return false;
}
switch (action) {
case MotionEvent.ACTION_DOWN:
mDownTime = SystemClock.uptimeMillis();
setPressed(true);
if (mCode != 0) {
sendEvent(KeyEvent.ACTION_DOWN, 0, mDownTime);
} else {
// Provide the same haptic feedback that the system offers for virtual keys.
performHapticFeedback(HapticFeedbackConstants.VIRTUAL_KEY);
}
removeCallbacks(mCheckLongPress);
postDelayed(mCheckLongPress, ViewConfiguration.getLongPressTimeout());
break;
case MotionEvent.ACTION_MOVE:
x = (int)ev.getX();
y = (int)ev.getY();
setPressed(x >= -mTouchSlop
&& x < getWidth() + mTouchSlop
&& y >= -mTouchSlop
&& y < getHeight() + mTouchSlop);
break;
case MotionEvent.ACTION_CANCEL:
setPressed(false);
if (mCode != 0) {
sendEvent(KeyEvent.ACTION_UP, KeyEvent.FLAG_CANCELED);
}
removeCallbacks(mCheckLongPress);
break;
case MotionEvent.ACTION_UP:
final boolean doIt = isPressed();
setPressed(false);
if (mCode != 0) {
if (doIt) {
sendEvent(KeyEvent.ACTION_UP, 0);
sendAccessibilityEvent(AccessibilityEvent.TYPE_VIEW_CLICKED);
playSoundEffect(SoundEffectConstants.CLICK);
} else {
sendEvent(KeyEvent.ACTION_UP, KeyEvent.FLAG_CANCELED);
}
} else {
// no key code, just a regular ImageView
if (doIt) {
performClick();
}
}
removeCallbacks(mCheckLongPress);
break;
}
return true;
}
在ACTION_DOWN、ACTION_CANCEL以及ACTION_UP中都呼叫了一個很重要的方法sendEvent():
public void sendEvent(int action, int flags) {
sendEvent(action, flags, SystemClock.uptimeMillis());
}
void sendEvent(int action, int flags, long when) {
final int repeatCount = (flags & KeyEvent.FLAG_LONG_PRESS) != 0 ? 1 : 0;
final KeyEvent ev = new KeyEvent(mDownTime, when, action, mCode, repeatCount,
0, KeyCharacterMap.VIRTUAL_KEYBOARD, 0,
flags | KeyEvent.FLAG_FROM_SYSTEM | KeyEvent.FLAG_VIRTUAL_HARD_KEY,
InputDevice.SOURCE_KEYBOARD);
InputManager.getInstance().injectInputEvent(ev,
InputManager.INJECT_INPUT_EVENT_MODE_ASYNC);
}
KeyButtonView就是在sendEvent方法中通過構建出一個對應的keyCode的KeyEvent ev,然後呼叫InputManager的injectInputEvent模擬傳送來實現與物理按鍵相同的功能。
其中第7行中KeyEvent的構建引數中的mCode的定義語句是這樣的:
mCode = a.getInteger(R.styleable.KeyButtonView_keyCode, 0);
在上面貼出來的導航欄的佈局檔案中id為home以及id為back的KeyButtonView就分別通過systemui:keyCode屬性對其進行了設定,設定的值分別是3和4。然後檢視KeyEvent的KeyCode值:
/** Key code constant: Home key.
* This key is handled by the framework and is never delivered to applications. */
public static final int KEYCODE_HOME = 3;
/** Key code constant: Back key. */
public static final int KEYCODE_BACK = 4;
可以看到,id為home的KeyButtonView的mCode正好就是對應著物理鍵Home的KeyCode,id為back的KeyButtonView的mCode正好就是對應著物理鍵Back的KeyCode。基於以上的所有操作,導航欄的back和home就與物理按鍵中的back和home對應了起來。
這時候有人會問:還有一個按鍵呢?id為recent_apps的KeyButtonView又是對應著哪個按鍵呢?通過檢視佈局檔案,我們會發現recent_apps並沒有定義systemui:keyCode這個值,也就是說mCode會是一個預設值:0。檢視onTouchEvent可知,當mCode為0的時候,KeyButtonView並不會呼叫sendEvent方法。
也就是說點選id為recent_apps的KeyButtonView時的操作並不是通過模擬物理按鍵實現的,接下來我們將逐漸講到這一點。
在NavigationBar載入完成後,SystemUI會呼叫addNavigationBar方法,在這個方法裡先是呼叫prepareNavigationBarView方法中完成NavigationBarView的準備工作,比如給各個按鍵設定點選事件和點選效果。然後通過WindowManager的addView方法將NavigationBar載入到系統視窗中。
private void addNavigationBar() {
if (DEBUG) Log.v(TAG, "addNavigationBar: about to add " + mNavigationBarView);
if (mNavigationBarView == null) return;
prepareNavigationBarView();
mWindowManager.addView(mNavigationBarView, getNavigationBarLayoutParams());
......
private void prepareNavigationBarView() {
mNavigationBarView.reorient();
mNavigationBarView.getRecentsButton().setOnClickListener(mRecentsClickListener);
mNavigationBarView.getRecentsButton().setOnTouchListener(mRecentsPreloadOnTouchListener);
mNavigationBarView.getRecentsButton().setLongClickable(true);
mNavigationBarView.getRecentsButton().setOnLongClickListener(mLongPressBackRecentsListener);
mNavigationBarView.getBackButton().setLongClickable(true);
mNavigationBarView.getBackButton().setOnLongClickListener(mLongPressBackRecentsListener);
mNavigationBarView.getHomeButton().setOnTouchListener(mHomeActionListener);
mNavigationBarView.getHomeButton().setOnLongClickListener(mLongPressHomeListener);
mAssistManager.onConfigurationChanged();
/// M: add for multi window @{
if(MultiWindowProxy.isSupported()){
mNavigationBarView.getFloatButton().setOnClickListener(mFloatClickListener);
if(mIsSplitModeEnable){
mNavigationBarView.getFloatModeButton().setOnClickListener(mFloatModeClickListener);
mNavigationBarView.getSplitModeButton().setOnClickListener(mSplitModeClickListener);
}
MultiWindowProxy.getInstance().setSystemUiCallback(new MWSystemUiCallback());
}
/// @}
}
在第14行:prepareNavigationBarView方法中通過
mNavigationBarView.getRecentsButton().setOnClickListener(mRecentsClickListener);
給recent_apps這個KeyButtonView設定了監聽:
private View.OnClickListener mRecentsClickListener = new View.OnClickListener() {
public void onClick(View v) {
awakenDreams();
toggleRecentApps();
}
};
邏輯非常簡單清晰:當點選的時候就開啟RecentApp的Activity。
到此NavigationBar的基本載入以及按鍵實現就分析完畢了,歡迎提問或拍磚,謝謝。