第五章:ViewGroup焦点策略源码解析

102 阅读4分钟

5.1 descendantFocusability 机制深度解析

属性定义与作用

descendantFocusability 是ViewGroup的关键属性,决定其如何处理子视图的焦点:

// ViewGroup.java
public static final int FOCUS_BEFORE_DESCENDANTS = 0x20000;
public static final int FOCUS_AFTER_DESCENDANTS = 0x40000;
public static final int FOCUS_BLOCK_DESCENDANTS = 0x60000;
常量行为
0FOCUS_BEFORE_DESCENDANTSViewGroup优先获取焦点
1FOCUS_AFTER_DESCENDANTS子视图优先获取焦点
2FOCUS_BLOCK_DESCENDANTS阻止子视图获取焦点

源码实现

// ViewGroup.java
public void addFocusables(ArrayList<View> views, int direction) {
    final int focusableCount = views.size();
    
    // 1. FOCUS_BEFORE_DESCENDANTS: 先添加自身
    if (mDescendantFocusability != FOCUS_AFTER_DESCENDANTS) {
        super.addFocusables(views, direction);
    }
    
    // 2. 添加可聚焦的子视图
    final int count = mChildrenCount;
    final View[] children = mChildren;
    for (int i = 0; i < count; i++) {
        View child = children[i];
        if ((child.mViewFlags & VISIBILITY_MASK) == VISIBLE) {
            child.addFocusables(views, direction);
        }
    }
    
    // 3. FOCUS_AFTER_DESCENDANTS: 最后添加自身
    if (mDescendantFocusability == FOCUS_AFTER_DESCENDANTS && 
        focusableCount == views.size()) {
        super.addFocusables(views, direction);
    }
}

5.2 焦点请求处理流程

请求焦点时的决策

// ViewGroup.java
@Override
public boolean requestChildFocus(View child, View focused) {
    // 1. 更新当前焦点子视图
    if (mFocused != child) {
        if (mFocused != null) {
            mFocused.unFocus(); // 清除原焦点
        }
        mFocused = child; // 设置新焦点
    }
    
    // 2. 处理滚动容器的自动滚动
    if (mParent != null) {
        mParent.requestChildFocus(this, focused);
        
        // 如果是ScrollView,自动滚动到子视图
        if (isScrollContainer()) {
            scrollToDescendant(focused);
        }
    }
    return true;
}

ScrollView的滚动实现

// ScrollView.java
@Override
public void requestChildFocus(View child, View focused) {
    if (!mIsLayoutDirty) {
        // 立即滚动到焦点视图
        scrollToChild(focused);
    } else {
        // 布局未完成时延迟滚动
        mChildToScrollTo = focused;
    }
    super.requestChildFocus(child, focused);
}

private void scrollToChild(View child) {
    child.getDrawingRect(mTempRect);
    
    // 坐标转换
    offsetDescendantRectToMyCoords(child, mTempRect);
    
    // 计算滚动距离
    int scrollDelta = computeScrollDeltaToGetChildRectOnScreen(mTempRect);
    
    if (scrollDelta != 0) {
        scrollBy(0, scrollDelta);
    }
}

5.3 RecyclerView焦点优化

焦点查找特殊处理

// RecyclerView.java
@Override
public View focusSearch(View focused, int direction) {
    // 1. 委托给LayoutManager处理
    View result = mLayout.onInterceptFocusSearch(focused, direction);
    if (result != null) {
        return result;
    }
    
    // 2. 处理自定义焦点逻辑
    final boolean canRunFocusFailure = mLayout != null && mAdapter != null;
    
    // 3. 处理方向键导航
    if (canRunFocusFailure && 
        (direction == View.FOCUS_UP || direction == View.FOCUS_DOWN)) {
        return handleGridFocusSearch(focused, direction);
    }
    
    return super.focusSearch(focused, direction);
}

网格布局焦点处理

// GridLayoutManager.java
@Override
public View onInterceptFocusSearch(View focused, int direction) {
    final int currentPos = getPosition(focused);
    
    switch (direction) {
        case View.FOCUS_UP:
            return findViewByPosition(currentPos - mSpanCount);
        case View.FOCUS_DOWN:
            return findViewByPosition(currentPos + mSpanCount);
        case View.FOCUS_LEFT:
            return findViewByPosition(currentPos - 1);
        case View.FOCUS_RIGHT:
            return findViewByPosition(currentPos + 1);
    }
    return null;
}

5.4 焦点边界处理

边界循环逻辑

