在 Android 系统中一个窗口的触摸区域和窗口区域不一定是一样的。窗口区域是一个 Rect 对象,是一个矩形,而触摸区域是一个 Region 对象,Region 可以包含多个矩形。
在默认情况下,显示区域和窗口区域是一样的。在「Window Touchable Region」中详细说明了触摸区域的传递、计算等逻辑。
一个窗口的触摸区域是在 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
的,所以只有系统应用可以修改触摸区域。
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);
}
});
设置完 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();
...
}}