Android 焦点获取:舞台聚光灯下的演员选拔故事

69 阅读4分钟

一、申请聚光灯:演员的 "上台资格" 审查

想象一个 Android 舞台(Activity)上,每个 View 都是等待聚光灯的演员。当你调用button.requestFocus()时,就像演员对导演说:"我想站在聚光灯下!"

但导演不会随便给灯,首先会检查演员的 "资格证":

java

private boolean requestFocusNoSearch(int direction, Rect previouslyFocusedRect) {
    // 1. 必须有"可聚焦"资格证(focusable=true)且在舞台上可见
    if ((mViewFlags & FOCUSABLE) != FOCUSABLE || (mViewFlags & VISIBILITY_MASK) != VISIBLE) {
        return false; // 没资格,直接淘汰
    }
    
    // 2. 如果是触摸模式(观众用手指点选),还得有"触摸模式资格证"
    if (isInTouchMode() && (FOCUSABLE_IN_TOUCH_MODE != (mViewFlags & FOCUSABLE_IN_TOUCH_MODE))) {
        return false; // 触摸模式下没资格,淘汰
    }
    
    // 3. 检查有没有上级导演拦着(父容器设置了FOCUS_BLOCK_DESCENDANTS)
    if (hasAncestorThatBlocksDescendantFocus()) {
        return false; // 被上级拦住,不让上台
    }
    
    handleFocusGainInternal(direction, previouslyFocusedRect);
    return true; // 资格通过,准备上台
}

故事版解读

  • 就像演员需要带齐证件(focusable 和 visibility),触摸模式下还得额外带 "触摸许可证"(focusableInTouchMode)
  • 如果演员的爸爸(父容器)对导演说 "别让我儿子上台"(FOCUS_BLOCK_DESCENDANTS),直接被拒

二、舞台调度:旧演员下台,新演员登记

通过资格审查后,演员会走到舞台中央,但导演需要先让原来的聚光灯演员下台:

java

void handleFocusGainInternal(...) {
    // 给自己贴一个"聚光灯演员"的标签
    mPrivateFlags |= PFLAG_FOCUSED;
    
    // 找到当前站在聚光灯下的旧演员
    View oldFocus = getRootView().findFocus();
    
    // 告诉爸爸(父容器):"我要上台了!"
    if (mParent != null) {
        mParent.requestChildFocus(this, this); // 通知父容器更新焦点
    }
    
    // 通知观众(ViewTreeObserver)聚光灯换演员了
    if (mAttachInfo != null) {
        mAttachInfo.mTreeObserver.dispatchOnGlobalFocusChange(oldFocus, this);
    }
    
    // 演员自己换衣服(刷新样式)
    onFocusChanged(true, ...);
    refreshDrawableState();
}

父容器(导演组)接到通知后,会执行调度逻辑:

java

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

故事版解读

  • 新演员(View)给自己戴一个 "当前焦点" 的徽章(mPrivateFlags 标记位)
  • 导演组(ViewGroup)先让旧演员(oldFocus)下台,刷新他的服装(失焦样式)
  • 然后把新演员的名字记在导演组的小本本上(mFocused 变量),并层层上报给总导演(ViewRootImpl)
  • 总导演收到通知后,喊一声:"灯光师准备,换焦点啦!"(scheduleTraversals 触发 UI 重绘)

三、导演组的三种 "偏心" 策略

不同的导演组(ViewGroup)有不同的选角策略,决定谁优先上台:

java

@Override
public boolean requestFocus(int direction, Rect previouslyFocusedRect) {
    int strategy = getDescendantFocusability();
    switch (strategy) {
        case FOCUS_BLOCK_DESCENDANTS:
            // 导演自己想上台,不让小孩上
            return super.requestFocus(direction, previouslyFocusedRect);
            
        case FOCUS_BEFORE_DESCENDANTS:
            // 导演先自己试试,不行再让小孩上
            if (super.requestFocus(direction, previouslyFocusedRect)) {
                return true;
            } else {
                return onRequestFocusInDescendants(direction, previouslyFocusedRect);
            }
            
        case FOCUS_AFTER_DESCENDANTS:
            // 先让所有小孩试,都不行导演自己上
            if (onRequestFocusInDescendants(direction, previouslyFocusedRect)) {
                return true;
            } else {
                return super.requestFocus(direction, previouslyFocusedRect);
            }
    }
}

其中onRequestFocusInDescendants是遍历所有小演员(子 View):

java

protected boolean onRequestFocusInDescendants(int direction, Rect previouslyFocusedRect) {
    // 根据方向决定从左到右还是从右到左遍历演员
    int index = (direction & FOCUS_FORWARD) != 0 ? 0 : mChildrenCount - 1;
    int increment = (direction & FOCUS_FORWARD) != 0 ? 1 : -1;
    int end = (direction & FOCUS_FORWARD) != 0 ? mChildrenCount : -1;
    
    // 遍历所有可见的小演员,让他们申请焦点
    for (int i = index; i != end; i += increment) {
        View child = mChildren[i];
        if (child.isVisible() && child.requestFocus(direction, previouslyFocusedRect)) {
            return true; // 有小演员成功上台,结束选拔
        }
    }
    return false;
}

故事版解读

  • FOCUS_BLOCK_DESCENDANTS:导演组老大说 "我自己上,小孩别抢",直接自己申请焦点
  • FOCUS_BEFORE_DESCENDANTS:导演先试镜,如果自己演不好(requestFocus 失败),再让小演员一个个试
  • FOCUS_AFTER_DESCENDANTS:先让所有小演员排队试镜,直到没人能演,导演才自己上

四、关键角色总结

故事角色Android 术语核心作用
演员View申请焦点的控件,需满足资格条件
导演组ViewGroup管理子 View 焦点,执行调度策略
总导演ViewRootImpl最终触发 UI 重绘,更新焦点显示
聚光灯徽章mPrivateFlags(PFLAG_FOCUSED)标记当前焦点状态
导演组小本本mFocused记录当前焦点所在的子 View
资格证focusable/focusableInTouchMode决定 View 能否获取焦点
导演组命令FOCUS_BLOCK_DESCENDANTS 等控制焦点分配策略

五、小白可操作的代码示例

java

// 1. 给按钮设置焦点资格证
Button button = findViewById(R.id.my_button);
button.setFocusable(true);         // 普通资格证
button.setFocusableInTouchMode(true); // 触摸模式资格证

// 2. 让按钮申请聚光灯
button.requestFocus();

// 3. 监听聚光灯变化(演员换装通知)
button.setOnFocusChangeListener((v, hasFocus) -> {
    if (hasFocus) {
        v.setBackgroundColor(Color.YELLOW); // 获得焦点时变黄
    } else {
        v.setBackgroundColor(Color.GRAY);   // 失去焦点时变灰
    }
});

// 4. 给父容器设置导演策略(比如先让小孩上)
ViewGroup parent = findViewById(R.id.parent_layout);
parent.setDescendantFocusability(ViewGroup.FOCUS_AFTER_DESCENDANTS);

通过这个故事,你可以理解:焦点获取本质是 View 向父容器申请资格,父容器按策略调度,最终更新状态并通知 UI 的过程。就像舞台上的聚光灯,只有满足条件的演员才能站上去,而导演组决定了谁优先获得这个机会。