// 实现环形焦点导航
@Override
public View focusSearch(View focused, int direction) {
    int currentIndex = indexOfChild(focused);
    
    switch (direction) {
        case FOCUS_RIGHT:
            if (currentIndex < getChildCount() - 1) {
                return getChildAt(currentIndex + 1);
            } else {
                return getChildAt(0); // 循环到第一个
            }
        case FOCUS_LEFT:
            if (currentIndex > 0) {
                return getChildAt(currentIndex - 1);
            } else {
                return getChildAt(getChildCount() - 1); // 循环到最后一个
            }
    }
    return super.focusSearch(focused, direction);
}

边界检测算法

// FocusFinder.java
private boolean isCandidate(Rect focusedRect, Rect candidateRect, int direction) {
    switch (direction) {
        case FOCUS_RIGHT:
            // 候选视图必须在焦点视图右侧
            return focusedRect.right <= candidateRect.right;
        case FOCUS_LEFT:
            // 候选视图必须在焦点视图左侧
            return focusedRect.left >= candidateRect.left;
        // ... 其他方向类似
    }
    return false;
}

5.5 焦点性能优化

优化策略

  1. 空间分区索引

    // 使用空间索引加速查找
    SparseArray<View> focusableViews = new SparseArray<>();
    
    void indexFocusables() {
        ArrayList<View> focusables = new ArrayList<>();
        addFocusables(focusables, View.FOCUS_FORWARD);
        
        for (View v : focusables) {
            Rect rect = new Rect();
            v.getFocusedRect(rect);
            focusableViews.put(hashPosition(rect), v);
        }
    }
    
  2. 延迟计算

    // 仅在布局变化时重新计算
    @Override
    protected void onLayout(boolean changed, int l, int t, int r, int b) {
        super.onLayout(changed, l, t, r, b);
        if (changed) {
            indexFocusables();
        }
    }
    
  3. 方向预过滤

    // 只考虑当前方向的候选视图
    void addFocusables(ArrayList<View> views, int direction) {
        // 根据方向过滤视图
        for (View child : children) {
            if (isInDirection(child, direction)) {
                views.add(child);
            }
        }
    }
    

5.6 常见问题解决方案

问题1:焦点被父容器拦截

// 解决方案:设置正确的descendantFocusability
viewGroup.setDescendantFocusability(ViewGroup.FOCUS_AFTER_DESCENDANTS);

问题2:滚动容器不自动滚动

// 确保容器可滚动且子视图正确请求焦点
scrollView.setScrollContainer(true);
childView.requestFocus();

问题3:RecyclerView焦点跳跃

// 自定义LayoutManager处理焦点
recyclerView.setLayoutManager(new LinearLayoutManager(context) {
    @Override
    public View onInterceptFocusSearch(View focused, int direction) {
        // 自定义焦点逻辑
    }
});

5.7 调试工具与技术

焦点边界可视化

// 在onDraw中绘制焦点边界
@Override
protected void onDraw(Canvas canvas) {
    super.onDraw(canvas);
    
    if (isFocused()) {
        Rect rect = new Rect();
        getFocusedRect(rect);
        
        Paint paint = new Paint();
        paint.setColor(Color.RED);
        paint.setStyle(Paint.Style.STROKE);
        paint.setStrokeWidth(2);
        
        canvas.drawRect(rect, paint);
    }
}

ADB调试命令

# 查看ViewGroup的焦点策略
adb shell dumpsys activity top | grep -A 10 "Focusability"

# 检查descendantFocusability值
adb shell dumpsys view --property descendantFocusability com.example.app

本章小结

  1. descendantFocusability机制

    • 控制ViewGroup与子视图的焦点获取优先级
    • 三种策略:BEFORE_DESCENDANTS, AFTER_DESCENDANTS, BLOCK_DESCENDANTS
  2. 滚动容器焦点处理

    • ScrollView自动滚动到焦点子视图
    • RecyclerView委托LayoutManager处理焦点查找
  3. 焦点边界处理

    • 边界循环导航实现
    • 方向性候选视图筛选
  4. 性能优化策略

    • 空间分区索引
    • 延迟计算
    • 方向预过滤
  5. 常见问题解决

    • 焦点被拦截:调整descendantFocusability
    • 滚动不生效:确保容器可滚动
    • RecyclerView焦点跳跃:自定义LayoutManager
  6. 调试技术

    • 焦点边界可视化
    • ADB命令检查焦点策略

关键源码路径
frameworks/base/core/java/android/view/ViewGroup.java
frameworks/base/core/java/android/widget/ScrollView.java
frameworks/base/core/java/android/support/v7/widget/RecyclerView.java

在下一章中,我们将探讨自定义焦点控制的高级技巧,包括如何重写焦点行为、构建动态焦点链以及解决复杂场景下的焦点冲突。