前记:
按键分发是android面试的一个重点,大家有必要好好掌握一下。
在手机上,重点考察的是触摸事件的分发,TV上考察的则是对按键分发的掌握情况。
研究的切入点:
客户的需求:开机向导App,在遥控器连接上之后,用户可以按任意键跳转到下一步。
mView 代表整个红色的跟布局,即LinearLayout。
跳转代码如下:只要有按键分发下来,就做跳转的动作。
mView.setOnKeyListener(new View.OnKeyListener() {
@Override
public boolean onKey(View v, int keyCode, KeyEvent event) {
//跳转到下一个界面
mHandler.sendEmptyMessageDelayed(MSG_ID_GO_NEXT_STEP, 200);
return true;
}
});
遇到的问题:阿伦反馈OK键不能跳转,但是方向键可以跳转!!
那么我们就带着这个问题,来从源码角度分析为什么OK键不可,上下左右可?
提问时间:大家能看出是什么问题吗?? (5分钟)
开始分析:
屏蔽跳转+加log
mView.setOnKeyListener(new View.OnKeyListener() {
@Override
public boolean onKey(View v, int keyCode, KeyEvent event) {
CLog.d(TAG, "onKey keyCode:" + keyCode);
CLog.d(TAG, "event:" + event.getAction());
//mHandler.sendEmptyMessageDelayed(MSG_ID_GO_NEXT_STEP, 200);
return true;
}
});
Log分析结果
首先按OK键,没有任何打印。
然后按下键,有响应,但是仅仅是收到了下键的Up事件。
再按OK键,可收到OK键的Down+Up事件。
看起来就有点奇怪了!
提问时间:这就是造成OK键无法跳转的原因!!大家能看出是什么问题吗?? (5分钟)
开始看源码
LinearLayout 没有重写dispatchKeyEvent(),故继承父类ViewGroup的分发方法。
ViewGroup.java
// The view contained within this ViewGroup that has or contains focus.
private View mFocused; //子view
int mPrivateFlags //代表“当前ViewGroup”的属性
@Override
public boolean dispatchKeyEvent(KeyEvent event) {
if (mInputEventConsistencyVerifier != null) {
mInputEventConsistencyVerifier.onKeyEvent(event, 1);
}
//只有eng版本有效,可忽略(输入事件一致性校验器)
if ((mPrivateFlags & (PFLAG_FOCUSED | PFLAG_HAS_BOUNDS))
== (PFLAG_FOCUSED | PFLAG_HAS_BOUNDS)) {
if (super.dispatchKeyEvent(event)) {
return true;
}
} else if (mFocused != null && (mFocused.mPrivateFlags & PFLAG_HAS_BOUNDS)
== PFLAG_HAS_BOUNDS) {
if (mFocused.dispatchKeyEvent(event)) {
return true;
}
}
if (mInputEventConsistencyVerifier != null) {
mInputEventConsistencyVerifier.onUnhandledEvent(event, 1);
}//只有eng版本有效,可忽略(输入事件一致性校验器)
return false;
}
时间很充裕,我们来简单讲一下这个输入事件一致性效验器:mInputEventConsistencyVerifier
下面结合源代码来看:
有几个要点:
1.只有eng版本有效,user版本直接跳过
2.eng版本它能用来干啥?只讲一个点:up事件弹起的时候,会检测上一个键是不是同样的keycode。
因为正常情况下,肯定是先有down再有up,如果up的前一个不是down那么就是异常。
异常会最终通过finishEvent()方法通过log打印出来!!
更多的细节暂时没研究了,大家自己去发现!
由于user版本,一致性效验器不生效,那么我们简化下代码。
// The view contained within this ViewGroup that has or contains focus.
//子view有焦点或者子view包含有焦点的子view
private View mFocused;
//代表“当前ViewGroup”的属性
int mPrivateFlags
@Override
public boolean dispatchKeyEvent(KeyEvent event) {
//...
if ((mPrivateFlags & (PFLAG_FOCUSED | PFLAG_HAS_BOUNDS))
== (PFLAG_FOCUSED | PFLAG_HAS_BOUNDS)) {
if (super.dispatchKeyEvent(event)) {
return true;
}
} else if (mFocused != null && (mFocused.mPrivateFlags & PFLAG_HAS_BOUNDS)
== PFLAG_HAS_BOUNDS) {
if (mFocused.dispatchKeyEvent(event)) {
return true;
}
}
//...
return false;
}
现在来分析这个分发方法:
- 属性PFLAG_FOCUSED 表示有焦点
- 属性PFLAG_HAS_BOUNDS 表示有边界(一个view绘制完成,就肯定有边界的!)
如果当前的ViewGroup有焦点+有边界,则进入super.dispatchKeyEvent(event) 方法!
ViewGroup的Super是View.java
//View.java的按键分发代码:
public boolean dispatchKeyEvent(KeyEvent event) {
if (mInputEventConsistencyVerifier != null) {
mInputEventConsistencyVerifier.onKeyEvent(event, 0);
}//一致性校验,跳过
// Give any attached key listener a first crack at the event.
//noinspection SimplifiableIfStatement
ListenerInfo li = mListenerInfo;
if (li != null && li.mOnKeyListener != null && (mViewFlags & ENABLED_MASK) == ENABLED
&& li.mOnKeyListener.onKey(this, event.getKeyCode(), event)) {
return true;
}
if (event.dispatch(this, mAttachInfo != null
? mAttachInfo.mKeyDispatchState : null, this)) {
return true;
}
if (mInputEventConsistencyVerifier != null) {
mInputEventConsistencyVerifier.onUnhandledEvent(event, 0);
}//一致性校验,跳过
return false;
}
继续分析:
app层调用了setOnKeyListener()方法。
View.java 执行getListenerInfo().mOnKeyListener = l;
执行之后,mListenerInfo不为空了。mListenerInfo.mOnKeyListener 也不为空。
(mViewFlags & ENABLED_MASK) == ENABLED 成立,正常的View肯定是enable状态的!
接下来,就进入 li.mOnKeyListener.onKey(this, event.getKeyCode(), event) 完成app的回调!
mView.setOnKeyListener(new View.OnKeyListener() {
@Override
public boolean onKey(View v, int keyCode, KeyEvent event) {
CLog.d(TAG, "onKey keyCode:" + keyCode);
CLog.d(TAG, "event:" + event.getAction());
//mHandler.sendEmptyMessageDelayed(MSG_ID_GO_NEXT_STEP, 200);
return true;
}
});
不做上层开发的,可能觉得回调不好理解,这里就是实现的方法!好像以前凯荣疑惑过这个问题。
这里大家可以一起沟通(5分钟),最好是能问懵的那种~··
整个回调过程已经看清楚了,并没有对OK键跟方向键做差异,那么问题在哪呢??
继续排查:
首先从ViewGroup开始加log:
// The view contained within this ViewGroup that has or contains focus.
//子view有焦点或者子view包含有焦点的子view
private View mFocused;
//代表“当前ViewGroup”的属性
int mPrivateFlags
@Override
public boolean dispatchKeyEvent(KeyEvent event) {
//...
Log.d(TAG, "mPrivateFlags :" + mPrivateFlags);
if ((mPrivateFlags & (PFLAG_FOCUSED | PFLAG_HAS_BOUNDS))
== (PFLAG_FOCUSED | PFLAG_HAS_BOUNDS)) {
if (super.dispatchKeyEvent(event)) {
return true;
}
} else if (mFocused != null && (mFocused.mPrivateFlags & PFLAG_HAS_BOUNDS)
== PFLAG_HAS_BOUNDS) {
if (mFocused.dispatchKeyEvent(event)) {
return true;
}
}
//...
return false;
}
通过打印发现,原来是mPrivateFlags 没有焦点!
那么基本可以推测:OK键之所以没有分发下来,是因为无焦点造成的。而方向键会赋予ViewGroup焦点。所以方向键可实现跳转!!
尝试解决:
mView.requestFocus();
测试验证OK!
上面讲的是解决+分析问题的过程,现在我们稍微延伸一下。
View焦点获取的过程
方向键Down事件,使该ViewGroup获取的焦点,调用堆栈:
I ViewGroup: android.view.ViewGroup.requestChildFocus
I ViewGroup: android.view.View.handleFocusGainInternal
I ViewGroup: android.view.ViewGroup.handleFocusGainInternal
I ViewGroup: android.view.View.requestFocusNoSearch
I ViewGroup: android.view.View.requestFocus
I ViewGroup: android.view.ViewGroup.requestFocus
I ViewGroup: android.view.View.requestFocus
I ViewGroup: android.view.ViewRootImpl$ViewPostImeInputStage.processKeyEvent
下面看一下代码:
代码位于 ViewRootImpl.java
看下source的官网说明:
/*
* The top of a view hierarchy, implementing the needed protocol between View
* and the WindowManager. This is for the most part an internal implementation
* detail of {@link WindowManagerGlobal}.
* {@hide}
*/
view层级的顶部,JAVA层的按键事件、触摸事件、View的绘制流程,都是由改类分发发起。
但是ViewRootImpl是不是View,更像是View图层的大脑!!
讨论5分钟:假如我们不看代码,大致猜猜焦点切换逻辑,越详细越好
...
...
...
下面我们一起看下代码层,看看焦点是如何完成切换的:
计算机术语中modifierkey 是“辅助按键”的意思。
private int processKeyEvent(QueuedInputEvent q) {
final KeyEvent event = (KeyEvent)q.mEvent;
//... 省略不相关的代码
// Handle automatic focus changes.
//下面的代码块,完成的方向判定,即焦点切换方向(上、下、左、右)。
if (event.getAction() == KeyEvent.ACTION_DOWN) {
int direction = 0;
switch (event.getKeyCode()) {
case KeyEvent.KEYCODE_DPAD_LEFT:
if (event.hasNoModifiers()) {
direction = View.FOCUS_LEFT;
}
break;
case KeyEvent.KEYCODE_DPAD_RIGHT:
if (event.hasNoModifiers()) {
direction = View.FOCUS_RIGHT;
}
break;
case KeyEvent.KEYCODE_DPAD_UP:
if (event.hasNoModifiers()) {
direction = View.FOCUS_UP;
}
break;
case KeyEvent.KEYCODE_DPAD_DOWN:
if (event.hasNoModifiers()) {
direction = View.FOCUS_DOWN;
}
break;
case KeyEvent.KEYCODE_TAB:
if (event.hasNoModifiers()) {
direction = View.FOCUS_FORWARD;
} else if (event.hasModifiers(KeyEvent.META_SHIFT_ON)) {
direction = View.FOCUS_BACKWARD;
}
break;
}
//焦点切换方向已经确定
if (direction != 0) {
//找到当前获取焦点的View(因为要以当前焦点View为基点,然后才能计算出下一个焦点是哪个小伙~)
View focused = mView.findFocus();
if (focused != null) {
//根据方向direction,计算到下一个Foucus View,如果我们要该修改聚焦逻辑,那么可以从这里着手。
View v = focused.focusSearch(direction);
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);
}
//自此下一个View已经获得了焦点(对应的UI把焦点画上)
if (v.requestFocus(direction, mTempRect)) {
//咔一下,播放下焦点改变的声音。
//这里可以加log,下面再讲做啥
playSoundEffect(SoundEffectConstants
.getContantForFocusDirection(direction));
return FINISH_HANDLED;
}
}
// Give the focused view a last chance to handle the dpad key.
if (mView.dispatchUnhandledMove(focused, direction)) {
return FINISH_HANDLED;
}
} else {
// find the best view to give focus to in this non-touch-mode with no-focus
View v = focusSearch(null, direction);
if (v != null && v.requestFocus(direction)) {
return FINISH_HANDLED;
}
}
}
}
return FORWARD;
}
上面的代码,就是焦点如何完成切换的代码。
大致过程是:首先找点找点当前焦点View,然后根据位置关系+dirrection,计算出下一个焦点View.
聚焦逻辑的代码细节,暂时不仔细分析,如果你非要问,那只能说:
泽宝反馈的嘟嘟嘟bug:
大家一致的总结是两个方向:
1.按键事件异常
2.audio播放异常
想法:
我们可以在上面的播放按键声音的地方,添加上log,然后压测。
如果异常复现的时候,没有反复调用这个播放代码,则不是按键异常的问题。
基本定位在Audio上面了,也许是audio模块的一个概率性播放异常!!!
这个需要大家下来好好分析了!!!
下面我们继续聚焦在按键分发上面~~
安卓View层级的按键分发过程:
最后再根据Listener的不同返回值,来看下代码调用过程:
APP响应代码
mView.setOnKeyListener(new View.OnKeyListener() {
//onKey( )方
@Override
public boolean onKey(View v, int keyCode, KeyEvent event) {
// TODO Auto-generated method stub
return false; //case 1
return true; //case 2
}
});
ViewGroup.java 分发流程
@Override
public boolean dispatchKeyEvent(KeyEvent event) {
if ((mPrivateFlags & (PFLAG_FOCUSED | PFLAG_HAS_BOUNDS)) //自己有焦点
== (PFLAG_FOCUSED | PFLAG_HAS_BOUNDS)) {
//VG -1
if (super.dispatchKeyEvent(event)) {//VG -2
return true;//VG -3
}
//VG -4
} else if (mFocused != null && (mFocused.mPrivateFlags & PFLAG_HAS_BOUNDS) //包含焦点View
== PFLAG_HAS_BOUNDS) {
//VG -5
if (mFocused.dispatchKeyEvent(event)) { //VG -6
return true; //VG -7
}
}
return false; //VG -8
}
View.java 分发流程
public boolean dispatchKeyEvent(KeyEvent event) {
// Give any attached key listener a first crack at the event.
//noinspection SimplifiableIfStatement
//V-1
ListenerInfo li = mListenerInfo;
if (li != null && li.mOnKeyListener != null && (mViewFlags & ENABLED_MASK) == ENABLED
&& li.mOnKeyListener.onKey(this, event.getKeyCode(), event)) { //V -2
return true; //V -3
}
if (event.dispatch(this, mAttachInfo != null
? mAttachInfo.mKeyDispatchState : null, this)) {
return true;
}
return false; //V -4
}
要点提要:
一、mPrivateFlags 代表“当前ViewGroup”的属性
if ((mPrivateFlags & (PFLAG_FOCUSED | PFLAG_HAS_BOUNDS))
== (PFLAG_FOCUSED | PFLAG_HAS_BOUNDS)) {
//当前ViewGroup有焦点且有边框,条件才成立。
}
二、mFocused
// The view contained within this ViewGroup that has or contains focus.
private View mFocused;
意思是当前的子ViewGroup有焦点,或者当前子ViewGroup包含有焦点。
是当前ViewGroup的子View。
onKey( )方法返回false,整个调用流程:
层级1. DecorView :VG-5 VG-6
层级2. LinearLayout : VG-5 VG-6
层级3. FrameLayout : VG-5 VG-6
层级4. LinearLayout : VG-5 VG-6
层级5. LinearLayout : VG-5 VG-6
层级6. FrameLayout : VG-5 VG-6 ( framelayout_container )
层级7. LinearLayout VG-1 VG-2 V-1 V-2 V-4
接着开始返回了:
VG-8( 层级7返回 ) VG-8( 层级6返回 ) VG-8( 层级5返回 ) VG-8( 层级4返回 ) VG-8( 层级3返回 ) VG-8( 层级2返回 ) VG-8( 层级1返回 )
onKey( )方法返回true,整个调用流程:
层级1. DecorView :VG-5 VG-6
层级2. LinearLayout : VG-5 VG-6
层级3. FrameLayout : VG-5 VG-6
层级4. LinearLayout : VG-5 VG-6
层级5. LinearLayout : VG-5 VG-6
层级6. FrameLayout : VG-5 VG-6 ( framelayout_container )
层级7. LinearLayout VG-1 VG-2 V-1 V-2 V-3
接着开始返回了:
VG-3(层级7返回)VG-7( 层级6返回 ) VG-7(层级5返回 ) VG-7(层级 4返回 ) VG-7( 层级3返回 ) VG-7(层级 2返回 ) VG-7( 层级1返回 )
总结:
总体上,按键分发是按View的层级逐层分发,分发的规则是以焦点View为基础,焦点View-> 焦点View -> 焦点View ... 。
彩蛋:
一、KeyEvent传递过程:
主要可以划分为三步:过滤器、View树、Activity。
1.WindowManagerService.java内有两个线程,一个负责分发按键消息,一个负责读取按键消息。在执行processEvent分发事件时,系统有一些过滤器可以进行一些特定的处理操作,这些过滤器的处理既可以在事件分发前也可以在事件分发后。比如home键在分发前进行处理消费,应用无法监听该类消息,而返回键是在事件分发之后,只有当应用不进行处理时,系统才会处理返回键用来退出当前Activity,。
2.processEvent分发事件后,先传递到ViewRootImpl的deliverKeyEvent中,如果mView(即根DecorView)为空或者mAdded为false,则直接返回。若不为空,然后判断IME窗口(输入法)是否存在,若存在则优先传递到IME中,当输入窗口没有处理这个事件,事件则会真正派发到根视图mView.dispatchKeyEvent中,然后回调Activity的dispatchKeyEvent(event)去处理,由于Activity中的dispatchKeyEvent最终调用的是mDecor中的superDispatchKeyEvent(event),之后就是ViewGroup通过递归调用把Event传递到指定的View上。
3.事件传递到VIew之后,先会调用View的dispatchKeyEvent,如果有注册Listener,就直接调用Listener的onKey去响应,如果没有注册Listener,z之后会根据Action的类型调用对应的onXXX(onKeyDown,onKeyup)函数去处理,如果所有的view都没有进行处理,那么最终会回调到activity的onXXX方法中。
二、Activity View的dispatchKeyEvent,onkeyDown,onKeyUp, setListener处理顺序:
Activity.dispatchKeyEvent(down) ----->view.dispatchKeyEvent ------>view.setListener(如setOnKeyListener) ------>view.onkeyDown------->Activity.onkeyDown------>Activity.dispatchKeyEven(up)------>view.dispatchKeyEvent---->view.setListener(如setOnKeyListener) ------>view.onkeyup----->view.onClick(setOnClickListener)---->Activity.onkeyUp
当一个event事件传递到某个view上时,如果对一些Action(比如Down)进行了消费后,则该View下的子view以及想消费该event的Action的行为都不会执行。默认情况下,ViewGroup控件不会执行onkeyDown和onkeyup,只有当其焦点属性为true时,才可以传递到执行
以后再来进一步分析一下~~