从Android源码的角度理解应用开发(2)-Focus机制
来源:互联网 发布:js字符串包含某个字符 编辑:程序博客网 时间:2024/05/17 11:56
前言
为什么要有Focus机制
这是因为,如果界面上有两个按钮,假设你按了回车,这时候究竟代表着你按了哪个按钮呢?这时候就需要Focus来帮忙了,因为如果一个View得到焦点,那么这个View就可以处理键盘的输入,做出回应。
两种模式
然后Android的设备现在大多数都是触屏的,键盘非常少,但是还有类似键盘的输入类似TV的DPad。键盘输入与触屏输入是一对有矛盾的交互设计方式。所以Android有两个模式来分别对待这两种交互方式,触摸模式(TouchMode)与普通模式,普通模式以键盘按下开始,触屏(Pointer操作,包括触屏,鼠标操作)为结束,而触摸模式相反,触屏为开始,触摸模式以键盘按下结束。
Focus机制必不可少
显然,普通模式是需要Focus机制来支持键盘Dpad等操作,但这并不代表着触摸模式就不需要焦点机制,比如手机需要打字时候软键盘需要对EditText进行输入,EditText就获取了焦点。
触摸模式与普通模式的切换
触摸模式与普通模式是通过ViewRootImpl中的ensureTouchModeLocally(boolean)
来进行切换。
/*** Ensure that the touch mode for this window is set, and if it is changing, * take the appropriate action. * @param inTouchMode Whether we want to be in touch mode. * @return True if the touch mode changed and focus changed was changed as a result */private boolean ensureTouchModeLocally(boolean inTouchMode) { if (DBG) Log.d("touchmode", "ensureTouchModeLocally(" + inTouchMode + "), current " + "touch mode is " + mAttachInfo.mInTouchMode); if (mAttachInfo.mInTouchMode == inTouchMode) return false; mAttachInfo.mInTouchMode = inTouchMode; mAttachInfo.mTreeObserver.dispatchOnTouchModeChanged(inTouchMode); return (inTouchMode) ? enterTouchMode() : leaveTouchMode();}
所以我们只需要找到ensureTouchModeLocally(boolean)
在哪些地方切换就能知道什么时候进入触摸模式与退出触摸模式。
初始化模式
新建ViewRootImpl是否进入触摸模式是由WindowManagerService中的mInTouchMode
,mInTouchMode
代表着当前系统是否在TouchMode环境下。而mInTouchMode
的开机初始化值由R.bool.config_defaultInTouchMode决定,之后将随用户对系统的操作决定。
用户操作:进入触摸模式
用户操作进入触摸模式的情况非常单一,代码在ViewRootImpl中
final class EarlyPostImeInputStage extends InputStage{ ··· 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); } } return FORWARD; } private int processPointerEvent(QueuedInputEvent q) { final MotionEvent event = (MotionEvent)q.mEvent; ··· // Enter touch mode on down or scroll. final int action = event.getAction(); if (action == MotionEvent.ACTION_DOWN || action == MotionEvent.ACTION_SCROLL) { ensureTouchMode(true); //进入触摸模式 } ··· return FORWARD; } ···}
可以看到当有Pointer操作(鼠标,触摸)传到EarlyPostImeStage时候,Down操作与Scroll操作将进入触摸模式
用户操作:退出触摸模式
退出方法1
同理,在EarlyPostImeStage中会检测用户是不是使用Dpad或者键盘输入,如果是,也会退出触摸模式
```final class EarlyPostImeInputStage extends InputStage{ ··· 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); } } return FORWARD; } private int processKeyEvent(QueuedInputEvent q) { final KeyEvent event = (KeyEvent)q.mEvent; // If the key's purpose is to exit touch mode then we consume it // and consider it handled. if (checkForLeavingTouchModeAndConsume(event)) { return FINISH_HANDLED; } // Make sure the fallback event policy sees all keys that will be // delivered to the view hierarchy. mFallbackEventHandler.preDispatchKeyEvent(event); return FORWARD; } ···}private boolean checkForLeavingTouchModeAndConsume(KeyEvent event) { // Only relevant in touch mode. if (!mAttachInfo.mInTouchMode) { return false; } // Only consider leaving touch mode on DOWN or MULTIPLE actions, never on UP. final int action = event.getAction(); if (action != KeyEvent.ACTION_DOWN && action != KeyEvent.ACTION_MULTIPLE) { return false; } // Don't leave touch mode if the IME told us not to. if ((event.getFlags() & KeyEvent.FLAG_KEEP_TOUCH_MODE) != 0) { return false; } // 1.导航键退出触摸模式 if (isNavigationKey(event)) { return ensureTouchMode(false); } // 2.键盘输入退出触摸模式 if (isTypingKey(event)) { ensureTouchMode(false); return false; } return false;}
退出方法2
退出TouchMode还可能通过辅助功能来退出,当用辅助功能转移焦点时候,就会退出触摸模式
//View.javapublic boolean performAccessibilityActionInternal(int action, Bundle arguments) { ··· switch (action) { ··· case AccessibilityNodeInfo.ACTION_FOCUS: { if (!hasFocus()) { // Get out of touch mode since accessibility // wants to move focus around. getViewRootImpl().ensureTouchMode(false); return requestFocus(); } } break; ··· } ···}
退出方法3
还有第三种退出触摸模式的方式:通过requestFocusFromTouch()
这个方法相当于退出触摸模式后再调用一次requestFocus()
public final boolean requestFocusFromTouch() { // Leave touch mode if we need to if (isInTouchMode()) { ViewRootImpl viewRoot = getViewRootImpl(); if (viewRoot != null) { viewRoot.ensureTouchMode(false); } } return requestFocus(View.FOCUS_DOWN);}
因为有些大多数View只设置了Focusable属性,但是没有设置FocusableInTouchMode属性,在触摸模式情况下,只设置Focusable属性没有设置FocusableInTouchMode属性的View是无法获取焦点的,所以调用requestFocus会无效。所以必须调用requestFocusFromTouch先退出触摸模式后获取焦点。
焦点查找
ViewRootImpl部分
对于轨迹球的上下左右,键盘的上下左右,tab,shift-tab,或者Dpad的上下左右,如果当前焦点没有消费完事件,会触发系统自动寻找下个焦点。关键代码如下:
/*** Delivers post-ime input events to the view hierarchy. */final class ViewPostImeInputStage extends InputStage { ··· @Override protected int onProcess(QueuedInputEvent q) { //1. 如果事件是KeyEvent,会走这里,接下来触发焦点转移(虽然轨迹球上下左右不是KeyEvent,但是可能会在人工合成阶段转化成KeyEvent) if (q.mEvent instanceof KeyEvent) { return processKeyEvent(q); } else { ··· } } 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; } // 2.KeyEvent如果没有被焦点消费的话,走这里 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; } // 3.焦点主要逻辑部分 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; }}
从第3点焦点的主要逻辑部分可以看到,主要通过调用View(ViewGroup)的findFocus来做一次树高度的查找,从上到下找到当前焦点,再通过ViewRootImpl,ViewGroup或View的focusSearch查找,从下到上查到下一个焦点,并且对下一个焦点进行对焦。
//View.java public View focusSearch(@FocusRealDirection int direction) { if (mParent != null) { return mParent.focusSearch(this, direction); } else { return null; }}//ViewGroup.javapublic 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;}//ViewRootImpl.java public View focusSearch(View focused, int direction) { checkThread(); if (!(mView instanceof ViewGroup)) { return null; } return FocusFinder.getInstance().findNextFocus((ViewGroup) mView, focused, direction);}
FocusFinder部分
从上面逻辑可以看出,最后都将调用到FocusFinder的findNextFocus(viewgroup,view,direction),对于第一个参数viewgroup,就是window中addView时候添加的View,也就是decorView。对于第二个参数view,如果没有当前没有焦点则为null,否则就是焦点。所以接下来看findNextFocus(viewgroup,view,direction)的实现。
//FocusFinder.javapublic View findNextFocusFromRect(ViewGroup root, Rect focusedRect, int direction) { mFocusedRect.set(focusedRect); return findNextFocus(root, null, mFocusedRect, 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);//1.添加可能获取到焦点的View if (!focusables.isEmpty()) { next = findNextFocus(root, focused, focusedRect, direction, focusables);//2.确定焦点区域 } } finally { focusables.clear(); } return next; }
以上代码比较关键的逻辑就是1,2
对于第1点,添加可能的焦点集合,对于addFocusable的逻辑主要跟desendantFocusability属性有关,分为三种,block,after,before。addFocusable会遍历整个View树,如果某个节点是block,则不会添加次节点的子节点;如果当前节点是before,则会添加子节点与当前节点;如果当前节点是after,则只会在子节点全都不能获取焦点的情况下添加当前节点。注意这里能不能获取到焦点跟当前焦点的模式有关,如果是触摸模式,只有focusableInTouchMode的节点来能获取焦点,如果是普通模式,则只需要focusable即可获取焦点。
对于第2点,终于到了确定焦点区域。
private View findNextFocus(ViewGroup root, View focused, Rect focusedRect, int direction, ArrayList<View> focusables) { //1. 确定焦点区域 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; } } } } //2 确定算法 switch (direction) { case View.FOCUS_FORWARD: case View.FOCUS_BACKWARD: return findNextFocusInRelativeDirection(focusables, root, focused, focusedRect, direction); case View.FOCUS_UP: case View.FOCUS_DOWN: case View.FOCUS_LEFT: case View.FOCUS_RIGHT: return findNextFocusInAbsoluteDirection(focusables, root, focused, focusedRect, direction); default: throw new IllegalArgumentException("Unknown direction: " + direction); } }
可以知道,如果当前有焦点,则焦点区域的矩形为焦点,如果当前没有焦点,当按下“下”“右”时候,焦点区域为DecorView左上的端点,当按下“左”“上”焦点区域为DecorView右下的断电。然后根据按下的按键,选择findNextFocusInRelativeDirection或者findNextFocusInAbsoluteDirection算法,我们在此只分析findNextFocusInAbsoluteDirection算法:
findNextFocusInAbsoluteDirection
View findNextFocusInAbsoluteDirection(ArrayList<View> focusables, ViewGroup root, View focused, Rect focusedRect, int direction) { //1.先把匹配矩形设置成最坏的情况,这样在接下来的比较中,总能把这种最坏的情况淘汰掉。 mBestCandidateRect.set(focusedRect); switch(direction) { case View.FOCUS_LEFT: mBestCandidateRect.offset(focusedRect.width() + 1, 0); break; case View.FOCUS_RIGHT: mBestCandidateRect.offset(-(focusedRect.width() + 1), 0); break; case View.FOCUS_UP: mBestCandidateRect.offset(0, focusedRect.height() + 1); break; case View.FOCUS_DOWN: mBestCandidateRect.offset(0, -(focusedRect.height() + 1)); } View closest = null; int numFocusables = focusables.size(); //2.遍历Focusables for (int i = 0; i < numFocusables; i++) { View focusable = focusables.get(i); // only interested in other non-root views if (focusable == focused || focusable == root) continue; // get focus bounds of other view in same coordinate system focusable.getFocusedRect(mOtherRect); root.offsetDescendantRectToMyCoords(focusable, mOtherRect); //3. 使用比较算法isBetterCandidate来求得最好的匹配结果 if (isBetterCandidate(direction, focusedRect, mOtherRect, mBestCandidateRect)) { mBestCandidateRect.set(mOtherRect); closest = focusable; } } return closest; }
可以看到,通过isBetterCandidate来比较所有focusables,选取最好的情况来作为下一个焦点。
isBetterCandidate
//是否rect1更加匹配boolean isBetterCandidate(int direction, Rect source, Rect rect1, Rect rect2) { //Candidate算法用于方向判断,如果rect1方向不对,那就不能淘汰rect2 if (!isCandidate(source, rect1, direction)) { return false; } //如果rect2方向不对,但是rect1方向对,那么rect1更加匹配 if (!isCandidate(source, rect2, direction)) { return true; } // beamBeats算法用于比较rect1,rect2主要通过在direction方向上是否重叠以及距离来比较 if (beamBeats(direction, source, rect1, rect2)) { return true; } // if rect2 is better, then rect1 cant' be :) if (beamBeats(direction, source, rect2, rect1)) { return false; } // 以上都比较不了,那么就用主次轴方向上距离的比较来算出结果 return (getWeightedDistanceFor( majorAxisDistance(direction, source, rect1), minorAxisDistance(direction, source, rect1)) < getWeightedDistanceFor( majorAxisDistance(direction, source, rect2), minorAxisDistance(direction, source, rect2))); }
- 从Android源码的角度理解应用开发(2)-Focus机制
- 从Android源码的角度理解应用开发(1)-Touch机制
- 从Android源码的角度理解应用开发(1)-Touch机制
- 从源码的角度理解Android消息处理机制
- Android开发从源码的角度理解Volley
- Android异步消息处理机制(二):从源码的角度彻底理解
- Android 进阶学习:事件分发机制完全解析,带你从源码的角度彻底理解(上)
- Android 进阶学习:事件分发机制完全解析,带你从源码的角度彻底理解(上)
- (转载)Android事件分发机制完全解析,带你从源码的角度彻底理解(上)
- (转载) Android事件分发机制完全解析,带你从源码的角度彻底理解(下)
- Android事件分发机制完全解析,带你从源码的角度彻底理解(上)
- Android事件分发机制完全解析,带你从源码的角度彻底理解(上)
- Android事件分发机制完全解析,带你从源码的角度彻底理解(下)
- Android事件分发机制完全解析,带你从源码的角度彻底理解(上)
- Android事件分发机制完全解析,带你从源码的角度彻底理解(上)
- Android事件分发机制完全解析,带你从源码的角度彻底理解
- Android事件分发机制完全解析,带你从源码的角度彻底理解(上)
- Android事件分发机制完全解析,带你从源码的角度彻底理解(下)
- MySQL数据库自增主键归零的几种方法
- ARM汇编中ldr伪指令和ldr指令
- 数据结构C——单链表
- 文章标题
- 解决小米手机不能运行Android Studio程序的问题
- 从Android源码的角度理解应用开发(2)-Focus机制
- UVA 548 —— 二叉树的递归遍历
- 深入理解javascript函数定义与函数作用域
- 升级gradle版本之后小米手机调试安装失败
- Spring(五、AOP面向切面编程)
- PAT算法笔记(十六)————组个最小数
- SBULL块设备驱动程序分析
- 休闲时光的小代码-----用div格式制作田字格
- JavaScript 异步机制