一步步浅析Android系统导航栏(NavigationBar)
2017-03-01 16:20
585 查看
Android手机可分为有导航栏以及没导航栏两种,一般有物理按键的机器不会带有导航栏,而没有物理按键的机器则基本会带,比如华为的手机基本都是带导航栏的。
导航栏是如何加载到桌面上?是如何实现与物理按键相同的功能的呢?带着种种疑问,我们来read the fucking source code。
导航栏是属于系统界面的一部分,也就是SystemUI的一部分。在SystemUI中导航栏实质上是一个继承LinearLayout的ViewGroup:NavigationBarView,在系统界面初始化的时候在PhoneStatusBar.java的makeStatusBarView方法中通过以下代码初始化中:
首先在第2行通过
NavigationBarView的布局文件比较长,我就只贴一部分:
我们主要关注在第10、25和41行中id分别为back、home、和recent_apps的3个KeyButtonView。如大家所料,这3个View分别对应着返回键,home键以及最近活动键(不是MENU键)。至于其它View的主要作用是为了布局的整齐,让三个按键平分导航栏的空间。
接下来我们看下KeyButtonView是个什么东东:
可见KeyButtonView是继承自ImageView的。接下来我们去了解下它是如何处理点击事件的:
在ACTION_DOWN、ACTION_CANCEL以及ACTION_UP中都调用了一个很重要的方法sendEvent():
KeyButtonView就是在sendEvent方法中通过构建出一个对应的keyCode的KeyEvent ev,然后调用InputManager的injectInputEvent模拟发送来实现与物理按键相同的功能。
其中第7行中KeyEvent的构建参数中的mCode的定义语句是这样的:
在上面贴出来的导航栏的布局文件中id为home以及id为back的KeyButtonView就分别通过systemui:keyCode属性对其进行了设置,设置的值分别是3和4。然后查看KeyEvent的KeyCode值:
可以看到,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加载到系统窗口中。
在第14行:prepareNavigationBarView方法中通过
给recent_apps这个KeyButtonView设置了监听:
逻辑非常简单清晰:当点击的时候就打开RecentApp的Activity。
到此NavigationBar的基本加载以及按键实现就分析完毕了,欢迎提问或拍砖,谢谢。
导航栏是如何加载到桌面上?是如何实现与物理按键相同的功能的呢?带着种种疑问,我们来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) { retu ce70 rn 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的基本加载以及按键实现就分析完毕了,欢迎提问或拍砖,谢谢。
相关文章推荐
- Android系统Google Maps开发实例浅析
- 浅析Android 4.0的通知系统(附Android 4.0设计指南全文翻译)
- [MDIT每天一小时]2014年Android系统就业前景浅析
- Android系统SD驱动浅析
- 系统去掉 Android 4.4.2 的StatusBar和NavigationBar
- 带你从头一步步配置android系统
- Android 监听系统虚拟导航栏按键
- Android 删除隐藏NavigationBar (虚拟导航栏)
- linux 内核启动过程以及挂载android 根文件系统的过程,以及介绍android 源代码中文件系统部分的浅析
- (Android系统)android log机制浅析
- 浅析Android 4.0的通知系统(附Android 4.0设计指南全文翻译)
- 浅析Android系统架构及内核
- Android系统之闹钟模块浅析
- iOS/Android系统多任务浅析
- 浅析Android 4.0的通知系统(附Android 4.0设计指南全文翻译)
- iOS/Android系统多任务浅析
- 浅析Android 4.0的通知系统(附Android 4.0设计指南全文翻译)
- 浅析Android 4.0的通知系统
- Android系统之闹钟模块浅析
- Android系统Google Maps开发实例浅析