从源码出发浅析Android TV的焦点移动原理-下篇
来源:互联网 发布:苹果软件发布 编辑:程序博客网 时间:2024/05/21 09:22
2.2 findNextFocus
如果开发者没有指定nextFocusId,则用findNextFocus找指定方向上最近的视图
看一下这里的用法
focusables.clear();// 2.2.1 找到所有isFocusable的View root.addFocusables(focusables, direction);if (!focusables.isEmpty()) { // 2.2.2 从focusables中找到最近的一个 next = findNextFocus(root, focused, focusedRect, direction, focusables);}
2.2.1 View.addFocusables,从root开始找所有isFocusable的视图
public void addFocusables(ArrayList<View> views, @FocusDirection int direction) { addFocusables(views, direction, FOCUSABLES_TOUCH_MODE);}public void addFocusables(ArrayList<View> views, @FocusDirection int direction, @FocusableMode int focusableMode) { ... views.add(this);}
如果root是一个单纯View,则添加自己,但这种情况很少见,大部分的root都是ViewGroup
// ViewGroup.javapublic void addFocusables(ArrayList<View> views, int direction, int focusableMode) { final int focusableCount = views.size(); final int descendantFocusability = getDescendantFocusability(); if (descendantFocusability != FOCUS_BLOCK_DESCENDANTS) { ... final int count = mChildrenCount; final View[] children = mChildren; for (int i = 0; i < count; i++) { final View child = children[i]; if ((child.mViewFlags & VISIBILITY_MASK) == VISIBLE) { child.addFocusables(views, direction, focusableMode); } } } if ((descendantFocusability != FOCUS_AFTER_DESCENDANTS // No focusable descendants || (focusableCount == views.size())) && (isFocusableInTouchMode() || !shouldBlockFocusForTouchscreen())) { super.addFocusables(views, direction, focusableMode); }}
对于ViewGroup来说,遍历并添加自己的所有isFocusable的child
这里有个descendantFocusability变量,有三个取值
FOCUS_BEFORE_DESCENDANTS:在所有子视图之前获取焦点
FOCUS_AFTER_DESCENDANTS: 在所有子视图之后获取焦点
FOCUS_BLOCK_DESCENDANTS: 阻止所有子视图获取焦点,即使他们是focusable的
2.2.2 FocusFinder.findNextFocus
private View findNextFocus(ViewGroup root, View focused, Rect focusedRect, int direction, ArrayList<View> focusables) { if (focused != null) { if (focusedRect == null) { focusedRect = mFocusedRect; } // 2.2.2.1 取得考虑scroll之后的焦点Rect,该Rect是相对focused视图本身的 // fill in interesting rect from focused focused.getFocusedRect(focusedRect); // 2.2.2.2 将当前focused视图的坐标系,转换到root的坐标系中,统一坐标,以便进行下一步的计算 root.offsetDescendantRectToMyCoords(focused, focusedRect); } else { ... } switch (direction) { ... case View.FOCUS_UP: case View.FOCUS_DOWN: case View.FOCUS_LEFT: case View.FOCUS_RIGHT: 2.2.2.3 找出指定方向上的下一个focus视图 return findNextFocusInAbsoluteDirection(focusables, root, focused, focusedRect, direction); default: throw new IllegalArgumentException("Unknown direction: " + direction); }}
2.2.2.1 focused.getFocusedRect(focusedRect);
public void getFocusedRect(Rect r) { getDrawingRect(r);}public void getDrawingRect(Rect outRect) { outRect.left = mScrollX; outRect.top = mScrollY; outRect.right = mScrollX + (mRight - mLeft); outRect.bottom = mScrollY + (mBottom - mTop);}
这里是取得考虑scroll之后的焦点Rect,该Rect是相对focused视图本身的
2.2.2.2 root.offsetDescendantRectToMyCoords(focused, focusedRect);
public final void offsetDescendantRectToMyCoords(View descendant, Rect rect) { offsetRectBetweenParentAndChild(descendant, rect, true, false);}/** * Helper method that offsets a rect either from parent to descendant or * descendant to parent. */void offsetRectBetweenParentAndChild(View descendant, Rect rect, boolean offsetFromChildToParent, boolean clipToBounds) { // already in the same coord system :) if (descendant == this) { return; } ViewParent theParent = descendant.mParent; // search and offset up to the parent // 在View树上往上层层遍历,直到root为止 while ((theParent != null) && (theParent instanceof View) && (theParent != this)) { if (offsetFromChildToParent) { // 把focusedRect转换到当前当前parent的坐标系中去 rect.offset(descendant.mLeft - descendant.mScrollX, descendant.mTop - descendant.mScrollY); ... } else { ... rect.offset(descendant.mScrollX - descendant.mLeft, descendant.mScrollY - descendant.mTop); } // 继续往上找 descendant = (View) theParent; theParent = descendant.mParent; } // now that we are up to this view, need to offset one more time // to get into our coordinate space if (theParent == this) { if (offsetFromChildToParent) { // 最后再转换一次,终于把focusedRect的坐标转换到了root的坐标中 rect.offset(descendant.mLeft - descendant.mScrollX, descendant.mTop - descendant.mScrollY); } else { rect.offset(descendant.mScrollX - descendant.mLeft, descendant.mScrollY - descendant.mTop); } } else { throw new IllegalArgumentException("parameter must be a descendant of this view"); }}
经过层层转换,最终把focused视图的坐标,转换到了root坐标系中。这样就统一了坐标,以便进行下一步的计算。
2.2.2.3 找出指定方向上的下一个focus视图
findNextFocusInAbsoluteDirection(focusables, root, focused, focusedRect, direction);
View findNextFocusInAbsoluteDirection(ArrayList<View> focusables, ViewGroup root, View focused, Rect focusedRect, int direction) { // initialize the best candidate to something impossible // (so the first plausible view will become the best choice) mBestCandidateRect.set(focusedRect); switch(direction) { case View.FOCUS_LEFT: // 先虚构出一个默认候选Rect,就是把focusedRect向右移一个"身位",按键向左,那么他肯定就是优先级最低的了 mBestCandidateRect.offset(focusedRect.width() + 1, 0); break; ... } View closest = null; int numFocusables = focusables.size(); // 遍历所有focusable的视图 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); // 将focusable的坐标转换到root的坐标系中,统一坐标 root.offsetDescendantRectToMyCoords(focusable, mOtherRect); // 进行比较,选出较好的那一个,如果都是默认候选的Rect差,则closest为null if (isBetterCandidate(direction, focusedRect, mOtherRect, mBestCandidateRect)) { mBestCandidateRect.set(mOtherRect); closest = focusable; } } return closest;}
在统一坐标之后,对于所有focusable的视图,进行一次遍历比较,得到最“近”的视图作为下一个焦点视图。这里用到了一个方法isBetterCandidate,从两个候选Rect中找到在指定方向上离当前Rect最近的一个,具体算法这里不细讲了。
至此,就找到了下一个焦点视图,然后调用requestFocus方法,让其获得焦点。
小结
经过对源码的分析,系统本身寻找下一个焦点视图的过程是:
首先寻找用户指定了id的视图,从当前焦点视图的节点开始遍历,直到找到匹配该id的视图。也许存在多个相同id的视图,但是只会找到视图节点树中最近的一个。
如果没有指定id,则遍历找出所有isFocusable的视图,统一坐标系,然后计算出指定方向上离当前焦点视图最近的一个视图。
结合KeyEvent事件的流转,处理焦点的时机,按照优先级(顺序)依次是:
dispatchKeyEvent
mOnKeyListener.onKey回调
onKeyDown/onKeyUp
focusSearch
指定nextFocusId
系统自动从所有isFocusable的视图中找下一个焦点视图
以上任一处都可以指定焦点,一旦使用了就不再往下走。
很多视图控件就重写了其中一些方法。
比如ScrollView,它会在dispatchKeyEvent的时候,自己去处理,用来进行内部的焦点移动或者整体滑动。
// ScrollView.java@Overridepublic boolean dispatchKeyEvent(KeyEvent event) { // Let the focused view and/or our descendants get the key first return super.dispatchKeyEvent(event) || executeKeyEvent(event);}public boolean executeKeyEvent(KeyEvent event) { mTempRect.setEmpty(); if (!canScroll()) { if (isFocused() && event.getKeyCode() != KeyEvent.KEYCODE_BACK) { View currentFocused = findFocus(); if (currentFocused == this) currentFocused = null; View nextFocused = FocusFinder.getInstance().findNextFocus(this, currentFocused, View.FOCUS_DOWN); // 如果不能滑动,则直接让下一个Focus视图获取焦点 return nextFocused != null && nextFocused != this && nextFocused.requestFocus(View.FOCUS_DOWN); } return false; } boolean handled = false; // 如果可以滑动,则进行ScrollView本身的滑动 if (event.getAction() == KeyEvent.ACTION_DOWN) { switch (event.getKeyCode()) { case KeyEvent.KEYCODE_DPAD_UP: if (!event.isAltPressed()) { handled = arrowScroll(View.FOCUS_UP); } else { handled = fullScroll(View.FOCUS_UP); } break; case KeyEvent.KEYCODE_DPAD_DOWN: if (!event.isAltPressed()) { handled = arrowScroll(View.FOCUS_DOWN); } else { handled = fullScroll(View.FOCUS_DOWN); } break; case KeyEvent.KEYCODE_SPACE: pageScroll(event.isShiftPressed() ? View.FOCUS_UP : View.FOCUS_DOWN); break; } } return handled;}
由于在dispatchKeyEvent里优先处理的,因此对于滑动方向的KeyEvent,onKeyDown就监听不到了。这也就是为什么onKeyDown里居然截获不到按键事件的原因。
本文从源码的角度分析了焦点的移动原理,如果大家有兴趣可以一起多多交流。
- 从源码出发浅析Android TV的焦点移动原理-下篇
- 从源码出发浅析Android TV的焦点移动原理
- 从源码出发浅析Android TV的焦点移动原理-上篇
- Android TV 焦点上下左右移动
- android tv 焦点移动特效
- Android TV 焦点移动飞框的实现
- Android TV RecyclerView焦点移动飞框的实现
- android tv焦点特效实现浅析
- android tv焦点特效实现浅析
- android tv焦点特效实现浅析
- android tv焦点特效实现浅析
- android tv焦点特效实现浅析
- Android TV listView焦点平滑移动
- Android TV 焦点分发原理解析
- Android TV 上使用的RecyclerView和焦点框架,焦点框移动效果,完胜androidTvwidget的MainUpView
- Android Tv 焦点移动特效项目学习经验1
- Android TV 关于gridview的焦点问题
- 从源码出发深入理解 Android Service
- cookie 原理及应用
- hibernate 多对一关联关系报错
- 手机编程经验总结
- top命令标识详解
- android中一个app跳转到另一个app中(超级详细)
- 从源码出发浅析Android TV的焦点移动原理-下篇
- Tensorflow12-Dlib人脸检测
- Python3之协程
- js 子页面控制父页面操作刷新
- 如何溜溜地使用pandas操作数据(一)
- iOS-给数组去重的几个方法
- Java中return在Try-Catch中的执行
- HashMap的实现原理
- IDEA自动重置LanguageLevel和JavaCompiler的问题 (internal java compiler error)