该系列文章总纲链接:专题分纲目录 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() {@Overridepublic void onVerticalChanged(boolean isVertical) {if (mSearchPanelView != null) {mSearchPanelView.setHorizontal(isVertical);}mNotificationPanel.setQsScrimEnabled(!isVertical);}});//设置导航栏触摸事件mNavigationBarView.setOnTouchListener(new View.OnTouchListener() {@Overridepublic 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 beautodetected 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.NavigationBarViewxmlns: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"><LinearLayoutandroid: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 --><Viewandroid: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"/><FrameLayoutandroid:layout_width="@dimen/navigation_side_padding"android:layout_height="match_parent"android:layout_weight="0" ><com.android.systemui.statusbar.policy.KeyButtonViewandroid: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.KeyButtonViewandroid: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 --><LinearLayoutandroid:layout_height="match_parent"android:layout_width="match_parent"android:orientation="horizontal"android:id="@+id/lights_out"android:visibility="gone"><ImageViewandroid: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"/><ImageViewandroid: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"/><ImageViewandroid: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.DeadZoneandroid: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 --><FrameLayoutandroid:layout_weight="0"android:layout_width="match_parent"android:layout_height="@dimen/navigation_side_padding" ><com.android.systemui.statusbar.policy.KeyButtonViewandroid: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.KeyButtonViewandroid: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"/><Viewandroid: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"><ImageViewandroid: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"/><ImageViewandroid: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"/><ImageViewandroid: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.DeadZoneandroid: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方法@Overridepublic void onFinishInflate() {//屏幕方位0和180方向显示的导航栏为rot0,90和270显示的导航栏为rot90mRotatedViews[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;//关键点1prepareNavigationBarView();//关键点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之后的其他windowFLAG_WATCH_OUTSIDE_TOUCH:不接受事件,转发到其他windowFLAG_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 thisif (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_DOWNif (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_CANCELEDif (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_UPif (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 ImageViewif (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) {//计算重复次数repeatCountfinal 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