从源码出发浅析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方法,让其获得焦点。

小结

经过对源码的分析,系统本身寻找下一个焦点视图的过程是:

  1. 首先寻找用户指定了id的视图,从当前焦点视图的节点开始遍历,直到找到匹配该id的视图。也许存在多个相同id的视图,但是只会找到视图节点树中最近的一个。

  2. 如果没有指定id,则遍历找出所有isFocusable的视图,统一坐标系,然后计算出指定方向上离当前焦点视图最近的一个视图。

结合KeyEvent事件的流转,处理焦点的时机,按照优先级(顺序)依次是:

  1. dispatchKeyEvent

  2. mOnKeyListener.onKey回调

  3. onKeyDown/onKeyUp

  4. focusSearch

  5. 指定nextFocusId

  6. 系统自动从所有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里居然截获不到按键事件的原因。

本文从源码的角度分析了焦点的移动原理,如果大家有兴趣可以一起多多交流。

原创粉丝点击