View工作原理之按键消息派发过程
来源:互联网 发布:screenflow for mac 编辑:程序博客网 时间:2024/05/21 21:45
1、按键派发总体过程
ViewRoot中定义了一个InputHandler对象:
private final InputHandler mInputHandler = new InputHandler() { public void handleKey(KeyEvent event, Runnable finishedCallback) { startInputEvent(finishedCallback); dispatchKey(event, true); } public void handleMotion(MotionEvent event, Runnable finishedCallback) { startInputEvent(finishedCallback); dispatchMotion(event, true); } };
该对象有两个方法,分别是handleKey()和handleMotion()
当底层得到按键消息,会调用InputHandler对象的handleKey(),handleKey()中再调用dispatchKey(),在dispatchKey()中发送一个异步消息,然后调用到了deliverKeyEvent(),代码如下:
private void deliverKeyEvent(KeyEvent event, boolean sendDone) { // If mView is null, we just consume the key event because it doesn't // make sense to do anything else with it. boolean handled = mView != null ? mView.dispatchKeyEventPreIme(event) : true; if (handled) { if (sendDone) { finishInputEvent(); } return; } // If it is possible for this window to interact with the input // method window, then we want to first dispatch our key events // to the input method. if (mLastWasImTarget) { InputMethodManager imm = InputMethodManager.peekInstance(); if (imm != null && mView != null) { int seq = enqueuePendingEvent(event, sendDone); if (DEBUG_IMF) Log.v(TAG, "Sending key event to IME: seq=" + seq + " event=" + event); imm.dispatchKeyEvent(mView.getContext(), seq, event, mInputMethodCallback); return; } } deliverKeyEventToViewHierarchy(event, sendDone); }
该方法中,在输入法窗口处理前,调用dispatchKeyEventPreIme(),如果需要回执,通过finishInputEvent()告诉WmS已经处理了该按键消息。接着把按键消息转发给输入法,在ViewRoot对象里包含真正的View对象mView,故最后就能通过deliverKeyEventToViewHierarchy()转发给真正的视图了。
private void deliverKeyEventToViewHierarchy(KeyEvent event, boolean sendDone) { try { if (mView != null && mAdded) { final int action = event.getAction(); boolean isDown = (action == KeyEvent.ACTION_DOWN); if (checkForLeavingTouchModeAndConsume(event)) { return; } if (Config.LOGV) { captureKeyLog("captureDispatchKeyEvent", event); } boolean keyHandled = mView.dispatchKeyEvent(event); if (!keyHandled && isDown) { int direction = 0; switch (event.getKeyCode()) { case KeyEvent.KEYCODE_DPAD_LEFT: direction = View.FOCUS_LEFT; break; case KeyEvent.KEYCODE_DPAD_RIGHT: direction = View.FOCUS_RIGHT; break; case KeyEvent.KEYCODE_DPAD_UP: direction = View.FOCUS_UP; break; case KeyEvent.KEYCODE_DPAD_DOWN: direction = View.FOCUS_DOWN; break; } if (direction != 0) { View focused = mView != null ? mView.findFocus() : null; if (focused != null) { View v = focused.focusSearch(direction); boolean focusPassed = false; if (v != null && v != focused) { // do the math the get the interesting rect // of previous focused into the coord system of // newly focused view focused.getFocusedRect(mTempRect); if (mView instanceof ViewGroup) { ((ViewGroup) mView).offsetDescendantRectToMyCoords( focused, mTempRect); ((ViewGroup) mView).offsetRectIntoDescendantCoords( v, mTempRect); } focusPassed = v.requestFocus(direction, mTempRect); } if (!focusPassed) { mView.dispatchUnhandledMove(focused, direction); } else { playSoundEffect(SoundEffectConstants.getContantForFocusDirection(direction)); } } } } } } finally { if (sendDone) { finishInputEvent(); } // Let the exception fall through -- the looper will catch // it and take care of the bad app for us. } }
deliverKeyEventToViewHierarchy()方法可分四步执行:
(1)checkForLeavingTouchModeAndConsume()判断该消息是否会导致离开触摸模式,一般返回false
(2)mView.dispatchKeyEvent(event)把事件派发给根视图
(3)如果是Down事件且是方向键,进行方向键的处理
(4)最后通过finishInputEvent()报告WmS已处理该消息。
2、根视图内部派发过程
接着上一节讲,系统调用到了mView.dispatchKeyEvent(event),这个根视图是DecorView或普通的ViewGroup。现在介绍PhoneWindow.DecorView这个根视图内部的按键消息派发流程,该函数代码如下:
public boolean dispatchKeyEvent(KeyEvent event) { final int keyCode = event.getKeyCode(); final boolean isDown = event.getAction() == KeyEvent.ACTION_DOWN; /* * If the user hits another key within the play sound delay, then * cancel the sound */ if (keyCode != KeyEvent.KEYCODE_VOLUME_DOWN && keyCode != KeyEvent.KEYCODE_VOLUME_UP && mVolumeKeyUpTime + VolumePanel.PLAY_SOUND_DELAY > SystemClock.uptimeMillis()) { /* * The user has hit another key during the delay (e.g., 300ms) * since the last volume key up, so cancel any sounds. */ AudioManager audioManager = (AudioManager) getContext().getSystemService( Context.AUDIO_SERVICE); if (audioManager != null) { audioManager.adjustSuggestedStreamVolume(AudioManager.ADJUST_SAME, mVolumeControlStreamType, AudioManager.FLAG_REMOVE_SOUND_AND_VIBRATE); } } if (isDown && (event.getRepeatCount() == 0)) { // First handle chording of panel key: if a panel key is held // but not released, try to execute a shortcut in it. if ((mPanelChordingKey > 0) && (mPanelChordingKey != keyCode)) { // Perform the shortcut (mPreparedPanel can be null since // global shortcuts (such as search) don't rely on a // prepared panel or menu). boolean handled = performPanelShortcut(mPreparedPanel, keyCode, event, Menu.FLAG_PERFORM_NO_CLOSE); if (!handled) { /* * If not handled, then pass it to the view hierarchy * and anyone else that may be interested. */ handled = dispatchKeyShortcutEvent(event); if (handled && mPreparedPanel != null) { mPreparedPanel.isHandled = true; } } if (handled) { return true; } } // If a panel is open, perform a shortcut on it without the // chorded panel key if ((mPreparedPanel != null) && mPreparedPanel.isOpen) { if (performPanelShortcut(mPreparedPanel, keyCode, event, 0)) { return true; } } } final Callback cb = getCallback(); final boolean handled = cb != null && mFeatureId < 0 ? cb.dispatchKeyEvent(event) : super.dispatchKeyEvent(event); if (handled) { return true; } return isDown ? PhoneWindow.this.onKeyDown(mFeatureId, event.getKeyCode(), event) : PhoneWindow.this.onKeyUp(mFeatureId, event.getKeyCode(), event); }
现分析处理流程:
(1)处理间量键
(2)调用performPanelShortcut()方法处理系统快捷键
(3)调用Callback对象的dispatchKeyEvent()
(4)最后调用PhoneWindow对象的onKeyDown()和onKeyUp()事件根视图只处理了少数按键消息。
3、Activity内部派发过程
Activity实现了Window.Callback接口,上一节中的(3)调用Callback对象的dispatchKeyEvent(),实际上就是使Activity对象调用了dispatchKeyEvent(),代码如下:
public boolean dispatchKeyEvent(KeyEvent event) { onUserInteraction(); Window win = getWindow(); if (win.superDispatchKeyEvent(event)) { return true; } View decor = mDecor; if (decor == null) decor = win.getDecorView(); return event.dispatch(this, decor != null ? decor.getKeyDispatcherState() : null, this); }
执行过程分析如下:
1、调用onUserInteraction(),当开始和activity交互的时候,用户可以做点什么
2、获取Window对象,实际上就是PhoneWindow对象,因此调用到:
public boolean superDispatchKeyEvent(KeyEvent event) { return mDecor.superDispatchKeyEvent(event); }
接着又调用:
public boolean superDispatchKeyEvent(KeyEvent event) { return super.dispatchKeyEvent(event); }
DecorView的父类是FrameLayout,该函数并没有重载dispatchKeyEvent(),则super最后会调用到ViewGroup的同名函数
ViewGroup的dispatchKeyEvent()的实现如下:
public boolean dispatchKeyEvent(KeyEvent event) { if ((mPrivateFlags & (FOCUSED | HAS_BOUNDS)) == (FOCUSED | HAS_BOUNDS)) { return super.dispatchKeyEvent(event); } else if (mFocused != null && (mFocused.mPrivateFlags & HAS_BOUNDS) == HAS_BOUNDS) { return mFocused.dispatchKeyEvent(event); } return false; }如果ViewGroup本身拥有焦点,则把消息派发到试图本身,如果是子视图拥有焦点,则把消息派发给子视图。
例如有多重嵌套的视图,由于mFocused变量代表的是拥有焦点的视图或是包含拥有焦点视图的ViewGroup,所以从最上层开始,逐层调用dispatchKeyEvent(),最后到拥有焦点的视图。如果这个视图是View,则最后调用的是:
public boolean dispatchKeyEvent(KeyEvent event) { // If any attached key listener a first crack at the event. //noinspection SimplifiableIfStatement if (android.util.Config.LOGV) { captureViewInfo("captureViewKeyEvent", this); } if (mOnKeyListener != null && (mViewFlags & ENABLED_MASK) == ENABLED && mOnKeyListener.onKey(this, event.getKeyCode(), event)) { return true; } return event.dispatch(this, mAttachInfo != null ? mAttachInfo.mKeyDispatchState : null, this); }
这个方法中,首先回调onKey()函数,如果onKey()中没有消费该消息,则最后调用event.dispatch()函数。第一个参数是View本身,因此会调用到View对象的onKeyDown和onKeyUp()方法等。如果在这个地方继续返回false,则会执行到第三步。
3、第2步中派发给用户视图的消息没有被处理,则通过event.dispatch()方法调用Activity内部的onKeyDown()和onKeyUp()等。
分析到这里也解释出了如果自定义View没有获得焦点,那么你复写的onKeyDown()和onKeyUp()方法是不会被调用到,而最终调用了Activity对象的onKeyDown()和onKeyUp()。
下面具体分析KeyEvent的dispatch()中的Down事件过程,代码如下
case ACTION_DOWN: { mFlags &= ~FLAG_START_TRACKING; if (DEBUG) Log.v(TAG, "Key down to " + target + " in " + state + ": " + this); boolean res = receiver.onKeyDown(mKeyCode, this); if (state != null) { if (res && mRepeatCount == 0 && (mFlags&FLAG_START_TRACKING) != 0) { if (DEBUG) Log.v(TAG, " Start tracking!"); state.startTracking(this, target); } else if (isLongPress() && state.isTracking(this)) { try { if (receiver.onKeyLongPress(mKeyCode, this)) { if (DEBUG) Log.v(TAG, " Clear from long press!"); state.performedLongPress(this); res = true; } } catch (AbstractMethodError e) { } } } return res; }
1、清除了mFlags标志
2、receiver.onKeyDown()执行,这个receiver可能是View对象或activity对象
3、如果第二步返回true,且是第一次按,并且mFlags包含FLAG_START_TRACKING,才执行后面的动作
这里的这个现象很奇怪,mFlags在前面明明在前被清除过,后面判断mFlags&FLAG_START_TRACKING肯定应该等于0啊
这里给出的解释是这样的,要想让你的自定义View能够执行到onKeyLongPress(),称为”处理生理长按“,须做以下几件事情。
1、你的View必须重载onKeyDown()事件,且返回true
2、onKeyDown()中必须执行event.startTracking(),参见Activity中的onKeyDown()方法也是调用了的。
3、最后要做的就是重载onKeyLongPress(),做你想要做的事情了。
在处理ACTION_UP,先调用state.handleUpEvent(),判断是否发生了“生理长按”,最后调到了receiver.onKeyUp()。
分析View中的onKeyDown()和onKeyUp()
onKeyDown()中实现的功能:
1、只接收DPAD_CENTER和KEYCODE_ENTER这两个按键消息
2、判断是View是否ENABLE,如果不是,返回false
3、如果可点击或可长按,通过setPress(true)设置视图为按下状态
4、postDelay一个Runnable来实现长按,不则于前面讲的“生理长按”,这里超过500ms,则会回调onLongClick()
onKeyUp()中实现的功能
1、判断是否可点
2、如果处于pressed的状态,调用setPress(false)
3、如果释放按键时还没有500ms,则取消前面的Runnable
4、调用performClick()方法,回调onClick()方法
- View工作原理之按键消息派发过程
- View工作原理之触摸消息派发过程
- 13 View工作原理【消息类型与按键消息派发】
- 《Android内核剖析》读书笔记 第13章 View工作原理【消息类型与按键消息派发】
- View工作原理【触摸消息派发】
- 《Android内核剖析》读书笔记 第13章 View工作原理【触摸消息派发】
- 按键消息派发流程
- Android 自定义View 之 触摸消息派发
- Android 触摸消息派发之ViewGroup的派发过程
- Android中按键消息的派发过程及源码分析
- Android中按键消息的派发过程及源码分析
- Android中按键消息的派发过程及源码分析
- Android中按键消息的派发过程及源码分析
- View消息派发
- View的工作原理之measure过程
- View的工作原理之layout过程
- 系统入门(15):Android中按键消息的派发过程及源码分析
- View工作原理【View重绘过程】
- Android应用开发之《生命线》
- Linux的五个查找命令:find,locate,whereis,which,type
- SHGetSpecialFolderPath 和 CSIDLf
- cloudfy
- Android的framework层音量控制原理分析--hot(key)处理
- View工作原理之按键消息派发过程
- java,传递和返回对象
- How To Convert DMG To ISO on Mac OSX, Windows and Linux
- 现有IOS设备唯一标示符的方案比较
- myeclipse configuration center总结
- Oracle 11g 安装过程中“检查网络配置要求 未执行”解决方法
- 【iOS功能实现】通过segue切换视图控制器
- 贴一段导出excel的源代码(数据由数据库查询出来然后以excle的格式导出)
- C#与SQLite的操作介绍