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的基本加载以及按键实现就分析完毕了