对AndroidTV寻焦的一点思考

整体流程

绘制

1.ViewRootImpl#performTraversals

ini 复制代码
private void performTraversals() {
    ...
    boolean skipDraw = false;
    if (mFirst) {
        if (mView != null) {
            if (!mView.hasFocus()) {
                mView.requestFocus(View.FOCUS_FORWARD);
            } 
        }
    }
    mFirst = false;
    ...
}

第一次收到VSYNC信号后,判断DecorView内子孙View是否有获焦,没有则调用DecorView#requestFocus

2.ViewGroup#requestFocus

java 复制代码
public boolean requestFocus(int direction, Rect previouslyFocusedRect) {
    int descendantFocusability = getDescendantFocusability();

    switch (descendantFocusability) {
        case FOCUS_BLOCK_DESCENDANTS:
            return super.requestFocus(direction, previouslyFocusedRect);
        case FOCUS_BEFORE_DESCENDANTS: {
            final boolean took = super.requestFocus(direction, previouslyFocusedRect);
            return took ? took : onRequestFocusInDescendants(direction, previouslyFocusedRect);
        }
        case FOCUS_AFTER_DESCENDANTS: {
            final boolean took = onRequestFocusInDescendants(direction, previouslyFocusedRect);
            return took ? took : super.requestFocus(direction, previouslyFocusedRect);
        }
    }
}

FOCUS_AFTER_DESCENDANTS:先让子View处理,未消费交给ViewGroup

FOCUS_BLOCK_DESCENDANTS:ViewGroup本身处理,不让子View获焦

FOCUS_BEFORE_DESCENDANTS:先让ViewGroup处理,未消费交给子View

3.ViewGroup#onRequestFocusInDescendants

arduino 复制代码
protected boolean onRequestFocusInDescendants(int direction,
        Rect previouslyFocusedRect) {
    ...
    for (int i = index; i != end; i += increment) {
        View child = children[i];
        if ((child.mViewFlags & VISIBILITY_MASK) == VISIBLE) {
            if (child.requestFocus(direction, previouslyFocusedRect)) {
                return true;
            }
        }
    }
    return false;
}

4.View#requestFocus

kotlin 复制代码
private boolean requestFocusNoSearch(int direction, Rect previouslyFocusedRect) {
    if ((mViewFlags & FOCUSABLE_MASK) != FOCUSABLE ||
            (mViewFlags & VISIBILITY_MASK) != VISIBLE) {
        return false;
    }

    if (isInTouchMode() &&
        (FOCUSABLE_IN_TOUCH_MODE != (mViewFlags & FOCUSABLE_IN_TOUCH_MODE))) {
           return false;
    }

    if (hasAncestorThatBlocksDescendantFocus()) {
        return false;
    }

    handleFocusGainInternal(direction, previouslyFocusedRect);
    return true;
}

判断当前View是否获焦、可见,以及祖先ViewGroup是否Block

5.View#handleFocusGainInternal

scss 复制代码
void handleFocusGainInternal(@FocusRealDirection int direction, Rect previouslyFocusedRect) {
    if ((mPrivateFlags & PFLAG_FOCUSED) == 0) {
        mPrivateFlags |= PFLAG_FOCUSED;

        View oldFocus = (mAttachInfo != null) ? getRootView().findFocus() : null;

        if (mParent != null) {
            mParent.requestChildFocus(this, this);
        }

        if (mAttachInfo != null) {
            mAttachInfo.mTreeObserver.dispatchOnGlobalFocusChange(oldFocus, this);
        }

        onFocusChanged(true, direction, previouslyFocusedRect);
        refreshDrawableState();
    }
}

告诉父View已获焦 回调ViewTreeObserver#OnGlobalFocusChangeListener 回调View#OnFocusChangedListener

6.ViewGroup#requestChildFocus

scss 复制代码
public void requestChildFocus(View child, View focused) {
    if (getDescendantFocusability() == FOCUS_BLOCK_DESCENDANTS) {
        return;
    }

    super.unFocus(focused);

    if (mFocused != child) {
        if (mFocused != null) {
            mFocused.unFocus(focused);
        }

        mFocused = child;
    }
    if (mParent != null) {
        mParent.requestChildFocus(this, focused);
    }
}

尝试清理焦点,更新mFocused mFocused:获焦View、包含获焦View的直接子View

7.ViewGroup#unFocus

typescript 复制代码
@Override
void unFocus(View focused) {
    if (mFocused == null) {
        super.unFocus(focused);
    } else {
        mFocused.unFocus(focused);
        mFocused = null;
    }
}

