从源码出发浅析 Android TV 的焦点移动原理(下篇)

从源码出发浅析 Android TV 的焦点移动原理 (上篇)

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.java
public 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
@Override
public 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里居然截获不到按键事件的原因。

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

原创声明,本文系作者授权云+社区-专栏发表,未经许可,不得转载。

如有侵权,请联系 yunjia_community@tencent.com 删除。

编辑于

我来说两句

0 条评论
登录 后参与评论

相关文章

来自专栏吴小龙同學

Android 画笔Paint和画布Canvas

先了解画笔Paint和画布Canvas一些基本方法,然后学习画笔特效处理的高级属性PorterDuffXfermode、Shader、PathEffect,并...

3496
来自专栏DannyHoo的专栏

改变UITextView的边框颜色

版权声明:本文为博主原创文章,未经博主允许不得转载。 https://blog.csdn.net/u010105969/article/details/...

691
来自专栏潇涧技术专栏

Android Heroes Reading Notes 2

《Android群英传》读书笔记 (2) 第三章 控件架构与自定义控件详解 + 第四章 ListView使用技巧 + 第五章 Scroll分析

571
来自专栏飞雪无情的博客

Android ViewGroup.setDescendantFocusability函数

这个函数是在ViewGroup里定义的,主要用于控制child View获取焦点的能力,比如是否阻止child View获取焦点。

501
来自专栏中国Android研究院

自定义控件之带渐变色的圆形进度条

七夕到了,祝大家七夕节快乐! 今天给大家讲解一个简单的进度条的自定义。首页呢,我们看下效果图:

1052
来自专栏Android开发小工

View的位置参数及其Scroller类的理解

x和y是View左上角的坐标,translationX和translationY是View左上角相对于容器的偏移量。这几个参数都是相对于父容器的坐标。

964
来自专栏Android干货

安卓开发_慕课网_百度地图_刮刮涂层效果

34214
来自专栏Android小菜鸡

CanvasAPI

662
来自专栏向治洪

Android ViewDragHelper及移动处理总结

概述 2013年谷歌i/o大会上介绍了两个新的layout: SlidingPaneLayout和DrawerLayout,现在这俩个类被广泛的运用。我们知道在...

2448
来自专栏成长道路

OneHotEncoder介绍单属性多属性scala实现

       因为项目的需要,将数据库中表的属性向量化,然后进行机器学习,所以去spark官网学习了一下OneHotEncoder,官网的相关介绍比较少,主要是...

2270

扫码关注云+社区