标签:托管 nts cti poi 根据 如何 col rop ldb
控件树中的输入事件派发是由ViewRootImpl为起点,沿着控件树一层一层传递给目标控件,最终再回到ViewRootImpl的一个环形过程。这一过程发生在创建ViewRootImpl的主线程之上,但是却独立于ViewRootImpl.performTraversals()之外,就是说输入事件的派发并不依赖于ViewRootImpl的"心跳"作为动力,而是有它自己的动力源泉。经过第5章的学习可以知道,这一动力源泉来自用于构建InputEventReceiver的Looper,当一个输入事件被派发给ViewRootImpl所在的窗口时,Looper会被唤醒并触发InputEventReciever.onInputEvent()回调,控件树的输入事件派发便起始于这一回调。
在正式讨论派发过程之前,首先需要讨论对派发过程有着决定性影响的两个概念——触摸模式以及焦点。
为了同时支持键盘和触摸这两种模式,必须清楚这两种操作模式的区别与共同点。可以从焦点的角度讨论这一问题。以一个拥有若干项的菜单为例:
二者也有共同点,例如一个文本框,无论在哪种操作方式下,它都可以获得焦点以接受用户的输入。也就是说可以获取焦点的控件分为两类:
触摸模式(TouchMode)正是为管理二者的差异而引入的概念,Android通过进入或退出触摸模式实现在二者之间的无缝切换。在非触摸模式下,文本框、按钮、菜单项等都可以获取焦点,并且可以通过方向键使得焦点在这些控件之间游走。而在进入触摸模式后,某些控件如菜单项、按钮将不再可以保持或获取焦点,而文本框则仍然可以保持或获取焦点。
触摸模式是一个系统级的概念,就是说会对所有窗口产生影响。系统是否处于触摸模式取决于WMS中的一个成员变量mInTouchMode,而确定是否进入或者退出触摸模式则取决于用户对某一个窗口所执行的操作。
导致退出触摸模式的操作有:
而进入触摸模式的操作只有一个,就是用户在窗口上进行了点击操作。
窗口的ViewRootImpl会识别上述操作,然后通过WMS的接口setInTouchMode()设置WMS.mInTouchMode使得系统进入或退出触摸模式。而当其他窗口进行relayout操作时会在WMS.relayoutWindow()的返回值中添加或删除RELAYOUT_RES_IN_TOUCH_MODE标记使得它们得知系统目前的操作模式。
注意:只有拥有ViewRootImpl的窗口才能影响触摸模式,或对触摸模式产生响应。通过WMS的接口直接创建的窗口必须手动地维护触摸模式。
和第5章所讨论的窗口焦点类似,控件的焦点影响了按键事件的派发。另外,控件的焦点还影响了控件的表现形式,拥有焦点的控件往往会高亮显示以区别其他控件。
控件获取焦点的方式有很多种,例如从控件树中按照一定策略查找到某个控件并使其获得焦点,或者用户通过方向键选择某个控件使其获得焦点等。而最基本的方式是通过View.requestFocus()。本节将通过介绍View.requestFous()的实现原理揭示控件系统管理焦点的方式。
View.requestFocus()的实现有两种,即View和ViewGroup的实现是不同的。当实例是一个View时,表示期望此View能够获取焦点。而当实例是一个ViewGroup时,则会根据一定的焦点选择策略选择其一个子控件或ViewGroup本身作为焦点。本小节将首先讨论实例是一个View时的情况以揭示控件系统管理焦点的方式,随后再讨论ViewGroup下requestFocus()方法的实现。
参考requestFocus()代码:
[View.java-->View.requestFocus()]
public final boolean requestFocus() { /*调用requestFoscus()的一个重载。View.FOCUS_DOWN表示焦点的寻找方向。 当本控件是一个ViewGroup时将会从左上角开始沿着这个方向查找可以获取焦点的子控件。 不过在本例只讨论控件是一个View时的情况,此时该参数并无任何效果*/ return requestFocus(View.FOCUS_DOWN); } public final boolean requestFocus (int direction) { /*继续调用另外一个重载,新的重载中增加了一个Rect作为参数。此Rect表示了上一个焦点控件的区域。 它表示从哪个位置开始沿着direction所指定的方向查找焦点控件。 仅当本控件是ViewGroup时此参数才有意义*/ return requestFocus(direction, null); } public boolean requestFocus(int direction, Rect previouslyFocusedRect) { /*requestFocus()的这一重载便是View和Viewgroup分道扬镳的地方。 requestFocusNoSearch()方法的意义就是无须查找,直接使本控件获取焦点*/ return requestFocusNoSearch(direction, previouslyFocusedRect); } private boolean requestFocusNoSearch(int direction, Rect previouslyFocusedRect) { //首先检查一下此控件是否符合拥有焦点的条件 //①首先,此控件必须是Focuaable的。可以通过View.setFocusable()方法设置控件是否focusable if ((mViewFlags & FOCUSABLE_MASK) !=FOCUSABLE || (mViewFlags & VISIBILITY_MASK) != VISIBLE) { return false; } //②再者,如果系统目前处于触摸模式,则要求此控件必须可以在触摸模式下拥有焦点 if (isInTouchMode() && (FOCUSABLE_IN_TOUCH_MODE != (mViewFlags & FOCUSABLE_IN_TOUCH_MODE))) { return false; } //③最后,如果任一父控件的DescendantFocusability取值为FOCUS_BLOCK_DESCENDANTS时,阻止此控件获取焦点。 hasAncestorThatBlocksDescendantFocus()会沿着控件树一路回溯到整个控件树的根控件并逐一检查DescendantFocusability特性的取值*/ if (hasAncestorThatBlocksDescendantFocus()) { return false; } //④最后调用handleFocusGainlnternal()使此控件获得焦点 handleFocusGainlnternal(direction, previouslyFocusedRect); return true; }
private boolean hasAncestorThatBlocksDescendantFocus() { final boolean focusableInTouchMode = isFocusableInTouchMode(); ViewParent ancestor = mParent; while (ancestor instanceof ViewGroup) { final ViewGroup vgAncestor = (ViewGroup) ancestor; if (vgAncestor.getDescendantFocusability() == ViewGroup.FOCUS_BLOCK_DESCENDANTS || (!focusableInTouchMode && vgAncestor.shouldBlockFocusForTouchscreen())) { return true; } else { ancestor = vgAncestor.getParent(); } } return false; }
因此控件能否获取焦点的策略如下:
接下来分析View.handleFocusGainlntemal()。
[View.java-->View.handleFocusGainlntemal(int direction, Rect previouslyFocusedRect)]
void handleFocusGainlnternal(int direction, Rect previouslyFocusedRect) { if((mPrivateFlags & PFLAG_FOCUSED) == 0) { //①把PFLAG_FOCUSED标记加入mPrivateFlags中。这便表示此控件已经拥有焦点了 mPrivateFlags |= PFLAG_FOCUSED; /*②将这一变化通知其父控件。这一操作的主要目的是保证控件树中只有一个控件拥有焦点,并且在ViewRootImpl中触发一次“遍历”以便对控件树进行重绘*/ if (mParent != null) { mParent.requestChildFocus(this, this); } /*③通知对此控件焦点变化感兴趣的监听者。在这个方法中,View.onFocusLose()、OnFocusChangeListener.onFocusChange()都会被调用。 另外,控件焦点决定了输入法的输入对象,因此InputMethodManager的focusIn()和focusOut()也会在这里被调用以更新输入法的状态*/ onFocusChanged (true, direction, previouslyFocusedRect); //④更新控件的Drawable状态。这将使得控件在随后的绘制中得以高亮显示 refreshDrawableState(); ... } }
接下来讨论mParent.requestChildFocus()的实现。PFLAG_FOCUSED是一个控件是否拥有焦点的最直接体现,然而这并不是焦点管理的全部。这一标记仅仅体现了焦点在个体级别上的特性,而mParent.requestChildFocus()则体现了焦点在控件树的级别上的特性。
mParent.requestChildFocus()是一个定义在ViewParent接口中的方法,其实现者为ViewGroup及ViewRootImpl。ViewGroup实现的目的之一是用于将焦点从上一个焦点控件手中夺走,即将PFLAG_FOCUSED标记从控件的mPrivateFlags中移除。而另一个目的则是将这一操作继续向控件树的根部进行回溯,直到ViewRootImpl,ViewRootImpl的requestChildFocus()会引发一次"遍历"。
参考ViewGroup.requestChildFocus()方法的实现:
[ViewGroup.java-->ViewGroup.requestChildFocus(View child, View focused)]
public void requestChildFocus(View child, View focused) { //①如果上一个焦点控件就是这个ViewGroup,则通过调用View.unFocus()将PLFAG_FOCUSED标记移除,以释放焦点 super.unFocus(); if (mFocused != child) { /*②如果上一个焦点控件在这个ViewGroup所表示的控件树之中,即mFocused不为null,则调用mFocused.unFocus()以释放焦点*/ if (mFocused != null) { mFocused.unFocus(); } /*3.设置mFocused成员为child。注意child参数不一定是实际拥有焦点的控件。而是此ViewGroup的直接子控件,同时它是实际拥有焦点的控件的父控件*/ mFocused = child; } if (mParent != null) { //4.将这一操作继续向控件树的根部回溯。注意child参数是此ViewGroup,而不是实际拥有焦点的focused mParent.requestChildFocus(this, focused); } }
最后会调用到ViewRootImpl.requestChildFocus
@Override public void requestChildFocus(View child, View focused) { if (DEBUG_INPUT_RESIZE) { Log.v(mTag, "Request child focus: focus now " + focused); } checkThread(); scheduleTraversals(); }
新的焦点体系的建立过程是通过在ViewGroup.requestChildFocus()方法的回溯过程中进行mFocused=child这一赋值操作完成的。当回溯完成后,mFocused=child将会建立起一个单向链表,使得从根控件开始通过mFocused成员可以沿着这一单向链表找到实际拥有焦点的控件,即实际拥有焦点的控件位于这个单向链表的尾端,如图6-22所示。
而旧有的焦点体系的销毁过程则是通过在回溯过程中调用mFocused.unFocus()完成的。unFocus()方法有ViewGroup和View两种实现。首先看一下ViewGroup.unFocus()的实现:
[ViewGroup.java-->ViewGroup.unFocus()]
void unFocus() { if (mFocused == null) { /*如果mFocused为空,则表示此ViewGroup位于mFocused单向链表的尾端,即此ViewGroup是焦点的实际拥有者,因此调用View.unFocus()使此ViewGroup放弃焦点*/ super.unFocus(); } else { //否则将unFocus()传递给链表的下一个控件 mFocused.unFocus(); //最后将mFocused设置为null mFocused = null; } }
可见ViewGroup.unFocus()将unFocus()调用沿着mFocused所描述的链表沿着控件树向下遍历,直到焦点的实际拥有者。焦点的实际拥有者会调用View.unFocus(),它会将PFLAG_FOCUSED移除,当然也少不了更新DrawableState以及onFocusChanged()方法的调用。
[View.java-->View.unFocus()]
void unFocus() { if ((mPrivateFlags & PFLAG_FOCUSED) != 0) { mPrivateFlags &= ~PFLAG_FOCUSED; onFocusChanged(false, 0, null); refreshDrawableState(); if (AccessibilityManager.getInstance(mContext).isEnabled()) { notifyAccessibilityStateChanged(); } } }
以图6-22的控件树的焦点状态为例来描述旧有焦点体系的销毁以及新焦点体系的建立过程。
当View2-1-1通过View.requestFocus()尝试获取焦点时,首先会将PLFAG_FOCUSED标记加入其mPrivateFlags成员中以声明其拥有焦点。然后调用ViewGroup2-1的requestChildFocus(),此时ViewGroup2-1会尝试通过unFocus()销毁旧有的焦点体系,但是由于其mFocused为null,它无法进行销毁,于是它将其mFocused设置为View2-1-1后将requestChildFocus()传递给ViewGroup2。此对ViewGroup2的mFocused指向了ViewGroup2-2,于是调用ViewGroup2-2的unFocus()进行旧有焦点体系的销毁工作。ViewGroup2-2的unFocus()将此操作传递给View2-2-2的unFocus()以移除View2-2-2的PFLAG_FOCUSED标记,并将其mFocused置为null。回到ViewGroup2的requestChildFocus()方法后,ViewGroup2将其mFocused重新指向到ViewGroup2-1。在这些工作完成后,图6-22所描述的焦点体系则变为图6-33所示。
总而言之,控件树的焦点管理分为两个部分:
另外,由于焦点的排他性,当一个控件通过requestFocus()获取焦点以创建新的焦点体系时伴随着旧有焦点体系的销毁过程。
说明:View类下有两个查询控件焦点状态的方法——isFocused()以及hasFocus(),二者的区别在于:
在图6-23中,所有灰色的控件的hasFocus()返回值都为true,而仅有View2-1-1的isFocused()返回值为true。
至此,相信读者已经对焦点的体系有了深刻理解。接下来的内容将会讨论一种稍微复杂的情况,即尝试在ViewGroup上调用reqestFocus()会发生什么。
在本节开始时曾经讨论了获取焦点的最基本方式是View.requestFocus()。如果调用此方法的实例是一个控件(非ViewGroup),其意义非常明确,即希望此控件能够获取焦点。而倘若调用此方法的实例是一个ViewGroup时又当如何呢?本节将讨论这一问题。
ViewGroup重写了View.requestFocus(int direction,Rect previouslyFocusedRect)以应对这种情况。参考如下代码:
[ViewGroup.java-->ViewGroup.requestFocus(int direction, Rect previouslyFocusedRect)]
public boolean requestFocus (int direction, Rect previouslyFocusedRect) { //①首先获取ViewGroup的DescendantFocusability将性的取值 int descendantFocusability = getDescendantFocusability(); //根据不同的DescendantFocusability特性,requestFocus()会产生不同的效果 switch (descendantFocusability) { case FOCUS_BLOCK_DESCENDANTS: /*②FOCUS_BLOCK_DESCENDANTS: ViewGroup将会阻止所有子控件获取焦点,于是调用 view.requestFocus()尝试自己获取焦点*/ return super.requestFocus(direction, previouslyFocusedRect); case FOCUS_BEFORE_DESCENDANTS: { /*③FOCUS_BEFORE_DESCENDANTS: viewGroup将有优先于子控件获取焦点的权利。因此会首 先调用View.requestFocus()尝试自己获取焦点,倘若自己不满足获取焦点的条件则通过调用onRequestFocusInDescendants()方法将获取焦点的请求转发给子控件*/ final boolean took = super.requestFocus(direction, previouslyFocusedRect); return took ? took : onRequestFocusInDescendants(direction, previouslyFocusedRect); } case FOCUS_AFTER_DESCENDANTS: { /*④FOCUS_AFTER_DESCENDANTS:子控件将有优先于此ViewGroup获取焦点的权利。因此会首 先调用onRequestFocusInDescendants()尝试将获取焦点的请求转发给子控件。倘若所有子控件都无法获取焦点,再调用View.requestFocus()尝试自己获取焦点*/ final boolean took = onRequestFocusInDescendants(direction, previouslyFocusedRect); return took ? took : super.requestFocus(direction, previouslyFocusedRect); } default: ...//抛出异常 } }
可见,在ViewGroup上调用requestFocus()方法会根据其DescendantsFocusability特性的不同而产生三种可能的结果。开发者可以通过ViewGroup.setDescendantFocusability()方法修改这一特性。
在FOCUS_BLOCK_DESCENDANTS特性下,ViewGroup将会拒绝所有子控件获取焦点,此时调用ViewGroup.requestFocus()会产生唯一的结果,即ViewGroup会尝试自己获取焦点。此时的流程与View.requestFocus()没有什么区别。
而在其他两种特性下调用ViewGroup.requestFocus()则会产生View.requestFocus()与ViewGroup.onRequestFocuslnDescendants()两种可能的结果,不同的特性下二者的优先级不同。onRequestFocuslnDescendants()负责遍历其所有子控件,并将requestFocus()转发给它们。
参考其实现:
[ViewGroup.java-->ViewGroup.onRequestFocusInDescendants()]
protected boolean onRequestFocusInDescendants(int direction, Rect previouslyFocusedRect) { /*此方法的目的是按照direction参数所措述的方向在子控件列表中依次尝试使其获取焦点,这里direction 所描述的方向并不是控件在屏幕上的位置,而是它们在mChildren列表中的位置因此direction仅有 按照索引递增(FOCUS_FORWARD)或递减两种方向可选*/ int index, increment, end, count = mChildrenCount; if (direction & FOCUS_FORWARD) != 0) { index = 0; increment = 1; end = count; } else { index = count - 1; increment = -1; end = -1; } final View[] children = mChildren; for (int i = index; i != end; i += increment) { View child = children[i]; //首先子控件必须是可见的 if ((child.mViewFlags & VISIBILITY_MASK) == VISIBLE) { //调用子控件的requestFocus(),如果子控件获取了焦点,则停止继续查找 if (child.requestFocus(direction, previouslyFocusedRect)) { return true; } } } return false; }
ViewGroup.onRequestFocuslnDescendants()其实是一种最简单的焦点查找的算法。它按照direction所指定的方向,在mChildren列表中依次调用子控件的requestFocus()方法,直到有一个子控件获取了焦点。另外,需要注意子控件有可能也是一个ViewGroup,此时将会重复本节所讨论的工作,直到找到一个符合获取焦点条件的控件并使其获得焦点为止。
至此,焦点管理中的requestFocus()已经介绍完成。与其相对的还有一个clearFocus()方法用于清除控件的焦点。requestFocus()与clearFocus()作为互为反作用的一对双胞胎,它们的执行方式与requestFocus()是一致的。只不过它的执行过程是销毁现有的焦点体系而已(移除PFLAG_FOCUSED以及将mFocused设置为null)。需要注意的是,在现有的焦点体系被销毁后,它还会调用ViewRootImpl.mView.requestFocus()方法设置一个新的焦点。根据ViewGroup.requestFocus()的工作原理,这一行为会在控件树中寻找一个合适的控件并将焦点给它。而如果所选中的控件正好是执行clearFocus()的控件,那么它会重新获得焦点。
ViewGroup.clearFocus
@Override public void clearFocus() { if (DBG) { System.out.println(this + " clearFocus()"); } if (mFocused == null) { super.clearFocus(); } else { View focused = mFocused; mFocused = null; focused.clearFocus(); } }
View.clearFocus
public void clearFocus() { if (DBG) { System.out.println(this + " clearFocus()"); } clearFocusInternal(null, true, true); }
View.clearFocusInternal
void clearFocusInternal(View focused, boolean propagate, boolean refocus) { if ((mPrivateFlags & PFLAG_FOCUSED) != 0) { mPrivateFlags &= ~PFLAG_FOCUSED; if (propagate && mParent != null) { mParent.clearChildFocus(this); } onFocusChanged(false, 0, null); refreshDrawableState(); if (propagate && (!refocus || !rootViewRequestFocus())) { notifyGlobalFocusCleared(this); } } }
View.rootViewRequestFocus
boolean rootViewRequestFocus() { final View root = getRootView(); return root != null && root.requestFocus(); }
重新获取让view树获取一个焦点。
ViewGroup.clearChildFocus
@Override public void clearChildFocus(View child) { if (DBG) { System.out.println(this + " clearChildFocus()"); } mFocused = null; if (mParent != null) { mParent.clearChildFocus(this); } }
ViewRootImpl.clearChildFocus
@Override public void clearChildFocus(View child) { if (DEBUG_INPUT_RESIZE) { Log.v(mTag, "Clearing child focus"); } checkThread(); scheduleTraversals(); }
说明:ViewGroup.requestFocus()方法还有另外一个重要的用处,就像View.clearFocus()最后会设置新的焦点一样,当控件树被添加加到ViewRootImpl之后也会调用ViewRootImpl.mView.requestFocus()设置初始的焦点。
接下来讨论关于焦点的另外一个重要话题,即下一个焦点控件的查找。
当一个控件获取焦点之后,用户往往会通过按下方向键移动焦点到另一个控件上。这时控件系统需要在控件树中指定的方向上寻找距离当前控件最近的一个控件,并将焦点赋予它。与ViewGroup.onRequestFocusInDescendants()方法按照控件在mChildren数组中的顺序查找不同,这一查找依赖于控件在窗口中的位置。这一工作由View.focusSearch()方法完成。参考代码如下:
[View.java-->View.focusSearch(int direction)]
public View focusSearch(int direction) { if (mParent != null) { //查找工作会交给父控件完成 return mParent.focusSearch(this, direction); } else { /*如果控件没有父控件就直接返回null,毕竟一个控件没有添加到控件树中查找下一个焦点是没有 意义的*/ return null; } }
View.focusSearch()会调用父控件的ViewGroup.focusSearch(View focused,int direction)方法,由父控件决定下一个焦点控件是谁。参考其在ViewGroup中的实现:
[ViewGroup.java-->ViewGroup.focusSearch(View focused, int direction)]
public View focusSearch(View focused, int direction) { if (isRootNamespace()) { /*①如果isRootNamespace()返回true,则表示这是一个根控件。此时ViewGroup拥有整个控件 树,因此它是负责焦点查找的最合适的人选。它使用了FocusFinder工具类进行焦点查找*/ return FocusFinder.getlnstance().findNextFocus(this, focused, direction); } else if (mParent != null) { //②如果这不是根控件,则继续向控件树的根部回溯 return mParent.focusSearch(focused, direction); } return null; }
如果此ViewGroup不是根控件,则继续向控件树的根部回溯,一直回溯到根控件后,便使用FocusFinder的findNextFocus()方法查找下一个焦点。这个方法的三个参数的意义如下:
参考findNextFocus()方法的代码:
[FocusFinder.java-->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(); //③将控件树中所有可以获取焦点的控件存储到focusables列表中。后续的将会在这个列表中进行查找 root.addFocusables(focusables, direction); if (!focusables.isEmpty()) { //④调用findNextFocus()的另一个重载完成查找 next = findNextFocus(root, focused, focusedRect, direction, focusables); } } finally { focusables.clear(); } return next; }
FocusFinder.findNextFocus()会首先尝试通过findNextUserSpecifiedFocus()获取由开发者设置的下一个焦点控件。有时候控件系统内置的焦点查找算法并不能满足开发者的需求,因此开发者可以通过View.setNextFocusXXXId()方法设置此控件的下一个可获取焦点的控件的Id。其中XXX可以是Left、Right、Top、Bottom和Forward,分别用来设置不同方向下的下一个焦点控件。findNextUserSpecifiedFocus()会在focused上调用getNextFocusXXXld()方法获取对应的控件并返回。
倘若开发者在指定方向上没有设置下一个焦点控件,则findNextUserSpecifiedFocus()方法会返回null,findNextFocus()会使用内置的搜索算法进行查找。这个内置算法会首先将控件树中所有可以获取焦点的控件添加到一个名为focusables的列表中,并以这个列表作为焦点控件的候迭集合。这样做的目的并不仅仅是提高效率,更重要的是这个列表打破了控件在控件树中的层次关系。它在一定程度上体现了焦点查找的一个原则,即控件在窗口上的位置是唯一查找依据,与控件在控件树中的层次无关。
最后调用的findNextFocus()的另一个重载将在focusables列表中选出下一个焦点控件。
参考以下实现:
[FocusFinder.java-->FocusFinder.findNextFocus()]
private View findNextFocus(ViewGroup root, View focused, Rect focusedRect, int direction, ArrayList<View> focusables) { //①首先需要确定查找的起始位置 if (focused != null) { ... /*当focused不为null时,起始位置即focused所在的位置。View.getFocusedRect()所返回 的并不是控件的mLeft、mTop、mRight、mBottom。因为Scroll的存在它们并不能反映控件的 真实位置。View.getFocusedRect()会将Scroll所产生的偏移考虑在内,但是Tranformation(如 setScaledX()等设置)并没有计算在内,因此它们并不会影响焦点查找的结果*/ focused.getFocusedRect(focusedRect); /* View.getFocusedRect()所返回的结果基于View本身的坐标系。为了使得控件之间的位置可以 比较,必须将其转换到根控件所在的多坐标系中*/ root.offsetDescendantRectToMyCoords(focused, focusedRect); } else { if (focusedRect == null) { /*当focusedRect为null时,表示查找在指定方向上的第一个可以获取焦点的控件。 此时会以根控件的某个角所在位置作为起始位置。例如对Left和Up两个方向来说,起始位置会被 设置为根控件的右下角这个点,而对Right和Bottom来说,起始位置将会是根控件的左上角*/ ... } } //接下来便会根据不同的方向选择不同的查找算法 switch (direction) { case View.FOCUS_FORWARD: case View.FOCUS_BACKWARD: /*②对FOCUS_FORWARD/BACKWARD 来说将会选择相对位置进行查找。这种查找与控件位置无关, 它会选择focusables列表中索引近邻focused的控件作为查找结果*/ // 首先会把focusables列表按照内置的排序算法进行排序,排序算法是按照控件焦点区域上左下右的优先级进行递增排序的, // 先获取当前focused在focusables中的index,然后根据查找方向在列表中获取下一个或上一个能获取焦点的view即可 return findNextFocusInRelativeDirection(focusables, root, focused, focusedRect, direction); case View.FOCUS_UP: case View.FOCUS_DOWN: case View.FOCUS_LEFT: case View.FOCUS_RIGHT: //③对于UP、DOWN、LEFT、Right 4个方向会根据控件的实际位置进行查找 return findNextFocusInAbsoluteDirection(focusables, root, focused, focusedRect, direction); default: throw new IllegalArgumentException("Unknown direction: " + direction); } }
在这个方法中首先确定了查找的起点位置,然后根据direction参数的取值选择两种不同的查找策略。为FORWARD和BACKWARD两种查找方向所选择的查找策略比较简单,即首先确定focused所表示的控件在focusables列表中的索引index,然后选择在focusable列表中索引为index+1或index-1的两个控件之一作为查找结果。因此使用这两种方向进行查找的结果与ViewGroup.onRequestFocusInDescendants()类似,它反映了控件在ViewGroup.mChildren列表中的顺序。而对于其他4种方向的查找则复杂得多。参考findNextFocusInAbsoluteDirection()的代码:
[FocusFinder.java-->FocusFinder.findNextFocusInAbsoluteDirection()]
View findNextFocusInAbsoluteDirection(ArrayList<View> focusables, ViewGroup root, View focused, Rect focusedRect, int direction) { //①首先确定第一个最佳候选控件的位置。focusedRect即查找的起始位置 mBestCandidateRect.set(focusedRect); ... //closest表示在指定的方向上距离起始位置最接近的一个控件 View closest = null; int numFocusables = focusables.size(); //遍历focusables列表进行查找 for (int i = 0; i < numFocusables; i++) { View focusable = focusables.get(i); /*既然是查找下一个焦点控件,那么已经拥有焦点的控件自然不能算作候选者。另外根控件也不能作 为候选对象*/ if (focusable == focused || focusable == root) continue; /*②与获取起始位置一样,获取候选控件的位置。将其位置转换到根控件的坐标系中,以便能够与起始位置进行比较*/ focusable.getFocusedRect(mOtherRect); root.offsetDescendantRectToMyCoords(focusable, mOtherRect); /*③通过ieBetterCandidate()方法比较现有的mBestCandidateRect与候选控件的位置。倘若 候选控件的位置更佳,则设置候选控件为closest,设置候选控件的位置为mBesetCandidateRect。 如此往复,当所有候选控件都经过比较之后,closest便是最后的查找结果。*/ if (isBetterCandidate(direction, focusedRect, mOtherRect, mBestCandidateRect)) { mBestCandidateRect.set(mOtherRect); closest = focusable; } } //返回closest作为下一个焦点控件 return closest; }
这个方法的实现非常直观。在遍历focusables列表的过程中使用isBetterCandidate()方法不断地将mBestCandidateRect与候选控件的位置进行比较,并在遍历过程中保存最佳的候选控件到closest变量中。在遍历完成后,closest即下一个焦点。整个过程与插入排序非常相似。
那么isBetterCandidate()方法又是如何确定两个位置谁更合适呢?由于其算法实现十分繁琐并且难以理解,这里直接给出其比较原则:
注意:围绕BEAM的比较中(原则2)还有更细节的原则。例如,当一个控件与另一个方向的BEAM有交集时另一个控件是更佳候选,因为前者可以通过另一个方向查找到。读者可以通过阅读FocusFinder.beamBeats()穷法学习其细节。
至此,下一个焦点控件的查找便结束了,总结其查找过程如下:
在选出下一个焦点控件之后,便可以通过调用它的requestFocus()方法将其设置为焦点控件了。
说明:相对于提供一个新的工具类FocusFinder,将查找下一个焦点的算法实现在ViewGroup中看起来是一个更加直观的做法。但是这样一来查找算法在实现过程中难免会和焦点的体系结构藕合起来。将其独立到FocusFinder工具类中使得其实现更加纯粹,而且独立于焦点的体系结构之后使得其适用范围更加广泛。例如,开发者可以通过FocusFinder.findNextFocus()获取控件A的下一个焦点控件,而此时控件A不一定需要拥有焦点。假如这一算法与控件焦点的体系结构严重藕合,这一用法将是不存在的。
在第5章关于输入系统的探讨中可以发现,按键事件与触摸事件采取了两种不同的派发策略。按键事件是基于焦点的派发,而触摸事件是基于位置的派发。控件系统中事件的派发一样采取了这两种策略。在深入讨论这两种策略在控件系统中的实现之前,首先讨论一下二者的共通内容-ViewRootImpl处理输入事件的总体流程。
第5章中介绍了输入系统的派发终点是InputEventReceiver。作为控件系统最高级别的管理者,ViewRootImpl便是InputEventReceiver的一个用户,它从InputEventReceiver中获取事件,然后将它们按照一定的流程派发给所有可能感兴趣的对象,包括View、PhoneWindow、Activity以及Dialog等。因此本节的探讨将从InputEventReceiver.onlnputEvent()开始。
在ViewRootImpl.setView()中,新的窗口被创建之后,ViewRootImpl使用WMS分配的InputChannel以及当前线程的Looper一起创建了InputEventReceiver的子类WindowlnputEventReceiver的一个实例,并将其保存在ViewRootImpl.mInputEventReceiver成员之中。这标志着从设备驱动到本窗口的输入事件通道的正式建立。至此每当有输入事件到来时,ViewRootImpl都可以通过WindowlnputEventReceiver.onInputEvent()回调得到这个事件并进行处理。也就是说onInputEvent是在主线程中调用的。参考其实现:
[ViewRootImpl.java-->WindowlnputEventReceiver.onInputEvent()]
public void onInputEvent(InputEvent event) { //通过enqueueInputEvent将输入事件入队,注意第四个参数为true enqueueInputEvent (event, this, 0, true); }
再看enqueueInputEvent()的实现:
[ViewRootImpl.java-->ViewRootImpl.enqueueInputEvent()]
void enqueueInputEvent(InputEvent event, InputEventReceiver receiver, int flags, boolean processImmediately) { /*①将InputEvent对应的InputEventReceiver封装为一个QueuedInputEvent。 QueuedInputEvent将是输入事件在ViewRootImpl中的存在形式*/ QueuedInputEvent q = obtainQueuedInputEvent(event, receiver, flags); /*②将新建的QueuedInputEvent追加到mFirstPendingInputEvent所表示的一个单向链表的尾部。 ViewRootImpl将会沿着链表从头至尾地逐个处理输入事件*/ QueuedInputEvent last = mFirstPendingInputEvent; if (last == null) { mFirstPendingInputEvent = q; } else { while(last.mNext != null) { last = last.mNext; } last.mNext = q; } if (processImmediately) { //③倘若第四个参数为true,则直接在当前线程中开始对输入事件的处理工作 doProcessInputEvents(); } else { //④否则将处理事件的请求发送给主线程的Handler,随后进行处理 scheduleProcessInputEvents(); } }
此方法揭示了ViewRootImpl管理输入事件的方式。同InputDispatcher一样,在ViewRootImpl中也存在着一个输入事件队列mFirstPendingInputEvent。输入事件在队列中以QueuedInputEvent的形式存在。QueuedInputEvent保存了输入事件的实例、接收事件的InputEventReceiver,以及一个next成员用于指向下一个QueuedInputEvent。
注意此方法的第四个参数processImmediately。对于从InputEventReceiver收到的正常事件来说,此参数永远为true,即入队的输入事件会立刻得到执行。而当此参数为false时,则会将事件的处理发送到主线程的Handler中随后处理。推迟事件处理的原因是什么呢?原来ViewRootImpl会将某些类型的输入事件转换成为另外一种输入事件,并将新的输入事件入队enqueueInputEvent()。由于此时仍处于旧有事件的处理过程中,倘若立即处理新事件会导致输入事件的递归处理,即前一个事件尚未处理完毕时开始了新的事件处理流程。为了避免这一情况,需要在入队时将processImmediately参数设置为false,在一切都完成之后再来处理新的事件。
说明:ViewRootImpl转换输入事件的一个例子是轨迹球(TrackBall)事件的处理。操作轨迹球时在驱动和输入系统层面会产生MotionEvent。ViewRootImpl根据MotionEvent.getSource()得知这是一个来自轨迹球的事件后会根据其事件的数据将其转换为方向键(DPAD)的KeyEvent,并将其通过enqueueInputEvent()入队随后处理。这也是轨迹球的实际效果与方向键(或五向导航键)一致的原因。
接下来看doProcessInputEvent()的实现:
[ViewRootImpl.java-->ViewRootImpl.doProcesslnputEvent()]
void doProcessInputEvents() { // 遍历整个输入事件队列,逐个处理这些事件 // 判断mCurrentInputEvent是否为null的原因是,如果最后没有调用finishInputEvent()就不会再收到新的输入事件, // 也就是说当调用finishInputEvent()里会在从MessageQueue中取出一个输入事件,再次回调用onInputEvent(), // 而此时还没有执行到mCurrentInputEvent=null,所以加入这个判断就不会发生递归调用的错误。 while (mCurrentInputEvent == null && mFirstPendingInputEvent != null) { QueuedInputEvent q = mFirstPendingInputEvent; mFirstPendingInputEvent = q.mNext; q.mNext = null; //①正在处理的输入事件会被保存为mCurrentInputEvent mCurrentInputEvent = q; //②deliverInputEvent()方法将会完成单个事件的整个处理流程 deliverInputEvent(q); } }
显而易见,doProcessInputEvents()方法不动则已,一动则一发不可收拾,直到将输入事件队列中的所有事件处理完毕之前不会退出,换言之在所有输入事件处理完成之前它不会放下对于主线程的占用权。这种看似粗犷的处理方式其实大有深意。ViewRootImpl最繁重的工作performTraversals()"遍历"就发生在主线程之上,而引发这一"遍历"操作的最常见的原因就是在输入事件处理时修改控件的内容。doProcessInputEvents()这种粗犷的处理方式使得performTranversals()无法在单个输入事件处理后立刻得到执行,因输入事件所导致的requestLayout()或invalidate()操作会在输入事件全部处理完毕之后由一次performTranversals()统一完成。当队列中存在较多事件时这种方式所带来的效率提升是不言而喻的。
接下来分析deliverlnputEvent()的工作原理。参考代码如下:
[ViewRootImpl.java-->ViewRootImpl.deliverInputEvent()]
private void deliverInputEvent(QueuedInputEvent q) { try{ if (q.mEvent instanceof KeyEvent) { //处理按键事件 deliverKeyEvent(q); }else{ final int source = q.mEvent.getSource(); if ((source & InputDevice.SOURCE_CLASS_POINTER) != 0) { //处理触摸事件 deliverPointerEvent(q); } else if ((source & InputDevice.SOURCE_CLASS_TRACKBALL) != 0) { //处理轨迹球事件 deliverTrackballEvent(q); }else{ //处理其他Motion事件,如悬浮(HOVER)、游戏手柄等 deliverGenericMotionEvent(q); } } } finally {...} }
可以看到在deliverlnputEvent()方法中不同类型的输入事件的处理终于分道扬镳了。根据InputEvent的子类类型或Source的不同,分别用4个方法处理4种类型的事件。
由于篇幅的原因,本节将只介绍deliverKeyEvent()以及deliverPointerEvent()两个最常见的同时也是最具代表性的事件处理流程。其他类型事件的处理方式直接采用或借鉴了这两种事件处理流程中所体现的思想和流程,感兴趣的读者可以自行研究。
无论deliverlnputEvent()中分成了多少条不同的事件处理通道,应输入系统事件发送循环的要求,最终都要汇聚到一个方法中-ViewRootImpl.finishlnputEvent()。这个方法用于向InputDispatcher发送输入事件处理完毕的反馈,同时也标志着一条输入事件的处理流程的终结。
参考ViewRootImpl.finishlnputEvent()的实现:
[ViewRootImpl.java-->ViewRootImpl.finishInputEvent()]
private void finishInputEvent(QueuedInputEvent q, boolean handled) { /*倘若被完成的输入事件不是mCurrentInputEvent,则抛出异常。 ViewRootImpl不允许事件的嵌套处理*/ if (q != mCurrentInputEvent) { throw new IllegalStateException("finished input event out of order"); } //①回收输入事件并向InputDispatcher发送反馈 if (q.mReceiver != null) { /*如果rnReceiver不为null,表示这是一个来自InputDispatcher的事件,需要向InputDispatcher 发送反馈。事件实例的回收由InputEventReceiver托管完成.*/ q.mReceiver.finishInputEvent(q.mEvent, handled); } else { /*如果mReceiver为null,表示这是ViewRootImpl自行创建的事件,此时只要将事件实例回收即可。 不需要惊动InputDispatcher*/ q.mEvent.recycleIfNeededAfterDispatch(); } /*②回收不再有效的QueuedInputEvent实例。被回收的实例会组成一个以mQueuedInputEventPool为 头部的单向链表中。下次使用obtainQueuedInputEvent()时可以复用这个实例*/ recycleQueuedInputEvent(q); // 设置mCurrentInputEvent为null mCurrentInputEvent = null; //如果队列中有了新的输入事件,则重新启动输入事件的派发 if (mFirstPendingInputEvent != null) { scheduleProcessInputEvents(); } }
至此,输入事件在ViewRootImpl中从onInputEvent()开始到finishlnputEvent()终结的总体流程便终结了。不难得出输入事件在ViewRootImpl中派发的总体流程如图6-25所示。
https://blog.csdn.net/jinzhuojun/article/details/41909159
down/up,pointer down/up都是直接发送的,并不依赖vsync。
move是batched input event的处理依赖于vsync,
对于move事件就有差别了。因为触摸移动中的事件不一定要每一个都处理,因为显示也就60HZ,你如果100HZ的输入事件,全处理只会浪费计算资源。上面这条路是每当InputDispatcher有事件发过来时就会触发的,而对于move事件,系统会把一个VSync周期内的事件存为Batch,当VSync到来时一起处理。从JB开始,App对输入事件的处理是由VSync信号来驱动的。
当VSync到来时一起处理时,会把batch的move事件合成到一个MotionEvent中,其中MotionEvent的history就是VSync内的batch 的move事件。
首先查看在在哪postCallback这个CALLBACK_INPUT的。
void scheduleConsumeBatchedInput() { if (!mConsumeBatchedInputScheduled) { mConsumeBatchedInputScheduled = true; mChoreographer.postCallback(Choreographer.CALLBACK_INPUT, mConsumedBatchedInputRunnable, null); } }
mConsumedBatchedInputRunnable里会调用doConsumeBatchedInput。
void doConsumeBatchedInput(long frameTimeNanos) { if (mConsumeBatchedInputScheduled) { mConsumeBatchedInputScheduled = false; if (mInputEventReceiver != null) { Consumes all pending batched input events. Must be called on the same Looper thread to which the receiver is attached. This method forces all batched input events to be delivered immediately. mInputEventReceiver.consumeBatchedInputEvents(frameTimeNanos); } doProcessInputEvents(); } }
再查看在什么时候调用scheduleConsumeBatchedInput。
void scheduleTraversals() { if (!mTraversalScheduled) { mTraversalScheduled = true; mTraversalBarrier = mHandler.getLooper().postSyncBarrier(); mChoreographer.postCallback(Choreographer.CALLBACK_TRAVERSAL, mTraversalRunnable, null); scheduleConsumeBatchedInput(); } } final class WindowInputEventReceiver extends InputEventReceiver { public WindowInputEventReceiver(InputChannel inputChannel, Looper looper) { super(inputChannel, looper); } @Override public void onInputEvent(InputEvent event) { enqueueInputEvent(event, this, 0, true); } Called when a batched input event is pending.
The batched input event will continue to accumulate additional movement samples(收集额外的运动样本) until the recipient(接受者) calls consumeBatchedInputEvents
or an event is received that ends the batch and causes it to be consumed immediately (such as a pointer up event). @Override public void onBatchedInputEventPending() { scheduleConsumeBatchedInput(); } @Override public void dispose() { unscheduleConsumeBatchedInput(); super.dispose(); } }
从上边可以看出Choreographer.CALLBACK_INPUT处理的是batched input event,最常用的就是move事件。
那些高采样率的手机,应该是把move事件和vsync分开,move事件单独处理,120hz的采样率那么就会比原来多一倍的处理次数,一般move事件都会引发重绘操作,这就可以使得有更跟手的感觉。
标签:托管 nts cti poi 根据 如何 col rop ldb
原文地址:https://www.cnblogs.com/muouren/p/11706430.html