前記:
按鍵分發(fā)是android面試的一個重點芒炼,大家有必要好好掌握一下。
在手機上拌蜘,重點考察的是觸摸事件的分發(fā)彼棍,TV上考察的則是對按鍵分發(fā)的掌握情況。
研究的切入點:
客戶的需求:開機向?qū)pp跷乐,在遙控器連接上之后肥败,用戶可以按任意鍵跳轉(zhuǎn)到下一步。
mView 代表整個紅色的跟布局愕提,即LinearLayout馒稍。
跳轉(zhuǎn)代碼如下:只要有按鍵分發(fā)下來,就做跳轉(zhuǎn)的動作浅侨。
mView.setOnKeyListener(new View.OnKeyListener() {
@Override
public boolean onKey(View v, int keyCode, KeyEvent event) {
//跳轉(zhuǎn)到下一個界面
mHandler.sendEmptyMessageDelayed(MSG_ID_GO_NEXT_STEP, 200);
return true;
}
});
遇到的問題:阿倫反饋OK鍵不能跳轉(zhuǎn)纽谒,但是方向鍵可以跳轉(zhuǎn)!仗颈!
那么我們就帶著這個問題佛舱,來從源碼角度分析為什么OK鍵不可,上下左右可挨决?
提問時間:大家能看出是什么問題嗎请祖?? (5分鐘)
開始分析:
屏蔽跳轉(zhuǎn)+加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分析結(jié)果
首先按OK鍵脖祈,沒有任何打印肆捕。
然后按下鍵,有響應(yīng)盖高,但是僅僅是收到了下鍵的Up事件慎陵。
再按OK鍵,可收到OK鍵的Down+Up事件喻奥。
看起來就有點奇怪了席纽!
提問時間:這就是造成OK鍵無法跳轉(zhuǎn)的原因!撞蚕!大家能看出是什么問題嗎润梯?? (5分鐘)
開始看源碼
LinearLayout 沒有重寫dispatchKeyEvent()甥厦,故繼承父類ViewGroup的分發(fā)方法纺铭。
ViewGroup.java
// The view contained within this ViewGroup that has or contains focus.
private View mFocused; //子view
int mPrivateFlags //代表“當(dāng)前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
下面結(jié)合源代碼來看:
有幾個要點:
1.只有eng版本有效,user版本直接跳過
2.eng版本它能用來干啥谦秧?只講一個點:up事件彈起的時候竟纳,會檢測上一個鍵是不是同樣的keycode撵溃。
因為正常情況下,肯定是先有down再有up蚁袭,如果up的前一個不是down那么就是異常征懈。
異常會最終通過finishEvent()方法通過log打印出來JА揩悄!
更多的細節(jié)暫時沒研究了,大家自己去發(fā)現(xiàn)鬼悠!
由于user版本删性,一致性效驗器不生效,那么我們簡化下代碼焕窝。
// The view contained within this ViewGroup that has or contains focus.
//子view有焦點或者子view包含有焦點的子view
private View mFocused;
//代表“當(dāng)前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;
}
現(xiàn)在來分析這個分發(fā)方法:
- 屬性PFLAG_FOCUSED 表示有焦點
- 屬性PFLAG_HAS_BOUNDS 表示有邊界(一個view繪制完成蹬挺,就肯定有邊界的!)
如果當(dāng)前的ViewGroup有焦點+有邊界它掂,則進入super.dispatchKeyEvent(event) 方法巴帮!
ViewGroup的Super是View.java
//View.java的按鍵分發(fā)代碼:
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;
}
繼續(xù)分析:
app層調(diào)用了setOnKeyListener()方法榕茧。
View.java 執(zhí)行g(shù)etListenerInfo().mOnKeyListener = l;
執(zhí)行之后,mListenerInfo不為空了客给。mListenerInfo.mOnKeyListener 也不為空用押。
(mViewFlags & ENABLED_MASK) == ENABLED 成立,正常的View肯定是enable狀態(tài)的靶剑!
接下來蜻拨,就進入 li.mOnKeyListener.onKey(this, event.getKeyCode(), event) 完成app的回調(diào)!
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;
}
});
不做上層開發(fā)的桩引,可能覺得回調(diào)不好理解缎讼,這里就是實現(xiàn)的方法!好像以前凱榮疑惑過這個問題坑匠。
這里大家可以一起溝通(5分鐘)血崭,最好是能問懵的那種~··
整個回調(diào)過程已經(jīng)看清楚了,并沒有對OK鍵跟方向鍵做差異笛辟,那么問題在哪呢功氨??
繼續(xù)排查:
首先從ViewGroup開始加log:
// The view contained within this ViewGroup that has or contains focus.
//子view有焦點或者子view包含有焦點的子view
private View mFocused;
//代表“當(dāng)前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;
}
通過打印發(fā)現(xiàn)手幢,原來是mPrivateFlags 沒有焦點捷凄!
那么基本可以推測:OK鍵之所以沒有分發(fā)下來,是因為無焦點造成的围来。而方向鍵會賦予ViewGroup焦點跺涤。所以方向鍵可實現(xiàn)跳轉(zhuǎn)P僬觥!
嘗試解決:
mView.requestFocus();
測試驗證OK!
上面講的是解決+分析問題的過程桶错,現(xiàn)在我們稍微延伸一下航唆。
View焦點獲取的過程
方向鍵Down事件,使該ViewGroup獲取的焦點院刁,調(diào)用堆棧:
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的官網(wǎng)說明:
/*
* 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的繪制流程任岸,都是由改類分發(fā)發(fā)起。
但是ViewRootImpl是不是View狡刘,更像是View圖層的大腦O砬薄!
討論5分鐘:假如我們不看代碼嗅蔬,大致猜猜焦點切換邏輯剑按,越詳細越好
...
...
...
下面我們一起看下代碼層,看看焦點是如何完成切換的:
計算機術(shù)語中modifierkey 是“輔助按鍵”的意思澜术。
private int processKeyEvent(QueuedInputEvent q) {
final KeyEvent event = (KeyEvent)q.mEvent;
//... 省略不相關(guān)的代碼
// 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;
}
//焦點切換方向已經(jīng)確定
if (direction != 0) {
//找到當(dāng)前獲取焦點的View(因為要以當(dāng)前焦點View為基點锣枝,然后才能計算出下一個焦點是哪個小伙~)
View focused = mView.findFocus();
if (focused != null) {
//根據(jù)方向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已經(jīng)獲得了焦點(對應(yīng)的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;
}
上面的代碼薄坏,就是焦點如何完成切換的代碼趋厉。
大致過程是:首先找點找點當(dāng)前焦點View,然后根據(jù)位置關(guān)系+dirrection胶坠,計算出下一個焦點View.
聚焦邏輯的代碼細節(jié)君账,暫時不仔細分析,如果你非要問沈善,那只能說:
澤寶反饋的嘟嘟嘟bug:
大家一致的總結(jié)是兩個方向:
1.按鍵事件異常
2.audio播放異常
想法:
我們可以在上面的播放按鍵聲音的地方乡数,添加上log椭蹄,然后壓測。
如果異常復(fù)現(xiàn)的時候净赴,沒有反復(fù)調(diào)用這個播放代碼绳矩,則不是按鍵異常的問題。
基本定位在Audio上面了玖翅,也許是audio模塊的一個概率性播放異常R砉荨!烧栋!
這個需要大家下來好好分析了P赐住!审姓!
下面我們繼續(xù)聚焦在按鍵分發(fā)上面~~
安卓View層級的按鍵分發(fā)過程:
最后再根據(jù)Listener的不同返回值,來看下代碼調(diào)用過程:
APP響應(yīng)代碼
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 分發(fā)流程
@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 分發(fā)流程
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 代表“當(dāng)前ViewGroup”的屬性
if ((mPrivateFlags & (PFLAG_FOCUSED | PFLAG_HAS_BOUNDS))
== (PFLAG_FOCUSED | PFLAG_HAS_BOUNDS)) {
//當(dāng)前ViewGroup有焦點且有邊框魔吐,條件才成立。
}
二莱找、mFocused
// The view contained within this ViewGroup that has or contains focus.
private View mFocused;
意思是當(dāng)前的子ViewGroup有焦點酬姆,或者當(dāng)前子ViewGroup包含有焦點。
是當(dāng)前ViewGroup的子View奥溺。
onKey( )方法返回false辞色,整個調(diào)用流程:
層級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,整個調(diào)用流程:
層級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返回 )
總結(jié):
總體上浮定,按鍵分發(fā)是按View的層級逐層分發(fā)相满,分發(fā)的規(guī)則是以焦點View為基礎(chǔ),焦點View-> 焦點View -> 焦點View ... 桦卒。
彩蛋:
一立美、KeyEvent傳遞過程:
主要可以劃分為三步:過濾器、View樹方灾、Activity建蹄。
1.WindowManagerService.java內(nèi)有兩個線程,一個負責(zé)分發(fā)按鍵消息裕偿,一個負責(zé)讀取按鍵消息洞慎。在執(zhí)行processEvent分發(fā)事件時,系統(tǒng)有一些過濾器可以進行一些特定的處理操作嘿棘,這些過濾器的處理既可以在事件分發(fā)前也可以在事件分發(fā)后劲腿。比如home鍵在分發(fā)前進行處理消費,應(yīng)用無法監(jiān)聽該類消息蔫巩,而返回鍵是在事件分發(fā)之后谆棱,只有當(dāng)應(yīng)用不進行處理時快压,系統(tǒng)才會處理返回鍵用來退出當(dāng)前Activity,垃瞧。
2.processEvent分發(fā)事件后蔫劣,先傳遞到ViewRootImpl的deliverKeyEvent中,如果mView(即根DecorView)為空或者mAdded為false个从,則直接返回脉幢。若不為空,然后判斷IME窗口(輸入法)是否存在嗦锐,若存在則優(yōu)先傳遞到IME中嫌松,當(dāng)輸入窗口沒有處理這個事件,事件則會真正派發(fā)到根視圖mView.dispatchKeyEvent中奕污,然后回調(diào)Activity的dispatchKeyEvent(event)去處理萎羔,由于Activity中的dispatchKeyEvent最終調(diào)用的是mDecor中的superDispatchKeyEvent(event),之后就是ViewGroup通過遞歸調(diào)用把Event傳遞到指定的View上碳默。
3.事件傳遞到VIew之后贾陷,先會調(diào)用View的dispatchKeyEvent,如果有注冊Listener嘱根,就直接調(diào)用Listener的onKey去響應(yīng)髓废,如果沒有注冊Listener,z之后會根據(jù)Action的類型調(diào)用對應(yīng)的onXXX(onKeyDown,onKeyup)函數(shù)去處理,如果所有的view都沒有進行處理该抒,那么最終會回調(diào)到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
當(dāng)一個event事件傳遞到某個view上時愉适,如果對一些Action(比如Down)進行了消費后犯助,則該View下的子view以及想消費該event的Action的行為都不會執(zhí)行。默認(rèn)情況下维咸,ViewGroup控件不會執(zhí)行onkeyDown和onkeyup剂买,只有當(dāng)其焦點屬性為true時,才可以傳遞到執(zhí)行
以后再來進一步分析一下~~