Android 7.0 虚拟按键(NavigationBar)源码分析 之 点击事件的实现流程
来源:互联网 发布:java中单双引号 编辑:程序博客网 时间:2024/06/01 09:40
第二部分: Let's go!!!
【点击事件的实现流程】
1、初始化
虚拟按键点击效果的实现和实体按键相似,也是通过上报一个keyCode值,来判断哪个按钮被点击。不同的是,实体按键的keyCode值是硬件驱动层传递到上层的。而虚拟按键的keyCode值是应用层自己定义的。
首先来看KeyButtonView的构造函数。由此可见,最终都会调用到有三个参数的构造方法。最重要的是变量 mCode,它接收了在布局文件中定义的 keyCode 值。
public KeyButtonView(Context context, AttributeSet attrs) { this(context, attrs, 0); } public KeyButtonView(Context context, AttributeSet attrs, int defStyle) { super(context, attrs); TypedArray a = context.obtainStyledAttributes(attrs, R.styleable.KeyButtonView, defStyle, 0); //在布局xml文件中定义的keyCode值,用于分发点击事件时唯一标记一个按键 mCode = a.getInteger(R.styleable.KeyButtonView_keyCode, 0); //在布局xml文件中定义的值,定义该按钮是否支持长按。 mSupportsLongpress = a.getBoolean(R.styleable.KeyButtonView_keyRepeat, true); TypedValue value = new TypedValue(); //如果定义了android:contentDescription属性,则给该按钮添加描述 if (a.getValue(R.styleable.KeyButtonView_android_contentDescription, value)) { mContentDescriptionRes = value.resourceId; } a.recycle(); setClickable(true); //因为继承的ImageView,所以设置下它的Clickable为true,不然不能点击 mTouchSlop = ViewConfiguration.get(context).getScaledTouchSlop(); //该变量控制虚拟按键的可点击区域 mAudioManager = (AudioManager) context.getSystemService(Context.AUDIO_SERVICE); //获取音频服务,用于播放按键音 setBackground(new KeyButtonRipple(context, this)); //设置背景 }
2、事件的发送
之前说过KeyButtonView继承自ImageView,间接父类是View类,所以它的触摸事件可以通过 onTouchEvent() 回调方法来接收,单击和长按事件的发送,也是通过重写该方法实现的。
最重要的MotionEvent就是ACTION_DOWN事件,单击和长按事件主要是在这里处理的。
首先是单击事件。首先判断当前按钮的mCode,即keyCode的值。如果不为0,则通过 sendEvent() 发送ACTION_DOWN的事件。
然后把一个Runnable:mCheckLongPress 放入队列,延时0.5s执行,用与检查是否满足长按的条件。
注:ViewConfiguration.getLongPressTimeout() 的值为500ms,即0.5s。
其他MotionEvent就不细说了,代码里都写了注释。
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();//记录按下的时间 mLongClicked = false; setPressed(true); //设置当前按钮为按下的状态 if (mCode != 0) { //如果mCode不为零,则发送一个ACTION_DOWN类型的点击事件 sendEvent(KeyEvent.ACTION_DOWN, 0, mDownTime); } else { // Provide the same haptic feedback that the system offers for virtual keys. performHapticFeedback(HapticFeedbackConstants.VIRTUAL_KEY); } //再次进入MotionEvent.ACTION_DOWN时,移除检查长按状态的的Runnable removeCallbacks(mCheckLongPress); //发送一个延时0.5s的Runnable。用于检查当前按钮是否满足长按条件 postDelayed(mCheckLongPress, ViewConfiguration.getLongPressTimeout()); break; case MotionEvent.ACTION_MOVE: x = (int)ev.getX(); y = (int)ev.getY(); //获取当前触屏坐标,当手指移动出按键范围,将Pressed状态设为false setPressed(x >= -mTouchSlop && x < getWidth() + mTouchSlop && y >= -mTouchSlop && y < getHeight() + mTouchSlop); break; case MotionEvent.ACTION_CANCEL: setPressed(false); //发送CANCELED类型的点击事件 if (mCode != 0) { sendEvent(KeyEvent.ACTION_UP, KeyEvent.FLAG_CANCELED); } removeCallbacks(mCheckLongPress); break; case MotionEvent.ACTION_UP: final boolean doIt = isPressed() && !mLongClicked; setPressed(false); if (mCode != 0) { if (doIt) { sendEvent(KeyEvent.ACTION_UP, 0); //发送ACTION_UP事件 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; }
来看看这个mCheckLongPress的实现。当接收到 MotionEvent.ACTION_DOWN 事件0.5s后,run()就会被执行。但前提是,在这期间,没有再次接收到 ACTION_DOWN,ACTION_CANCEL,ACTION_UP 其中的任一事件,否则 mCheckLongPress 会被移除。
如果执行到了run(),判断当前按钮是否仍然为按下的状态,如果为true,表示满足长按的条件,因为从接收到ACTION_DOWN到现在一共0.5s,按钮一直处于pressed的状态。由此可见,系统默认按下按键持续0.5s即为长按动作。
通过isLongClickable()判断当前按钮是否支持长按,如果为true,则通过父类View的方法performLongClick()去发送一个长按的事件。
变量mSupportsLongpress默认值为true。用于确保当isLongClickable()为false时,也能发送出长按事件。
private final Runnable mCheckLongPress = new Runnable() { public void run() { if (isPressed()) { //判断当前按钮是否仍为按下的状态 if (isLongClickable()) { //判断是否支持长按 // Just an old-fashioned ImageView performLongClick(); //发送长按事件 mLongClicked = true; } else if (mSupportsLongpress) { sendEvent(KeyEvent.ACTION_DOWN, KeyEvent.FLAG_LONG_PRESS); sendAccessibilityEvent(AccessibilityEvent.TYPE_VIEW_LONG_CLICKED); mLongClicked = true; } } } };
细心的筒子们可能发现了。发送事件大部分都是通过 sendEvent() 来实现的。看下它的源码。
它将包含了keyCode,action和repeatCount等数据的KeyEvent,通过系统服务类InputManager,把事件发送了出去。
事件发送出去了,在哪处理呢?往下看。
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); }
3、事件的处理
由于虚拟按键需要在系统所有界面都能响应,所以点击事件也跟一般View的处理不太一样。我们知道,一个界面的点击事件发生时,是由当前Activity的dispatchTouchEvent()去分发,但具体的工作是由其内部的Window去完成的。所以要想在所有界面中都响应某个按键,则必须在Window的管理类中去处理。
路径是 frameworks/base/services/core/java/com/android/server/policy/PhoneWindowManager.java
当有点击事件发生时,首先都会在该类中进行处理,然后向下分发。来看看interceptKeyBeforeDispatching()。光看方法的名字,都可以推测,这个方法会在key事件被分发前被调用。
到这里,之前设置的keyCode就派上用场了。首先来看HOME键,通过keyCode确定当前按下了虚拟按键的HOME键。
先处理单击事件,把除了 KeyEvent.ACTION_DOWN 之外的key类型,作为单击事件结束的标志。中间加了一些条件,在某些条件下,不响应HOME键的点击操作。
关键方法是 handleShortPressOnHome(),下面细说。
接着是长按事件。 如果 repeatCount > 0 ,且事件里包含了 KeyEvent.FLAG_LONG_PRESS 这个FLAG,则说明是长按事件。在 handleLongPressOnHome(event.getDeviceId()) 中去处理。详情往下滑。
这里还有个双击事件,就不多说了,因为一般不用双击这个效果,而且原理也差不多。
@Override public long interceptKeyBeforeDispatching(WindowState win, KeyEvent event, int policyFlags) { final boolean keyguardOn = keyguardOn(); final int keyCode = event.getKeyCode(); final int repeatCount = event.getRepeatCount(); final int metaState = event.getMetaState(); final int flags = event.getFlags(); final boolean down = event.getAction() == KeyEvent.ACTION_DOWN; final boolean canceled = event.isCanceled(); ... // First we always handle the home key here, so applications // can never break it, although if keyguard is on, we do let // it handle it, because that gives us the correct 5 second // timeout. if (keyCode == KeyEvent.KEYCODE_HOME) { // If we have released the home key, and didn't do anything else // while it was pressed, then it is time to go home! if (!down) { cancelPreloadRecentApps(); //如果当前为显示最近使用APP列表界面,则隐藏掉 mHomePressed = false; if (mHomeConsumed) { mHomeConsumed = false; return -1; } if (canceled) { Log.i(TAG, "Ignoring HOME; event canceled."); return -1; } // If an incoming call is ringing, HOME is totally disabled. // (The user is already on the InCallUI at this point, // and his ONLY options are to answer or reject the call.) TelecomManager telecomManager = getTelecommService(); if (telecomManager != null && telecomManager.isRinging()) { Log.i(TAG, "Ignoring HOME; there's a ringing incoming call."); return -1; } // Delay handling home if a double-tap is possible. if (mDoubleTapOnHomeBehavior != DOUBLE_TAP_HOME_NOTHING) { mHandler.removeCallbacks(mHomeDoubleTapTimeoutRunnable); // just in case mHomeDoubleTapPending = true; mHandler.postDelayed(mHomeDoubleTapTimeoutRunnable, ViewConfiguration.getDoubleTapTimeout()); return -1; } handleShortPressOnHome(); return -1; } // Remember that home is pressed and handle special actions. if (repeatCount == 0) { mHomePressed = true; if (mHomeDoubleTapPending) { mHomeDoubleTapPending = false; mHandler.removeCallbacks(mHomeDoubleTapTimeoutRunnable); handleDoubleTapOnHome(); } else if (mLongPressOnHomeBehavior == LONG_PRESS_HOME_RECENT_SYSTEM_UI || mDoubleTapOnHomeBehavior == DOUBLE_TAP_HOME_RECENT_SYSTEM_UI) { preloadRecentApps(); } } else if ((event.getFlags() & KeyEvent.FLAG_LONG_PRESS) != 0) { if (!keyguardOn) { handleLongPressOnHome(event.getDeviceId()); } } return -1; } ... }
先讲单击的具体逻辑。一步步调用,来到了带两个参数的 launchHomeFromHotKey
这里分了两种情况。锁屏状态和非锁屏状态。
在锁屏状态下,不响应HOME键的点击操作,直接返回。
只有在非锁屏状态下,才能响应HOME键的操作。关键是 startDockOrHome(true, awakenFromDreams);
private void handleShortPressOnHome() { ... // Go home! launchHomeFromHotKey(); } void launchHomeFromHotKey() { launchHomeFromHotKey(true /* awakenFromDreams */, true /*respectKeyguard*/); } /** * A home key -> launch home action was detected. Take the appropriate action * given the situation with the keyguard. */ void launchHomeFromHotKey(final boolean awakenFromDreams, final boolean respectKeyguard) { if (respectKeyguard) { if (isKeyguardShowingAndNotOccluded()) { // don't launch home if keyguard showing return; } if (!mHideLockScreen && mKeyguardDelegate.isInputRestricted()) { // when in keyguard restricted mode, must first verify unlock // before launching home mKeyguardDelegate.verifyUnlock(new OnKeyguardExitResult() { @Override public void onKeyguardExitResult(boolean success) { if (success) { try { ActivityManagerNative.getDefault().stopAppSwitches(); } catch (RemoteException e) { } sendCloseSystemWindows(SYSTEM_DIALOG_REASON_HOME_KEY); startDockOrHome(true /*fromHomeKey*/, awakenFromDreams); } } }); return; } } // no keyguard stuff to worry about, just launch home! try { ActivityManagerNative.getDefault().stopAppSwitches(); } catch (RemoteException e) { } if (mRecentsVisible) { // Hide Recents and notify it to launch Home if (awakenFromDreams) { awakenDreams(); } hideRecentApps(false, true); } else { // Otherwise, just launch Home sendCloseSystemWindows(SYSTEM_DIALOG_REASON_HOME_KEY); startDockOrHome(true /*fromHomeKey*/, awakenFromDreams); } }
startDockOrHome(true, awakenFromDreams) 完成了界面的切换,从当前界面跳转到桌面。
每个桌面应用的主Activity会在AndroidManifest文件中设置一个 Intent.CATEGORY_HOME 的标签,通过这个标签,就可以通过intent匹配跳转到到桌面主界面。
mHomeIntent = new Intent(Intent.ACTION_MAIN, null); mHomeIntent.addCategory(Intent.CATEGORY_HOME); mHomeIntent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK | Intent.FLAG_ACTIVITY_RESET_TASK_IF_NEEDED); void startDockOrHome(boolean fromHomeKey, boolean awakenFromDreams) { if (awakenFromDreams) { awakenDreams(); } Intent dock = createHomeDockIntent(); if (dock != null) { try { if (fromHomeKey) { dock.putExtra(WindowManagerPolicy.EXTRA_FROM_HOME_KEY, fromHomeKey); } startActivityAsUser(dock, UserHandle.CURRENT); return; } catch (ActivityNotFoundException e) { } } Intent intent; if (fromHomeKey) { intent = new Intent(mHomeIntent); intent.putExtra(WindowManagerPolicy.EXTRA_FROM_HOME_KEY, fromHomeKey); } else { intent = mHomeIntent; } startActivityAsUser(intent, UserHandle.CURRENT); }
到此,单击事件告一段落。下面是长按事件。
关键方法 handleLongPressOnHome。
这里有个变量 mLongPressOnHomeBehavior,作用是控制按键长按所需要进行的操作。如果需要客制化,则改动mLongPressOnHomeBehavior的值,并在对应的值下进行响应的处理即可。
private void handleLongPressOnHome(int deviceId) { if (mLongPressOnHomeBehavior == LONG_PRESS_HOME_NOTHING) { return; } mHomeConsumed = true; //振动反馈 performHapticFeedbackLw(null, HapticFeedbackConstants.LONG_PRESS, false); switch (mLongPressOnHomeBehavior) { case LONG_PRESS_HOME_RECENT_SYSTEM_UI: toggleRecentApps(); //启动最近打开过的App列表界面 break; case LONG_PRESS_HOME_ASSIST: launchAssistAction(null, deviceId); //启动助手类应用 break; default: Log.w(TAG, "Undefined home long press behavior: " + mLongPressOnHomeBehavior); break; } }
OK。到此虚拟按键事件的发送和处理都已经完成了。
下面准备分享一个客制化修改NavigationBar的例子,并进行总结。
- Android 7.0 虚拟按键(NavigationBar)源码分析 之 点击事件的实现流程
- Android 7.0 虚拟按键(NavigationBar)源码分析 之 View的创建流程
- Android隐藏和沉浸式虚拟按键NavigationBar的实现
- android虚拟按键NavigationBar的判断
- Android源码基础解析之电源开关机按键事件流程
- Android之SystemUI加载流程和NavigationBar的分析
- Android移动开发-检测点击按键事件的实现
- android虚拟按键的实现
- 显示、隐藏NavigationBar(虚拟按键)
- 隐藏虚拟按键 NavigationBar
- Android学习之隐藏虚拟按键的实现
- 自定义外部按键实现android对按键事件的响应实现流程
- 【android】点击touch事件流程分析
- Android Activity的按键事件处理流程
- Android源码分析-点击事件派发机制
- Android源码分析-点击事件派发机制
- Android源码分析-点击事件派发机制
- Android源码分析-点击事件派发机制
- mysql获取两个时间的分钟数
- Eclipse创建一个新的spring Boot项目
- php如何使得你的对象可以像数组一样可以被访问(ArrayAccess 的作用)?
- 手机客户端弱网络下的断线重连处理
- 从程序员至技术管理者,那些年记忆深刻的书https://mudu.tv/watch/1341168
- Android 7.0 虚拟按键(NavigationBar)源码分析 之 点击事件的实现流程
- React Native 省市区地址选择器(仿京东)
- Claymore's ETH/ETC/ZEC挖矿免抽水提高算力教程!
- Java算法实现之归并排序
- 解决Microsoft Virtual Academy 和channel9下载字幕不能播放的问题
- [2017纪中10-24]方阵 二维ST表
- Linux学习-共用体及typedef
- ubuntu nohup命令
- 编写一个学生类(Students),包括姓名(name)、性别(sex)、学号(num)、语文课(Chinese)、英语课(English)、数学课(Math)和平均值(avg),方法包括求三门课的平