从源码出发浅析 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 条评论
登录 后参与评论

相关文章

来自专栏郭耀华‘s Blog

android:scaleType属性

ImageView.ScaleType.CENTER|android:scaleType="center" 以原图的几何中心点和ImagView的几何中心点为...

2589
来自专栏向治洪

Android滤镜效果实现及原理分析

Android在处理图片时,最常使用到的数据结构是位图(Bitmap),它包含了一张图片所有的数据。整个图片都是由点阵和颜色值组成的,所谓点阵就是一个包含像素的...

3606
来自专栏我的技术专栏

UnityShader 表面着色器简单例程集合

3225
来自专栏iOS 开发杂谈

如何手动实现一个 UIScrollView

UIKit 坐标系每一个 View 都定义了他自己的坐标系,如下图所示,x 轴指向右方,y 轴指向下方:

693
来自专栏数据结构与算法

BZOJ2287: 【POJ Challenge】消失之物(背包dp)

第三个的转移非常神仙,反正我是没想出来,我们考虑用总的方案数减去用了改物品的方案数,我们发现直接算不是很好算,然后补集转化一下,用了物品$i$,体积为$j$,那...

451
来自专栏人工智能头条

将机器学习应用于金融技术领域的15家公司(英)

1392
来自专栏理论坞

微光小插画—一篇不怎么专业的所谓教程

说说为什么选择大象和骆驼吧,选择大象是因为是前几天看了一篇呼吁大家不要骑大象的视频,觉得琦大象很残忍,并且这段时间又在读《未来简史》人类史,从另一些角度颠覆了一...

743
来自专栏刘望舒

Android 人脸识别之人脸注册

3362

应用潜在语义分析技术将文档进行3D可视化

这里使用了 WPF(译者注:Windows Presentation Foundation) 的 3D 展示功能来对一个文档集合进行了可视化,这些文...

2089
来自专栏一心无二用,本人只专注于基础图像算法的实现与优化。

PhotoShop算法原理解析系列 - 风格化---》查找边缘。                  闲谈.Net类型之public的不public,fixed的不能fixed     当然这个还可

      之所以不写系列文章一、系列文章二这样的标题,是因为我不知道我能坚持多久。我知道我对事情的表达能力和语言的丰富性方面的天赋不高。而一段代码需要我去用心...

2359

扫码关注云+社区