Android焦点事件分发与传递机制

男儿欲遂平生志,六经勤向窗前读。这篇文章主要讲述Android焦点事件分发与传递机制相关的知识,希望能为你提供帮助。
如果您对TouchEvent事件分发机制不太了解的,可以参考我的这篇文章——安卓TounchEvent事件分发机制。
问题:TV端焦点满天飞,如何解决和处理?记得初入TV开发,以为很简单。TV的这些界面与布局太简单了,分分钟就可以把页面搭建出来,处理好,然后就没有然后了。。。。
下面我们就从源码来带大家进行安卓TV焦点事件的传递
这里先给出android系统View的绘制流程:
依次执行View类里面的如下三个方法:

  • measure(int ,int) :测量View的大小
  • layout(int ,int ,int ,int) :设置子View的位置
  • draw(Canvas) :绘制View内容到Canvas画布上
Android焦点事件分发与传递机制

文章图片

ViewRootImpl的主要作用如下(此处不多讲,如有意图,看源码):
  • A:链接WindowManager和DecorView的纽带,更广一点可以说是Window和View之间的纽带。
  • B:完成View的绘制过程,包括measure、layout、draw过程。
  • C:向DecorView分发收到的用户发起的event事件,如按键,触屏等事件。
ViewRootImpl不再多余叙述,进入正题:
Android焦点分发的主要方法以及拦截方法的讲解。
在RootViewImpl中的函数通道是各种策略(InputStage)的组合,各策略负责的任务不同,如SyntheticInputStage、ViewPostImeInputStage、NativePostImeInputStage等等,这些策略以链表结构结构起来,当一个策略者没有消费事件时,就传递个下一个策略者。其中触摸和按键事件由ViewPostImeInputStage处理。
@Override protected int onProcess(QueuedInputEvent q) { if (q.mEvent instanceof KeyEvent) { return processKeyEvent(q); //如果是按键事件走此处,处理按键和焦点问题了 } else { final int source = q.mEvent.getSource(); if ((source & InputDevice.SOURCE_CLASS_POINTER) != 0) { return processPointerEvent(q); //如果是触摸事件走此处 } else if ((source & InputDevice.SOURCE_CLASS_TRACKBALL) != 0) { return processTrackballEvent(q); } else { return processGenericMotionEvent(q); } } }

processKeyEvent(QueuedInputEvent q)源码如下:
@Override protected void onDeliverToNext(QueuedInputEvent q) { if (mUnbufferedInputDispatch & & q.mEvent instanceof MotionEvent & & ((MotionEvent)q.mEvent).isTouchEvent() & & isTerminalInputEvent(q.mEvent)) { mUnbufferedInputDispatch = false; scheduleConsumeBatchedInput(); } super.onDeliverToNext(q); }private int processKeyEvent(QueuedInputEvent q) { final KeyEvent event = (KeyEvent)q.mEvent; // Deliver the key to the view hierarchy. if (mView.dispatchKeyEvent(event)) { return FINISH_HANDLED; }if (shouldDropInputEvent(q)) { return FINISH_NOT_HANDLED; }// If the Control modifier is held, try to interpret the key as a shortcut. if (event.getAction() == KeyEvent.ACTION_DOWN & & event.isCtrlPressed() & & event.getRepeatCount() == 0 & & !KeyEvent.isModifierKey(event.getKeyCode())) { if (mView.dispatchKeyShortcutEvent(event)) { return FINISH_HANDLED; } if (shouldDropInputEvent(q)) { return FINISH_NOT_HANDLED; } }// Apply the fallback event policy. if (mFallbackEventHandler.dispatchKeyEvent(event)) { return FINISH_HANDLED; } if (shouldDropInputEvent(q)) { return FINISH_NOT_HANDLED; }// 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 focused = mView.findFocus(); if (focused != null) { 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); } if (v.requestFocus(direction, mTempRect)) { 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; }

进入源码讲解:(1) 首先由dispatchKeyEvent进行焦点的分发
如果dispatchKeyEvent方法返回true,那么下面的焦点查找步骤就不会继续了。
dispatchKeyEvent方法返回true代表事件(包括焦点和按键)被消费了。
dispatchKeyEvent(event)如果不了解,看我上一篇文章安卓TounchEvent事件分发机制。
mView的dispatchKeyEvent方法,
mView是是Activity的顶层容器DecorView,它是一FrameLayout。
所以这里的dispatchKeyEvent方法应该执行的是ViewGroup的dispatchKeyEvent()方法,而不是View的dispatchKeyEvent方法。
@Override public boolean dispatchKeyEvent(KeyEvent event) { if (mInputEventConsistencyVerifier != null) { mInputEventConsistencyVerifier.onKeyEvent(event, 1); }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); } return false; }

ViewGroup的dispatchKeyEvent简略执行流程
首先ViewGroup会执行父类的dispatchKeyEvent方法,如果返回true那么父类的dispatchKeyEvent方法就会返回true,也就代表父类消费了该焦点事件,那么焦点事件自然就不会往下进行分发。
然后ViewGroup会判断mFocused这个view是否为空,如果为空就会****return false,焦点继续往下传递;如果不为空,那就会return mFocused的dispatchKeyEvent方法返回的结果。这个mFocused是ViewGroup中当前获取焦点的子View,这个可以从requestChildFocus方法中得到答案。
requestChildFocus()的源码如下:
@Override public void requestChildFocus(View child, View focused) { if (DBG) { System.out.println(this + " requestChildFocus()"); } if (getDescendantFocusability() == FOCUS_BLOCK_DESCENDANTS) { return; }// Unfocus us, if necessary super.unFocus(focused); // We had a previous notion of who had focus. Clear it. if (mFocused != child) { if (mFocused != null) { mFocused.unFocus(focused); }mFocused = child; } if (mParent != null) { mParent.requestChildFocus(this, focused); } }

居然有这个彩蛋?
Android焦点事件分发与传递机制

文章图片

View的dispatchKeyEvent简略执行流程
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; }

要修改ViewGroup焦点事件的分发:
  • 重写view的dispatchKeyEvent方法
  • 给某个子view设置onKeyListener监听
焦点没有被dispatchKeyEvent拦截的情况下的继续代码中的处理过程,还是进入ViewRootImpl源码
// 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 focused = mView.findFocus(); if (focused != null) { 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); } if (v.requestFocus(direction, mTempRect)) { 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; } } } }

dispatchKeyEvent方法返回false后,先得到按键的方向direction一个int值。direction值是后面来进行焦点查找的。
接着会调用DecorView的findFocus()方法一层一层往下查找已经获取焦点的子View。
DecorView则是PhoneWindow类的一个内部类,继承于FrameLayout,由此可知它是一个ViewGroup。
那么,DecroView到底充当了什么样的角色呢?
其实,DecorView是整个ViewTree的最顶层View,它是一个FrameLayout布局,代表了整个应用的界面。在该布局下面,有标题view和内容view这两个子元素。
@Override public View findFocus() { if (DBG) { System.out.println("Find focus in " + this + ": flags=" + isFocused() + ", child=" + mFocused); }if (isFocused()) { return this; }if (mFocused != null) { return mFocused.findFocus(); } return null; }

View的findFocus方法
/** * Find the view in the hierarchy rooted at this view that currently has * focus. * * @return The view that currently has focus, or null if no focused view can *be found. */ public View findFocus() { return (mPrivateFlags & PFLAG_FOCUSED) != 0 ? this : null; }

View的hasFocus()方法和isFocused()方法对比
Stackoverflow解释来了:
hasFocus() is different from isFocused(). hasFocus() == true means that the View or one of its descendants is focused. If you look closely, there’s a chain of hasFocused Views till you reach the View that isFocused.
/** * Returns true if this view has focus itself, or is the ancestor of the * view that has focus. * * @return True if this view has or contains focus, false otherwise. */ @ViewDebug.ExportedProperty(category = "focus") public boolean hasFocus() { return (mPrivateFlags & PFLAG_FOCUSED) != 0; }/** * Returns true if this view has focus * * @return True if this view has focus, false otherwise. */ @ViewDebug.ExportedProperty(category = "focus") public boolean isFocused() { return (mPrivateFlags & PFLAG_FOCUSED) != 0; }

接着,如果mView.findFocus()方法返回的mFocused不为空,说明找到了当前获取焦点的view(mFocused),接着focusSearch会把direction(遥控器按键按下的方向)作为参数,找到特定方向下一个将要获取焦点的view,最后如果该view不为空,那么就让该view获取焦点。
我们来看一下focusSearch方法的源码以及具体实现。
@Override public View focusSearch(View focused, int direction) { if (isRootNamespace()) { // root namespace means we should consider ourselves the top of the // tree for focus searching; otherwise we could be focus searching // into other tabs.see LocalActivityManager and TabHost for more info return FocusFinder.getInstance().findNextFocus(this, focused, direction); } else if (mParent != null) { return mParent.focusSearch(focused, direction); } return null; }

focusSearch其实是一层一层地网上调用父View的focusSearch方法,直到当前view是根布局(isRootNamespace()方法),通过注释可以知道focusSearch最终会调用DecorView的focusSearch方法。而DecorView的focusSearch方法找到的焦点view是通过FocusFinder来找到的。
FocusFinder是什么?
根据给定的按键方向,通过当前的获取焦点的View,查找下一个获取焦点的view这样算法的类。焦点没有被拦截的情况下,Android焦点的查找最终都是通过FocusFinder类来实现的。
Android焦点事件分发与传递机制

文章图片

FocusFinder是如何通过findNextFocus方法寻找焦点的?
public final View findNextFocus(ViewGroup root, View focused, int direction) { return findNextFocus(root, focused, null, direction); }private View findNextFocus(ViewGroup root, View focused, Rect focusedRect, int direction) { View next = null; if (focused != null) { next = findNextUserSpecifiedFocus(root, focused, direction); } if (next != null) { return next; } ArrayList< View> focusables = mTempList; try { focusables.clear(); root.addFocusables(focusables, direction); if (!focusables.isEmpty()) { next = findNextFocus(root, focused, focusedRect, direction, focusables); } } finally { focusables.clear(); } return next; }private View findNextFocus(ViewGroup root, View focused, Rect focusedRect, int direction, ArrayList< View> focusables) { if (focused != null) { if (focusedRect == null) { focusedRect = mFocusedRect; } // fill in interesting rect from focused focused.getFocusedRect(focusedRect); root.offsetDescendantRectToMyCoords(focused, focusedRect); } else { if (focusedRect == null) { focusedRect = mFocusedRect; // make up a rect at top left or bottom right of root switch (direction) { case View.FOCUS_RIGHT: case View.FOCUS_DOWN: setFocusTopLeft(root, focusedRect); break; case View.FOCUS_FORWARD: if (root.isLayoutRtl()) { setFocusBottomRight(root, focusedRect); } else { setFocusTopLeft(root, focusedRect); } break; case View.FOCUS_LEFT: case View.FOCUS_UP: setFocusBottomRight(root, focusedRect); break; case View.FOCUS_BACKWARD: if (root.isLayoutRtl()) { setFocusTopLeft(root, focusedRect); } else { setFocusBottomRight(root, focusedRect); break; } } } }private View findNextUserSpecifiedFocus(ViewGroup root, View focused, int direction) { // check for user specified next focus View userSetNextFocus = focused.findUserSetNextFocus(root, direction); if (userSetNextFocus != null & & userSetNextFocus.isFocusable() & & (!userSetNextFocus.isInTouchMode() || userSetNextFocus.isFocusableInTouchMode())) { return userSetNextFocus; } return null; }

FocusFinder类通过findNextFocus来找焦点的。一层一层往寻找,后面会执行findNextUserSpecifiedFocus()方法,这个方法会执行focused(即当前获取焦点的View)的findUserSetNextFocus方法,如果该方法返回的View不为空,
且isFocusable = true & & isInTouchMode() = true的话。
FocusFinder找到的焦点就是findNextUserSpecifiedFocus()返回的View。
findNextFocus会优先根据XML里设置的下一个将获取焦点的View的ID值来寻找将要获取焦点的View。
View findUserSetNextFocus(View root, @FocusDirection int direction) { switch (direction) { case FOCUS_LEFT: if (mNextFocusLeftId == View.NO_ID) return null; return findViewInsideOutShouldExist(root, mNextFocusLeftId); case FOCUS_RIGHT: if (mNextFocusRightId == View.NO_ID) return null; return findViewInsideOutShouldExist(root, mNextFocusRightId); case FOCUS_UP: if (mNextFocusUpId == View.NO_ID) return null; return findViewInsideOutShouldExist(root, mNextFocusUpId); case FOCUS_DOWN: if (mNextFocusDownId == View.NO_ID) return null; return findViewInsideOutShouldExist(root, mNextFocusDownId); case FOCUS_FORWARD: if (mNextFocusForwardId == View.NO_ID) return null; return findViewInsideOutShouldExist(root, mNextFocusForwardId); case FOCUS_BACKWARD: { if (mID == View.NO_ID) return null; final int id = mID; return root.findViewByPredicateInsideOut(this, new Predicate< View> () { @Override public boolean apply(View t) { return t.mNextFocusForwardId == id; } }); } } return null; }

焦点事件分发步骤:
  • DecorView会调用dispatchKey一层一层进行焦点的分发,如果dispatchKeyEvent方法返回true的话,那么焦点或者按键事件就不会往下分发了。
  • 如果你想拦截某个子View,对其设置OnKeyListener进行焦点的拦截。
  • 如果焦点没有被拦截的话,那么焦点就会交给系统来处理,还是会继续分发,直到找到那个获取焦点的View
  • Android底层先会记录按键的方向,后面DecorView会一层一层往下调用findFocus方法找到当前获取焦点的View
  • 后面系统又会根据按键的方向,执行focusSearch方法来寻找下一个将要获取焦点的View
  • focusSearch内部其实是通过FocusFinder来查找焦点的。FocusFinder会优先通过View在XML布局设置的下一个焦点的ID来查找焦点。
  • 最终如果找到将要获取焦点的View,就让其requestFocus。如果请求无效,将其放在onWindowFocusChanged()这个方法中去请求。这是在Activity寻找到焦点的时候。
【Android焦点事件分发与传递机制】我的前一篇文章,主要是介绍了TouchEvent事件分发机制,省略了焦点分发传递机制的代码,这篇文章与此相反。如果将两个结合起来,太繁杂,冗长了。分开反而有利于您的理解。至此,事件分发机制,你也了解的差不多了,給个粉吧!



    推荐阅读