该系列文章总纲链接:专题分纲目录 Android SystemUI组件
本章关键点总结 & 说明:
说明:本章节持续迭代之前章节的思维导图,主要关注左侧SystemBars分析中导航栏部分即可。
1 导航栏创建之makeStatusBarView
通过上一篇文章的分析,我们知道 addNavigationBar是在addStatusBarWindow之后执行的,addStatusBarWindow代码实现如下:
private void addStatusBarWindow() {
makeStatusBarView();//创建statusbar视图
mStatusBarWindowManager = new StatusBarWindowManager(mContext);
mStatusBarWindowManager.add(mStatusBarWindow, getStatusBarHeight());
}
addStatusBarWindow中makeStatusBarView也执行了导航栏相关逻辑,相关代码如下:
protected PhoneStatusBarView makeStatusBarView() {
final Context context = mContext;
//...
try {
//关键点1:导航栏显示与否
boolean showNav = mWindowManagerService.hasNavigationBar();
//是否显示导航栏
if (showNav) {
//关键点2:加载导航栏布局
mNavigationBarView =
(NavigationBarView) View.inflate(context, R.layout.navigation_bar, null);
mNavigationBarView.setDisabledFlags(mDisabled);
mNavigationBarView.setBar(this);
mNavigationBarView.setOnVerticalChangedListener(
new NavigationBarView.OnVerticalChangedListener() {
@Override
public void onVerticalChanged(boolean isVertical) {
if (mSearchPanelView != null) {
mSearchPanelView.setHorizontal(isVertical);
}
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
}
//...
startGlyphRasterizeHack();
return mStatusBarView;
}
这里主要分析两个部分:navigationBar显示与否 和 导航栏Layout相关。
1.1 navigationBar显示与否
导航栏显示与否关键在于方法mWindowManagerService.hasNavigationBar的实现,这里最终是掉用到了PhoneWindowManager中的hasNavigationBar方法。代码如下所示:
public class PhoneWindowManager implements WindowManagerPolicy {
//...
public boolean hasNavigationBar() {
return mHasNavigationBar;
}
//...
}
这里变量mHasNavigationBar的赋值操作为:
mHasNavigationBar = res.getBoolean(com.android.internal.R.bool.config_showNavigationBar);
这里可以看到 导航栏的配置主要是在/res/res/values中名为config_showNavigationBar的标识,即为默认的配置,如下所示:
<!-- Whether a software navigation bar should be shown. NOTE: in the future this may be
autodetected from the Configuration. -->
<bool name="config_showNavigationBar">false</bool>
因此,这里可以根据需求修改该配置。
1.2 导航栏Layout相关
加载导航栏布局的语句为:
NavigationBarView = (NavigationBarView) View.inflate(context, R.layout.navigation_bar, null);
从布局文件(res\layout\navigation_bar.xml)中来来看,xml文件内容如下:
<com.android.systemui.statusbar.phone.NavigationBarView
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:systemui="http://schemas.android.com/apk/res-auto"
android:layout_height="match_parent"
android:layout_width="match_parent"
android:background="@drawable/system_bar_background"
>
<!--横向导航栏-->
<FrameLayout android:id="@+id/rot0"
android:layout_height="match_parent"
android:layout_width="match_parent"
>
<LinearLayout
android:layout_height="match_parent"
android:layout_width="match_parent"
android:orientation="horizontal"
android:clipChildren="false"
android:clipToPadding="false"
android:id="@+id/nav_buttons"
android:animateLayoutChanges="true"
>
<!-- navigation controls -->
<View
android:layout_width="@dimen/navigation_side_padding"
android:layout_height="match_parent"
android:layout_weight="0"
android:visibility="invisible"
/>
<!--back按钮-->
<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"
/>
<!--home按钮-->
<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"
/>
<!--recent按钮-->
<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"
/>
<FrameLayout
android:layout_width="@dimen/navigation_side_padding"
android:layout_height="match_parent"
android:layout_weight="0" >
<com.android.systemui.statusbar.policy.KeyButtonView
android:id="@+id/menu"
android:layout_width="@dimen/navigation_extra_key_width"
android:layout_height="match_parent"
android:contentDescription="@string/accessibility_menu"
android:src="@drawable/ic_sysbar_menu"
android:visibility="invisible"
android:scaleType="centerInside"
android:layout_gravity="end"
systemui:keyCode="82" />
<com.android.systemui.statusbar.policy.KeyButtonView
android:id="@+id/ime_switcher"
android:layout_width="@dimen/navigation_extra_key_width"
android:layout_height="match_parent"
android:contentDescription="@string/accessibility_ime_switch_button"
android:scaleType="centerInside"
android:src="@drawable/ic_ime_switcher_default"
android:visibility="invisible"
android:layout_gravity="end" />
</FrameLayout>
</LinearLayout>
<!-- lights out layout to match exactly -->
<LinearLayout
android:layout_height="match_parent"
android:layout_width="match_parent"
android:orientation="horizontal"
android:id="@+id/lights_out"
android:visibility="gone"
>
<ImageView
android:layout_width="@dimen/navigation_key_width"
android:layout_height="match_parent"
android:layout_marginStart="@dimen/navigation_side_padding"
android:src="@drawable/ic_sysbar_lights_out_dot_small"
android:scaleType="center"
android:layout_weight="0"
/>
<View
android:layout_width="match_parent"
android:layout_height="match_parent"
android:layout_weight="1"
android:visibility="invisible"
/>
<ImageView
android:layout_width="@dimen/navigation_key_width"
android:layout_height="match_parent"
android:src="@drawable/ic_sysbar_lights_out_dot_large"
android:scaleType="center"
android:layout_weight="0"
/>
<View
android:layout_width="match_parent"
android:layout_height="match_parent"
android:layout_weight="1"
android:visibility="invisible"
/>
<ImageView
android:layout_width="@dimen/navigation_key_width"
android:layout_marginEnd="@dimen/navigation_side_padding"
android:layout_height="match_parent"
android:src="@drawable/ic_sysbar_lights_out_dot_small"
android:scaleType="center"
android:layout_weight="0"
/>
</LinearLayout>
<com.android.systemui.statusbar.policy.DeadZone
android:id="@+id/deadzone"
android:layout_height="match_parent"
android:layout_width="match_parent"
systemui:minSize="@dimen/navigation_bar_deadzone_size"
systemui:maxSize="@dimen/navigation_bar_deadzone_size_max"
systemui:holdTime="@integer/navigation_bar_deadzone_hold"
systemui:decayTime="@integer/navigation_bar_deadzone_decay"
systemui:orientation="horizontal"
android:layout_gravity="top"
/>
</FrameLayout>
<!--纵向显示-->
<FrameLayout android:id="@+id/rot90"
android:layout_height="match_parent"
android:layout_width="match_parent"
android:visibility="gone"
android:paddingTop="0dp"
>
<LinearLayout
android:layout_height="match_parent"
android:layout_width="match_parent"
android:orientation="vertical"
android:clipChildren="false"
android:clipToPadding="false"
android:id="@+id/nav_buttons"
android:animateLayoutChanges="true"
>
<!-- navigation controls -->
<FrameLayout
android:layout_weight="0"
android:layout_width="match_parent"
android:layout_height="@dimen/navigation_side_padding" >
<com.android.systemui.statusbar.policy.KeyButtonView
android:id="@+id/ime_switcher"
android:layout_width="match_parent"
android:layout_height="@dimen/navigation_extra_key_width"
android:contentDescription="@string/accessibility_ime_switch_button"
android:scaleType="centerInside"
android:src="@drawable/ic_ime_switcher_default"
android:layout_gravity="top"
android:visibility="invisible" />
<com.android.systemui.statusbar.policy.KeyButtonView
android:id="@+id/menu"
android:layout_width="match_parent"
android:layout_height="40dp"
android:contentDescription="@string/accessibility_menu"
android:src="@drawable/ic_sysbar_menu_land"
android:scaleType="centerInside"
android:layout_gravity="top"
android:visibility="invisible"
systemui:keyCode="82" />
</FrameLayout>
<!--recent按钮-->
<com.android.systemui.statusbar.policy.KeyButtonView android:id="@+id/recent_apps"
android:layout_height="@dimen/navigation_key_width"
android:layout_width="match_parent"
android:src="@drawable/ic_sysbar_recent_land"
android:scaleType="center"
android:layout_weight="0"
android:contentDescription="@string/accessibility_recent"
/>
<View
android:layout_height="match_parent"
android:layout_width="match_parent"
android:layout_weight="1"
android:visibility="invisible"
/>
<!--home按钮-->
<com.android.systemui.statusbar.policy.KeyButtonView android:id="@+id/home"
android:layout_height="@dimen/navigation_key_width"
android:layout_width="match_parent"
android:src="@drawable/ic_sysbar_home_land"
android:scaleType="center"
systemui:keyCode="3"
systemui:keyRepeat="false"
android:layout_weight="0"
android:contentDescription="@string/accessibility_home"
/>
<View
android:layout_height="match_parent"
android:layout_width="match_parent"
android:layout_weight="1"
android:visibility="invisible"
/>
<!--back按钮-->
<com.android.systemui.statusbar.policy.KeyButtonView android:id="@+id/back"
android:layout_height="@dimen/navigation_key_width"
android:layout_width="match_parent"
android:src="@drawable/ic_sysbar_back_land"
android:scaleType="center"
systemui:keyCode="4"
android:layout_weight="0"
android:contentDescription="@string/accessibility_back"
/>
<View
android:layout_height="@dimen/navigation_side_padding"
android:layout_width="match_parent"
android:layout_weight="0"
android:visibility="invisible"
/>
</LinearLayout>
<!-- lights out layout to match exactly -->
<LinearLayout
android:layout_height="match_parent"
android:layout_width="match_parent"
android:orientation="vertical"
android:id="@+id/lights_out"
android:visibility="gone"
>
<ImageView
android:layout_height="@dimen/navigation_key_width"
android:layout_marginTop="@dimen/navigation_side_padding"
android:layout_width="match_parent"
android:src="@drawable/ic_sysbar_lights_out_dot_small"
android:scaleType="center"
android:layout_weight="0"
/>
<View
android:layout_height="match_parent"
android:layout_width="match_parent"
android:layout_weight="1"
android:visibility="invisible"
/>
<ImageView
android:layout_height="@dimen/navigation_key_width"
android:layout_width="match_parent"
android:src="@drawable/ic_sysbar_lights_out_dot_large"
android:scaleType="center"
android:layout_weight="0"
/>
<View
android:layout_height="match_parent"
android:layout_width="match_parent"
android:layout_weight="1"
android:visibility="invisible"
/>
<ImageView
android:layout_height="@dimen/navigation_key_width"
android:layout_marginBottom="@dimen/navigation_side_padding"
android:layout_width="match_parent"
android:src="@drawable/ic_sysbar_lights_out_dot_small"
android:scaleType="center"
android:layout_weight="0"
/>
</LinearLayout>
<com.android.systemui.statusbar.policy.DeadZone
android:id="@+id/deadzone"
android:layout_height="match_parent"
android:layout_width="match_parent"
systemui:minSize="@dimen/navigation_bar_deadzone_size"
systemui:maxSize="@dimen/navigation_bar_deadzone_size_max"
systemui:holdTime="@integer/navigation_bar_deadzone_hold"
systemui:decayTime="@integer/navigation_bar_deadzone_decay"
systemui:orientation="vertical"
android:layout_gravity="top"
/>
</FrameLayout>
</com.android.systemui.statusbar.phone.NavigationBarView>
可以看到,横向和纵向是加载两个不同的FrameLayout配置文件。由于文件过长,这里使用简图来描述,如下:
简单解读说明如下:
- nav_buttons:4个控件,back,home,recent,menu。
- lights_out:多数情况不可见,当处于低辨识度模式下,nav_buttons隐藏且lights_out显示,显示为三个不明显的小灰点,降低对用户视线的干扰。
- search_light:多数情况不可见,当HOME按键被禁后serach功能还可用,此时会变成可见,用于提示用户该功能可使用。
- deadzone:防止边界误触操作。
导航栏显示以及布局由屏幕的方向来决定,而导航栏有两种不同的显示方式,横向显示和竖向显示,同时 我从mRotatedViews变量 分析:
public class NavigationBarView extends LinearLayout {
//...
View[] mRotatedViews = new View[4];
//...
//布局加载完成后,会回调onFinishInflate方法
@Override
public void onFinishInflate() {
//屏幕方位0和180方向显示的导航栏为rot0,90和270显示的导航栏为rot90
mRotatedViews[Surface.ROTATION_0] =
mRotatedViews[Surface.ROTATION_180] = findViewById(R.id.rot0);
mRotatedViews[Surface.ROTATION_90] = findViewById(R.id.rot90);
mRotatedViews[Surface.ROTATION_270] = mRotatedViews[Surface.ROTATION_90];
mCurrentView = mRotatedViews[Surface.ROTATION_0];
getImeSwitchButton().setOnClickListener(mImeSwitcherClickListener);
updateRTLOrder();
}
}
这里也可以看到,在加载完xml文件后,会根据不同的旋转角度加载不同的layout布局文件。
2 导航栏创建之addNavigationBar 入口分析
addNavigationBar 代码实现如下:
// For small-screen devices (read: phones) that lack hardware navigation buttons
private void addNavigationBar() {
if (mNavigationBarView == null) return;
//关键点1
prepareNavigationBarView();
//关键点2: getNavigationBarLayoutParams分析
mWindowManager.addView(mNavigationBarView, getNavigationBarLayoutParams());
}
2.1 prepareNavigationBarView分析
继续分析prepareNavigationBarView,代码实现如下:
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);
updateSearchPanel();
}
导航栏布局的明确显示在prepareNavigationBarView中的mNavigationBarView.reorient();来决定,我们查看reorient方法,代码实现如下:
public void reorient() {
//获取屏幕旋转方向
final int rot = mDisplay.getRotation();
//隐藏导航栏布局
for (int i=0; i<4; i++) {
mRotatedViews[i].setVisibility(View.GONE);
}
//根据屏幕方向显示导航栏布局
mCurrentView = mRotatedViews[rot];
mCurrentView.setVisibility(View.VISIBLE);
getImeSwitchButton().setOnClickListener(mImeSwitcherClickListener);
mDeadZone = (DeadZone) mCurrentView.findViewById(R.id.deadzone);
//初始化导航栏的转换效果,这些效果可能包括动画和过渡。
mBarTransitions.init(mVertical);
//根据 mDisabledFlags 设置导航栏的禁用状态
setDisabledFlags(mDisabledFlags, true /* force */);
//设置菜单按钮的可见性
setMenuVisibility(mShowMenu, true /* force */);
//如果导航栏处于横屏/垂直模式,mDelegateHelper 对象交换 X 和 Y 坐标,以适应横屏布局。
if (mDelegateHelper != null) {
mDelegateHelper.setSwapXY(mVertical);
}
updateTaskSwitchHelper();
setNavigationIconHints(mNavigationIconHints, true);
}
2.2 getNavigationBarLayoutParams分析
我们回到PhoneStatusBar的addNavigationBar继续分析最后一个导航栏的LayoutParameters,它决定了导航栏在窗体上的显示位置,getNavigationBarLayoutParams代码实现如下:
private WindowManager.LayoutParams getNavigationBarLayoutParams() {
/*初始化参数说明如下:
FLAG_TOUCHABLE_WHEN_WAKING:当手机处于睡眠状态时,如果屏幕被按下,那么该window将第一个收到事件
FLAG_NOT_FOCUSABLE:不获取焦点
FLAG_NOT_TOUCH_MODAL:即使该window在可获得焦点情况下,仍然把该window之外的任何event发送到该window之后的其他window
FLAG_WATCH_OUTSIDE_TOUCH:不接受事件,转发到其他window
FLAG_SPLIT_TOUCH:当window设置这个flag,window会接收来自window边界之外发送给其他window的点击事件,支持多点触控.当这个flag没有设置的时候,第一下点击则决定了哪个window会接收整个点击事件,直到手指拿开.当设置了这个flag,这每一个点击事件(不一定是第一个)都决定了那个window来接收剩下的点击事件,直到手指拿开.点击事件会被分开传递给多个window.
*/
WindowManager.LayoutParams lp = new WindowManager.LayoutParams(
LayoutParams.MATCH_PARENT, LayoutParams.MATCH_PARENT,
WindowManager.LayoutParams.TYPE_NAVIGATION_BAR,
0
| 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,
PixelFormat.TRANSLUCENT);
// this will allow the navbar to run in an overlay on devices that support this
if (ActivityManager.isHighEndGfx()) {
//硬件加速参数
lp.flags |= WindowManager.LayoutParams.FLAG_HARDWARE_ACCELERATED;
}
lp.setTitle("NavigationBar");//窗口名称
lp.windowAnimations = 0;//不设置窗口动画
return lp;
}
上面的LayoutParames决定了导航栏在窗体的大小和显示的位置效果,当然这也是受父窗口影响的,当我们有变更导航栏的显示需求时就可以同各国修正LayoutParames参数来解决。
3 导航栏虚拟按键工作原理
虚拟按键是用来替代物理按键的,而这也是导航栏最重要的工作,输入子系统(IMS)中有一个关键的方法:injectInputEvent,即直接模拟物理按键上报输入事件,它是虚拟按键的实现基础。导航栏中的KeyButtonView就是该接口使用者之一,KeyButtonView中最重要的字段是mCode,用于指示其生成的按键事件的键值。
导航栏中有4个KeyButtonView,其中back、home、menu分别产生 KEY_BACK、KEY_HOME 、KEY_MENU 三种按键事件,recent不产生按键事件。
接下来关注KeyButtonView的两个部分:从触屏事件转换到按键事件、键盘事件发送。
3.1 从触屏事件转换到按键事件
这里从KeyButtonView的onTouchEvent()方法(该方法是触屏的回调方法)开始分析,代码实现如下:
public boolean onTouchEvent(MotionEvent ev) {
final int action = ev.getAction();
int x, y;
switch (action) {
//触屏事件-按下
case MotionEvent.ACTION_DOWN:
//Log.d("KeyButtonView", "press");
mDownTime = SystemClock.uptimeMillis();
setPressed(true);
//如果mCode被设置有值,则发送按键事件KeyEvent.ACTION_DOWN
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);
}
if (mSupportsLongpress) {
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);
//如果mCode被设置有值,则发送按键事件KeyEvent.ACTION_UP,带标记KeyEvent.FLAG_CANCELED
if (mCode != 0) {
sendEvent(KeyEvent.ACTION_UP, KeyEvent.FLAG_CANCELED);
}
if (mSupportsLongpress) {
removeCallbacks(mCheckLongPress);
}
break;
//触屏事件-抬起
case MotionEvent.ACTION_UP:
final boolean doIt = isPressed();
setPressed(false);
//如果mCode被设置有值,则发送按键事件KeyEvent.ACTION_UP
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 {
//如果没设置,则此时触发OnClickListener
//导航栏会采用这种方式来处理事件
// no key code, just a regular ImageView
if (doIt) {
performClick();
}
}
if (mSupportsLongpress) {
removeCallbacks(mCheckLongPress);
}
break;
}
return true;
}
整个过程就是 触摸事件转换成按键事件的一个过程。
3.2 键盘事件发送
接下来专注分析KeyButtonView的sendEvent方法,代码实现如下:
public void sendEvent(int action, int flags) {
sendEvent(action, flags, SystemClock.uptimeMillis());
}
继续分析,代码实现如下:
void sendEvent(int action, int flags, long when) {
//计算重复次数repeatCount
final int repeatCount = (flags & KeyEvent.FLAG_LONG_PRESS) != 0 ? 1 : 0;
//根据参数构建KeyEvent事件
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);
//将KeyEvent事件加入到InputDispatcher的派发队列。
//说明:INJECT_INPUT_EVENT_MODE_ASYNC表示加入派发队列后立刻返回,不阻塞,也不等待事件派发的成功与否。
InputManager.getInstance().injectInputEvent(ev,
InputManager.INJECT_INPUT_EVENT_MODE_ASYNC);
}
接下来最关键的就是执行InputManager的injectInputEvent方法了。这一部分属于输入子系统了,感兴趣的伙伴可查看这篇文章的后半部分:
Android Framework 输入子系统 (10)Input命令解读_input swipe