这里其实就是找到和焦点View相同的公共父节点View,调用unFocus,将焦点所有的祖先View的mFocused置为空

8.View#unFocus

scss 复制代码
void clearFocusInternal(View focused, boolean propagate, boolean refocus) {
    if ((mPrivateFlags & PFLAG_FOCUSED) != 0) {
        mPrivateFlags &= ~PFLAG_FOCUSED;

        if (propagate && mParent != null) {
            mParent.clearChildFocus(this);
        }

        onFocusChanged(false, 0, null);
        refreshDrawableState();

        if (propagate && (!refocus || !rootViewRequestFocus())) {
            notifyGlobalFocusCleared(this);
        }
    }
}

按键

1.ViewPostImeInputStage#processKeyEvent

java 复制代码
private int processKeyEvent(QueuedInputEvent q) {
    ...
    if (mView.dispatchKeyEvent(event)) {
        return FINISH_HANDLED;
    }
    if (event.getAction() == KeyEvent.ACTION_DOWN) {
        ...
        if (direction != 0) {
            View focused = mView.findFocus();
            if (focused != null) {
                View v = focused.focusSearch(direction);
                if (v != null && v != focused) {
                    ...
                    if (v.requestFocus(direction, mTempRect)) {
                        return FINISH_HANDLED;
                    }
                }
            } else {
                View v = focusSearch(null, direction);
                if (v != null && v.requestFocus(direction)) {
                    return FINISH_HANDLED;
                }
            }
        }

ACTION_DOWN返回true,系统不会再进行焦点处理 找到当前焦点,从当前获焦位置进行寻焦,找到焦点后进行获焦 如果没有焦点,从DecorView中进行焦点查找

2.ViewGroup#dispatchKeyEvent

csharp 复制代码
public boolean dispatchKeyEvent(KeyEvent event) {
    ...
    if ((mPrivateFlags & (PFLAG_FOCUSED | PFLAG_HAS_BOUNDS))
            == (PFLAG_FOCUSED | PFLAG_HAS_BOUNDS)) {
        if (super.dispatchKeyEvent(event)) {
            return true;
        }
    } else if (mFocused != null && (mFocused.mPrivateFlags & PFLAG_HAS_BOUNDS)
            == PFLAG_HAS_BOUNDS) {
        if (mFocused.dispatchKeyEvent(event)) {
            return true;
        }
    }
    ...
    return false;
}

这里可以看出KeyEvent只在焦点View的祖先由上而下进行分发

3.ViewGroup#findFocus

kotlin 复制代码
public View findFocus() {
     ...
    if (isFocused()) {
        return this;
    }

    if (mFocused != null) {
        return mFocused.findFocus();
    }
    return null;
}

通过每一层持有的mFocused找到焦点

4.View#focusSearch

kotlin 复制代码
public View focusSearch(@FocusRealDirection int direction) {
    if (mParent != null) {
        return mParent.focusSearch(this, direction);
    } else {
        return null;
    }
}

5.ViewGroup#focusSearch

kotlin 复制代码
public 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;
}

递归到DecorView后调用FocusFinder进行焦点查找。当然ViewPager、RecyclerView的focusSearch已经override,并不一定能走到DecorView

寻焦策略

1. FocusFinder#findNextFocus

ini 复制代码
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);
        if (!focusables.isEmpty()) {
            next = findNextFocus(root, focused, focusedRect, direction, focusables);
        }
    } finally {
        focusables.clear();
    }
    return next;
}

寻找nextFocusLeft、nextFocusRight等focused在代码中强制指定要获焦的View 调用root#addFocusables,将ViewGroup下的参与本次选焦过程

2.ViewGroup#addFocusables

ini 复制代码
public void addFocusables(ArrayList<View> views, int direction, int focusableMode) {
    final int focusableCount = views.size();

    final int descendantFocusability = getDescendantFocusability();

    if (descendantFocusability != FOCUS_BLOCK_DESCENDANTS) {
        if (shouldBlockFocusForTouchscreen()) {
            focusableMode |= FOCUSABLES_TOUCH_MODE;
        }

        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);
            }
        }
    }

递归调用所有子View的addFocusables方法,可能将所有View都添加到ArrayList中,ViewPager、RecyclerView等的addFocusables也已override

3.FocusFinder#findNextFocus

