对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...

相关推荐
Dnelic-2 小时前
【单元测试】【Android】JUnit 4 和 JUnit 5 的差异记录
android·junit·单元测试·android studio·自学笔记
Eastsea.Chen4 小时前
MTK Android12 user版本MtkLogger
android·framework
长亭外的少年11 小时前
Kotlin 编译失败问题及解决方案:从守护进程到 Gradle 配置
android·开发语言·kotlin
建群新人小猿14 小时前
会员等级经验问题
android·开发语言·前端·javascript·php
1024小神15 小时前
tauri2.0版本开发苹果ios和安卓android应用,环境搭建和最后编译为apk
android·ios·tauri
兰琛15 小时前
20241121 android中树结构列表(使用recyclerView实现)
android·gitee
Y多了个想法16 小时前
RK3568 android11 适配敦泰触摸屏 FocalTech-ft5526
android·rk3568·触摸屏·tp·敦泰·focaltech·ft5526
NotesChapter17 小时前
Android吸顶效果,并有着ViewPager左右切换
android
_祝你今天愉快18 小时前
分析android :The binary version of its metadata is 1.8.0, expected version is 1.5.
android
暮志未晚Webgl18 小时前
109. UE5 GAS RPG 实现检查点的存档功能
android·java·ue5