0. 窗口的触摸区域概述

在 Android 系统中一个窗口的触摸区域和窗口区域不一定是一样的。窗口区域是一个 Rect 对象,是一个矩形,而触摸区域是一个 Region 对象,Region 可以包含多个矩形。

在默认情况下,显示区域和窗口区域是一样的。在「Window Touchable Region」中详细说明了触摸区域的传递、计算等逻辑。

1. 获取触摸区域

一个窗口的触摸区域是在 WindowState#getSurfaceTouchabeRegion() 方法中决定的。该方法中分 4 个情况,对于一个非 Activity 窗口会调用 getTouchableRegion() 方法。

// frameworks/base/services/core/java/com/android/server/wm/WindowState.java
/**
 * Flag indicating whether the touchable region should be adjusted by
 * the visible insets; if false the area outside the visible insets is
 * NOT touchable, so we must use those to adjust the frame during hit
 * tests.
 */
int mTouchableInsets = ViewTreeObserver.InternalInsetsInfo.TOUCHABLE_INSETS_FRAME;

/** Get the touchable region in global coordinates. */
void getTouchableRegion(Region outRegion) {
    final Rect frame = mWindowFrames.mFrame;
    switch (mTouchableInsets) {
        default:
        case TOUCHABLE_INSETS_FRAME:
            outRegion.set(frame);
            break;
        case TOUCHABLE_INSETS_CONTENT:
            applyInsets(outRegion, frame, mGivenContentInsets);
            break;
        case TOUCHABLE_INSETS_VISIBLE:
            applyInsets(outRegion, frame, mGivenVisibleInsets);
            break;
        case TOUCHABLE_INSETS_REGION: {
            outRegion.set(mGivenTouchableRegion);
            outRegion.translate(frame.left, frame.top);
            break;
        }
    }
    cropRegionToStackBoundsIfNeeded(outRegion);
    subtractTouchExcludeRegionIfNeeded(outRegion);
}

getTouchableRegion() 方法根据 mTouchableInsets 也分 4 种情况。mTouchableInsets 默认值是 TOUCHABLE_INSETS_FRAME,所以触摸区域是 mWindowFrames.mFrame,即窗口区域。如果是其他 3 种情况会分别使用 mGivenContentInsets、mGivenVisibleInsets 和 mGivenTouchableRegion。

这三个变量的设置在 ViewTreeObserver 类中留有接口,但一直都是 @hide 的,所以只有系统应用可以修改触摸区域。

2. 设置触摸区域

ViewTreeObserver 类中有个内部类接口 OnComputeInternalInsetsListener,当应用实现该回调接口后,ViewRootImpl 会在每次绘制过程中会回调 onComputeInternalInsets()。我们可以在该回调方法中修改触摸区域。

// frameworks/base/core/java/android/view/ViewRootImpl.java
private void performTraversals() {
    // Determine whether to compute insets.
    // If there are no inset listeners remaining then we may still need to compute
    // insets in case the old insets were non-empty and must be reset.
    final boolean computesInternalInsets =
            mAttachInfo.mTreeObserver.hasComputeInternalInsetsListeners()
            || mAttachInfo.mHasNonEmptyGivenInternalInsets;
    ...
    if (computesInternalInsets) {
        // Clear the original insets.
        final ViewTreeObserver.InternalInsetsInfo insets = mAttachInfo.mGivenInternalInsets;
        insets.reset();

        // Compute new insets in place.
        mAttachInfo.mTreeObserver.dispatchOnComputeInternalInsets(insets);
        ...
    }
    ...
}

// frameworks/base/core/java/android/view/ViewTreeObserver.java
/**
 * Calls all listeners to compute the current insets.
 */
@UnsupportedAppUsage
final void dispatchOnComputeInternalInsets(InternalInsetsInfo inoutInfo) {
    final CopyOnWriteArray<OnComputeInternalInsetsListener> listeners =
            mOnComputeInternalInsetsListeners;
    if (listeners != null && listeners.size() > 0) {
        CopyOnWriteArray.Access<OnComputeInternalInsetsListener> access = listeners.start();
        try {
            int count = access.size();
            for (int i = 0; i < count; i++) {
                access.get(i).onComputeInternalInsets(inoutInfo);
            }
        } finally {
            listeners.end();
}}}

回调方法 onComputeInternalInsets() 中会传递一个 InternalInsetsInfo 对象,我们调用 setTouchableInsets() 方法修改 TouchabeInsets,然后修改 touchableRegion 就可以修改触摸区域了。

view.getViewTreeObserver().addOnComputeInternalInsetsListener(
        new OnComputeInternalInsetsListener() {
    @Overrice
    public void onComputeInternalInsets(InternalInsetsInfo inoutInfo) {
        inoutInfo.setTouchableInsets(InternalInsetsInfo.TOUCHABLE_INSETS_REGION);
        inoutInfo.touchableRegion.set(left, top, right, bottom);
    }
});

3. 触摸区域的传递

设置完 InternalInsetsInfo 的 touchableRegion 后,ViewRootImpl 会调用 IWindowSession 的 setInsets() 方法把 touchableRegion 传递给 WindowState 类的 mGivenTouchableRegion。

// frameworks/base/core/java/android/view/ViewRootImpl.java
private void performTraversals() {
    ...
    // Compute new insets in place.
    mAttachInfo.mTreeObserver.dispatchOnComputeInternalInsets(insets);

    // Tell the window manager.
    if (insetsPending || !mLastGivenInsets.equals(insets)) {
        mLastGivenInsets.set(insets);

        // Translate insets to screen coordinates if needed.
        final Rect contentInsets;
        final Rect visibleInsets;
        final Region touchableRegion;
        if (mTranslator != null) {
            contentInsets = mTranslator.getTranslatedContentInsets(insets.contentInsets);
            visibleInsets = mTranslator.getTranslatedVisibleInsets(insets.visibleInsets);
            touchableRegion = mTranslator.getTranslatedTouchableArea(insets.touchableRegion);
        } else {
            contentInsets = insets.contentInsets;
            visibleInsets = insets.visibleInsets;
            touchableRegion = insets.touchableRegion;
        }

        try {
            mWindowSession.setInsets(mWindow, insets.mTouchableInsets,
                    contentInsets, visibleInsets, touchableRegion);
        } catch (RemoteException e) {
    }}
    ...
}

IWindowSession 的 setInsets() 方法最终会调用 WindowManagerService 的 setInsetsWindow() 方法设置该 WindowState 的 mGivenTouchableRegion。

// frameworks/base/services/core/java/com/android/server/wm/WindowManagerService.java
void setInsetsWindow(Session session, IWindow client, int touchableInsets, Rect contentInsets,
        Rect visibleInsets, Region touchableRegion) {
    ...
    WindowState w = windowForClientLocked(session, client, false);
    if (w != null) {
        w.mGivenInsetsPending = false;
        w.mGivenContentInsets.set(contentInsets);
        w.mGivenVisibleInsets.set(visibleInsets);
        w.mGivenTouchableRegion.set(touchableRegion);
        w.mTouchableInsets = touchableInsets;
        if (w.mGlobalScale != 1) {
            w.mGivenContentInsets.scale(w.mGlobalScale);
            w.mGivenVisibleInsets.scale(w.mGlobalScale);
            w.mGivenTouchableRegion.scale(w.mGlobalScale);
        }
        w.setDisplayLayoutNeeded();
        mWindowPlacerLocked.performSurfacePlacement();
        ...
}}

4. 总结