csharp 复制代码
private View findNextFocus(ViewGroup root, View focused, Rect focusedRect,
        int direction, ArrayList<View> focusables) {
    if (focused != null) {
        if (focusedRect == null) {
            focusedRect = mFocusedRect;
        }
        // fill in interesting rect from focused
        focused.getFocusedRect(focusedRect);
        root.offsetDescendantRectToMyCoords(focused, focusedRect);
    } 
    ...
    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);
    }
}
  • 统一到root所在的坐标系内, 修正focusedRect,left、top 、right、bottom 相对于root
  • 对下上左右与前进后退进行区分

4.FocusFinder#findNextFocusInAbsoluteDirection

ini 复制代码
View findNextFocusInAbsoluteDirection(ArrayList<View> focusables, ViewGroup root, View focused,
        Rect focusedRect, int direction) {

    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();
    for (int i = 0; i < numFocusables; i++) {
        View focusable = focusables.get(i);

        if (focusable == focused || focusable == root) continue;

        // get focus bounds of other view in same coordinate system
        focusable.getFocusedRect(mOtherRect);
        root.offsetDescendantRectToMyCoords(focusable, mOtherRect);

        if (isBetterCandidate(direction, focusedRect, mOtherRect, mBestCandidateRect)) {
            mBestCandidateRect.set(mOtherRect);
            closest = focusable;
        }
    }
    return closest;
}

修正后代的Rect,判断后代的Rect是否比mBestCandidateRect好,mBestCandidateRect初始值为假定的(FOCUS_LEFT : 大小相同,在其右侧+1px)

5.FocusFinder#isBetterCandidate

kotlin 复制代码
boolean isBetterCandidate(int direction, Rect source, Rect rect1, Rect rect2) {

    // to be a better candidate, need to at least be a candidate in the first
    // place :)
    if (!isCandidate(source, rect1, direction)) {
        return false;
    }

    // we know that rect1 is a candidate.. if rect2 is not a candidate,
    // rect1 is better
    if (!isCandidate(source, rect2, direction)) {
        return true;
    }

    // if rect1 is better by beam, it wins
    if (beamBeats(direction, source, rect1, rect2)) {
        return true;
    }

    // if rect2 is better, then rect1 cant' be :)
    if (beamBeats(direction, source, rect2, rect1)) {
        return false;
    }

    // otherwise, do fudge-tastic comparison of the major and minor axis
    return (getWeightedDistanceFor(
                    majorAxisDistance(direction, source, rect1),
                    minorAxisDistance(direction, source, rect1))
            < getWeightedDistanceFor(
                    majorAxisDistance(direction, source, rect2),
                    minorAxisDistance(direction, source, rect2)));
}
  • rect1不在获焦View左侧,直接丢弃
  • rect1在、rect2不在获焦View左侧,rect1是最佳候选
  • rect1、rect2都在获焦View左侧,通过向左投影进行判断
  • rect1在投影上、rect2不在投影上,rect1是最佳候选
  • rect2在投影上、rect1不在投影上,rect1竞选失败
  • 最后根据主、次轴加权距离进行判断,距离短获胜

6.FocusFinder#isCandidate

arduino 复制代码
boolean isCandidate(Rect srcRect, Rect destRect, int direction) {
    switch (direction) {
        case View.FOCUS_LEFT:
            return (srcRect.right > destRect.right || srcRect.left >= destRect.right) 
                    && srcRect.left > destRect.left;
        case View.FOCUS_RIGHT:
            return (srcRect.left < destRect.left || srcRect.right <= destRect.left)
                    && srcRect.right < destRect.right;
        case View.FOCUS_UP:
            return (srcRect.bottom > destRect.bottom || srcRect.top >= destRect.bottom)
                    && srcRect.top > destRect.top;
        case View.FOCUS_DOWN:
            return (srcRect.top < destRect.top || srcRect.bottom <= destRect.top)
                    && srcRect.bottom < destRect.bottom;
    }
}

FOCUS_LEFT :与source相比,src的left必须要小, right要小。可以简单理解为在左侧即可

7.FocusFinder#beamBeats

arduino 复制代码
boolean beamBeats(int direction, Rect source, Rect rect1, Rect rect2) {
    final boolean rect1InSrcBeam = beamsOverlap(direction, source, rect1);
    final boolean rect2InSrcBeam = beamsOverlap(direction, source, rect2);

    if (rect2InSrcBeam || !rect1InSrcBeam) {
        return false;
    }

    if (!isToDirectionOf(direction, source, rect2)) {
        return true;
    }

    if ((direction == View.FOCUS_LEFT || direction == View.FOCUS_RIGHT)) {
        return true;
    }        

    return (majorAxisDistance(direction, source, rect1)
            < majorAxisDistanceToFarEdge(direction, source, rect2));
}
  • 判断rect1是不是更好,从焦点处向获焦方向进行投影,如果rect2在投影内或rect1不在投影内,说明rect1不符合竞选结果。
  • rect2不在source的寻焦方向上,rect1竞选成功。

9.FocusFinder#getWeightedDistanceFor

arduino 复制代码
int getWeightedDistanceFor(int majorAxisDistance, int minorAxisDistance) {
    return 13 * majorAxisDistance * majorAxisDistance
            + minorAxisDistance * minorAxisDistance;
}

13*主轴距离的平方+次轴距离的平方

arduino 复制代码
static int majorAxisDistanceRaw(int direction, Rect source, Rect dest) {
    switch (direction) {
        case View.FOCUS_LEFT:
            return source.left - dest.right;
        case View.FOCUS_RIGHT:
            return dest.left - source.right;
        case View.FOCUS_UP:
            return source.top - dest.bottom;
        case View.FOCUS_DOWN:
            return dest.top - source.bottom;
    }
}

主轴距离: 获焦View的left到目标的right

arduino 复制代码
static int minorAxisDistance(int direction, Rect source, Rect dest) {
    switch (direction) {
        case View.FOCUS_LEFT:
        case View.FOCUS_RIGHT:
            // the distance between the center verticals
            return Math.abs(
                    ((source.top + source.height() / 2) -
                    ((dest.top + dest.height() / 2))));
        case View.FOCUS_UP:
        case View.FOCUS_DOWN:
            // the distance between the center horizontals
            return Math.abs(
                    ((source.left + source.width() / 2) -
                    ((dest.left + dest.width() / 2))));
    }
}

次轴距离: 获焦View与目标View的竖直中心距离

Case分析

焦点记忆

方案1:ViewGroup整体参与竞选

java 复制代码
@Override
public void addFocusables(ArrayList<View> views, int direction, int focusableMode) {
    if (hasFocus()) {
        super.addFocusables(views, direction, focusableMode);
    } else {
        views.add(this);
    }
}

@Override
protected boolean onRequestFocusInDescendants(int direction, Rect previouslyFocusedRect) {
    if (mLastFocused != null && mLastFocused.requestFocus()) {
        return true;
    }
    return super.onRequestFocusInDescendants(direction, previouslyFocusedRect);
}

@Override
public void requestChildFocus(View child, View focused) {
    super.requestChildFocus(child, focused);
    mLastFocused = focused;
}

方案2: 只让上次获焦View参与竞选

java 复制代码
@Override
public void addFocusables(ArrayList<View> views, int direction, int focusableMode) {
    if (hasFocus()) {
        super.addFocusables(views, direction, focusableMode);
    } else {
        views.add(mLastFocused);
    }
}

@Override
public void requestChildFocus(View child, View focused) {
    super.requestChildFocus(child, focused);
    mLastFocused = focused;
}

参考文档

cloud.tencent.com/developer/a... cloud.tencent.com/developer/a... tech.bytedance.net/articles/11...

相关推荐
帅次23 分钟前
Android 高级工程师面试参考答案:架构设计、Jetpack 与 Compose
android·面试·职场和发展·架构·composer·jetpack
limingade26 分钟前
Dialer3.0智能拨号器Android版功能说明书
android·蓝牙电话·手机转sip·手机蓝牙·智能拨号器
JJay.30 分钟前
Android BLE 的 notify 和 indicate 到底有什么区别
android
橙子1991101632 分钟前
Android 异步任务和消息机制
android
被开发耽误的大厨1 小时前
5、Integer缓存池里同一个对象指的是什么?Integer 和String 内存结构逻辑完全一样?
android·java·哈希算法
NoSi EFUL9 小时前
MySQL中ON DUPLICATE KEY UPDATE的介绍与使用、批量更新、存在即更新不存在则插入
android·数据库·mysql
安小牛11 小时前
Android 开发汉字转带声调的拼音
android·java·学习·android studio
聚美智数11 小时前
企业实际控制人查询-公司实控人查询
android·java·javascript
JMchen12313 小时前
第 3 篇|Android 项目结构解析与第一个界面 —— Hello, CSDN!
android·android studio·android 零基础·android 项目结构·android 界面开发
众少成多积小致巨16 小时前
Soong构建入门
android·go·编译器