本文主要聚焦于 App 端的 Insets(不是 WMS 端),从以下几个问题入手 Insets 相关的源码。
- 什么是 Insets ?
- Insets 来自哪里
- Insets 如何分发给 View
1. 什么是 Insets ?
Insets 在中文语境中,没有一个特别准确的对应词,Google 的文档以及博客将其翻译为边衬区。
Insets 描述的是显示屏幕上的一些矩形区域,这些区域是系统 UI 所在的区域,常见的有 StatusBar NavigationBar IME(输入法)等
既然是系统 UI,那为什么 App 端还需要去关注 Insets 呢?Activity 如果进行了显示方面的配置,Activity 的显示区域可能就会和 Insets 重叠。
比如我们配置了全屏的主题:
android:theme="@android:style/Theme.Black.NoTitleBar.Fullscreen"
或者配置了 window 的全屏 flag:
requestWindowFeature(Window.FEATURE_NO_TITLE);
getWindow().setFlags(WindowManager.LayoutParams.FLAG_FULLSCREEN,
WindowManager.LayoutParams.FLAG_FULLSCREEN);
那么 Activity 就会从 statusbar 下面开始布局和绘制:
要处理这种 App 界面与系统 UI 界面重叠的情况都需要了解 Insets。
最常见的场景就是沉浸式状态栏,沉浸式状态栏应该是每个 Android App 开发都处理过的恶心需求,不同的系统版本,不同的手机品牌(小米,魅族)都需要单独的适配!!!
我们可以通过 adb shell dumpsys activity 命令可以查看到 Insets 相关的信息:
那么源码中如何表示/描述一个 Insets,在 App 端使用 Insets 类:
// /frameworks/base/graphics/java/android/graphics/Insets.java
public final class Insets implements Parcelable {
public static final @NonNull Insets NONE = new Insets(0, 0, 0, 0);
public final int left;
public final int top;
public final int right;
public final int bottom;
private Insets(int left, int top, int right, int bottom) {
this.left = left;
this.top = top;
this.right = right;
this.bottom = bottom;
}
// ......
}
举个 Insets 对象的实际例子:
这里的 left top right bottom,描述的是一个偏移量,图片中有颜色的区域就是 insets 所在区域。
Insets 有很多类型,比如:
- StatusBar
- NavigationBar
- 系统手势区
- 刘海区
- 输入法
- .......
WindowInsets.Type 类中定义了多个常量来表示 Insets 的类型:
// /frameworks/base/core/java/android/view/WindowInsets.java
public final class WindowInsets {
// ......
public static final class Type {
static final int FIRST = 1;
static final int STATUS_BARS = FIRST;
static final int NAVIGATION_BARS = 1 << 1;
static final int CAPTION_BAR = 1 << 2;
static final int IME = 1 << 3;
static final int SYSTEM_GESTURES = 1 << 4;
static final int MANDATORY_SYSTEM_GESTURES = 1 << 5;
static final int TAPPABLE_ELEMENT = 1 << 6;
static final int DISPLAY_CUTOUT = 1 << 7;
static final int LAST = 1 << 8;
static final int SIZE = 9;
static final int WINDOW_DECOR = LAST;
private Type() {}
//......
}
// .....
}
具体每个类型的 Inset 的介绍可以查看官方博客处理视觉冲突 | 手势导航 (二)
2. Insets 来自哪里
2.1 App 中如何获取到 Insets
首先从 App 开发的角度,我们可以通过 View 的 setOnApplyWindowInsetsListener 方法设置一个回调对象来获取到 Insets:
val main:View = findViewById(R.id.main)
main.setOnApplyWindowInsetsListener { v, insets ->
val systemBars = insets.getInsets(WindowInsetsCompat.Type.systemBars())
Log.d("zzh","top is ${systemBars.top}")
v.setPadding(systemBars.left, systemBars.top, systemBars.right, systemBars.bottom)
insets
}
如果是通用 App,推荐使用 AndroidX 提供的兼容库来实现:
ViewCompat.setOnApplyWindowInsetsListener(findViewById(R.id.main)) { v, insets ->
val systemBars = insets.getInsets(WindowInsetsCompat.Type.systemBars())
v.setPadding(systemBars.left, systemBars.top, systemBars.right, systemBars.bottom)
Log.d("zzh","top is ${systemBars.top}")
insets
}
setOnApplyWindowInsetsListener 中返回的数据的类型是 WindowInsets。
WindowInset 中有一个成员 private final Insets[] mTypeInsetsMap; 该成员保存了所有的 Insets。
// /frameworks/base/core/java/android/view/WindowInsets.java
public final class WindowInsets {
// ......
private final Insets[] mTypeInsetsMap;
// ......
public Insets getInsets(@InsetsType int typeMask) {
return getInsets(mTypeInsetsMap, typeMask);
}
static Insets getInsets(Insets[] typeInsetsMap, @InsetsType int typeMask) {
Insets result = null;
for (int i = FIRST; i <= LAST; i = i << 1) {
if ((typeMask & i) == 0) {
continue;
}
Insets insets = typeInsetsMap[indexOf(i)];
if (insets == null) {
continue;
}
if (result == null) {
result = insets;
} else {
result = Insets.max(result, insets);
}
}
return result == null ? Insets.NONE : result;
}
// ......
}
我们只需要传入一个 WindowInsets.Type 类型,就可以通过 WindowInsets::getInsets 方法获取到具体类型的 Insets 对象。
2.2 获取 InsetsState
那么这里的 Inset 是从哪里来的呢?
在 Activity/Window 的显示过程中,相关的调用链如下:
ViewRootImpl::setView
ViewRootImpl::requestLayout
ViewRootImpl::scheduleTraversals
ViewRootImpl.TraversalRunnable::run -- 异步操作
ViewRootImpl::doTraversal
ViewRootImpl::performTraversals
ViewRootImpl::dispatchApplyInsets -- 第二步,计算并分发 Insets
ViewRootImpl::relayoutWindow
mWindowSession::relayout -- 第三步,获取 InsetsSourceControl
InsetsController::onControlsChanged -- 分发 InsetsSourceControl
Session.addToDisplayAsUser -- 第一步,获取 InsetsState
InsetsController::onStateChanged -- 分发 InsetsState
在 addToDisplayAsUser 的时候,会从 wms 中获取到 private final InsetsState mTempInsets
// # ViewRootImpl
private final InsetsState mTempInsets = new InsetsState();
res = mWindowSession.addToDisplayAsUser(mWindow, mWindowAttributes,
getHostVisibility(), mDisplay.getDisplayId(), userId,
mInsetsController.getRequestedVisibleTypes(), inputChannel, mTempInsets,
mTempControls, attachedFrame, compatScale);
InsetsState 又是什么?
InsetsState 保存了系统中所有的 Insets 的状态/信息
public class InsetsState implements Parcelable {
//......
private final SparseArray<InsetsSource> mSources;
// 基于 mSources 计算出 windowInsets
WindowInsets calculateInsets(...) {
//......
}
// // 根据计算值更新 source 值
// void processSource(InsetsSource source,...){
// // .....
// }
//......
}
InsetsState 内部成员 private final SparseArray<InsetsSource> mSources; 对应了每一个 Insets。
从 adb shell dumpsys activity 打印的信息可以看出对应关系:
InsetsSource id=f8ac0001 type=navigationBars frame=[0,2274][1080,2400] visible=true flags= insetsRoundedCornerFrame=false
InsetsSource id=f8ac0004 type=systemGestures frame=[0,0][0,0] visible=true flags= insetsRoundedCornerFrame=false
InsetsSource id=f8ac0005 type=mandatorySystemGestures frame=[0,2274][1080,2400] visible=true flags= insetsRoundedCornerFrame=false
InsetsSource id=f8ac0006 type=tappableElement frame=[0,2274][1080,2400] visible=true flags= insetsRoundedCornerFrame=false
InsetsSource id=f8ac0024 type=systemGestures frame=[0,0][0,0] visible=true flags= insetsRoundedCornerFrame=false
InsetsSource id=3 type=ime frame=[0,0][0,0] visible=false flags= insetsRoundedCornerFrame=false
InsetsSource id=27 type=displayCutout frame=[0,0][1080,128] visible=true flags= insetsRoundedCornerFrame=false
InsetsSource id=13c0000 type=statusBars frame=[0,0][1080,128] visible=true flags= insetsRoundedCornerFrame=false
InsetsSource id=13c0005 type=mandatorySystemGestures frame=[0,0][1080,160] visible=true flags= insetsRoundedCornerFrame=false
InsetsSource id=13c0006 type=tappableElement frame=[0,0][1080,128] visible=true flags= insetsRoundedCornerFrame=false
InsetsSource 的实现如下:
public class InsetsSource implements Parcelable {
//......
private final @InternalInsetsType int mType; // Insets 类型
private final Rect mFrame; //代表 Insets 区域
private boolean mVisible; //Insets 可见性
//......
}
InsetsState 中还有一个重要的方法 calculateInsets,该方法基于 mSources 计算出 windowInsets。这里没有具体的情景,源码我们先不分析。
2.3 获取 InsetsSourceControl
App 在 relayout 的时候,会从 wms 中获取到 InsetsSourceControl.Array mTempControls
// # ViewRootImpl
private final InsetsSourceControl.Array mTempControls = new InsetsSourceControl.Array();
relayoutResult = mWindowSession.relayout(mWindow, params,
requestedWidth, requestedHeight, viewVisibility,
insetsPending ? WindowManagerGlobal.RELAYOUT_INSETS_PENDING : 0, mRelayoutSeq,
mLastSyncSeqId, mTmpFrames, mPendingMergedConfiguration, mSurfaceControl,
mTempInsets, mTempControls, mRelayoutBundle);
// /frameworks/base/core/java/android/view/InsetsSourceControl.java
// # InsetsSourceControl.Array
public static class Array implements Parcelable {
private @Nullable InsetsSourceControl[] mControls;
// ......
}
可以看出 InsetsSourceControl.Array 实际就是 InsetsSourceControl 的数组。
InsetsSourceControl 对象与一个具体的 Insets 对应,主要用于控制 Insets 的显示与隐藏,输入法会涉及的场景会比较多一些。
public class InsetsSourceControl implements Parcelable {
private final int mId;
private final @InsetsType int mType;
private final Point mSurfacePosition;
private Insets mInsetsHint;
//......
}
通过 adb shell dumpsys activity 命令可以查看到 InsetsSourceControl 相关的信息:
InsetsSourceControl: {13c0000 mType=statusBars initiallyVisible mSurfacePosition=Point(0, 0) mInsetsHint=Insets{left=0, top=128, right=0, bottom=0}}
InsetsSourceControl: {f8ac0001 mType=navigationBars initiallyVisible mSurfacePosition=Point(0, 2274) mInsetsHint=Insets{left=0, top=0, right=0, bottom=126}}
InsetsSourceControl: {3 mType=ime mSurfacePosition=Point(0, 128) mInsetsHint=Insets{left=0, top=0, right=0, bottom=0}}
3. Insets 相关数据的分发
3.1 InsetsState 的分发
在调用 Session.addToDisplayAsUser 获取到 InsetsState 后,会调用到 InsetsController::onStateChanged 方法。
这里使用到了 InsetsController,它又是干什么的?
每个 窗口/ViewRootImpl 对应一个 InsetController,用于控制 insets 的显示,隐藏,状态更新以及动画的调度。
InsetsController 在 ViewRootImpl 初始化时构建并初始化。
private final InsetsController mInsetsController;
public ViewRootImpl(@UiContext Context context, Display display, IWindowSession session,
WindowLayout windowLayout) {
//.....
// 初始化
mInsetsController = new InsetsController(new ViewRootInsetsControllerHost(this));
//......
}
InsetsController 的实现如下:
// /frameworks/base/core/java/android/view/InsetsController.java
public class InsetsController implements WindowInsetsController, InsetsAnimationControlCallbacks {
// ......
private final Host mHost;
private final InsetsState mState = new InsetsState();
private final SparseArray<InsetsSourceControl> mTmpControlArray = new SparseArray<>();
private final TriFunction<InsetsController, Integer, Integer, InsetsSourceConsumer> mConsumerCreator;
private final SparseArray<InsetsSourceConsumer> mSourceConsumers = new SparseArray<>();
private final InsetsSourceConsumer mImeSourceConsumer;
// ......
public InsetsController(Host host) {
this(host, (controller, id, type) -> {
if (type == ime()) {
return new ImeInsetsSourceConsumer(id, controller.mState,
Transaction::new, controller);
} else {
return new InsetsSourceConsumer(id, type, controller.mState,
Transaction::new, controller);
}
}, host.getHandler());
}
@VisibleForTesting
public InsetsController(Host host,
TriFunction<InsetsController, Integer, Integer, InsetsSourceConsumer> consumerCreator,
Handler handler) {
mHost = host;
mConsumerCreator = consumerCreator;
mHandler = handler;
mAnimCallback = () -> {
//......
};
// Make mImeSourceConsumer always non-null.
mImeSourceConsumer = getSourceConsumer(ID_IME, ime());
}
// ......
@VisibleForTesting
public @NonNull InsetsSourceConsumer getSourceConsumer(int id, int type) {
InsetsSourceConsumer consumer = mSourceConsumers.get(id);
if (consumer != null) {
return consumer;
}
if (type == ime() && mImeSourceConsumer != null) {
// WindowInsets.Type.ime() should be only provided by one source.
mSourceConsumers.remove(mImeSourceConsumer.getId());
consumer = mImeSourceConsumer;
consumer.setId(id);
} else {
consumer = mConsumerCreator.apply(this, id, type);
}
mSourceConsumers.put(id, consumer);
return consumer;
}
// ......
}
InsetsController 有一个重要成员 SparseArray<InsetsSourceConsumer> mSourceConsumers,是 InsetsSource 的消费者。
InsetsController 的另一个成员 InsetsSourceConsumer mImeSourceConsumer;(InsetsSourceConsumer 的子类) 是 Ime InsetSource 的消费者。
InsetsSourceConsumer 中有几个重要的方法:
public void updateSource(InsetsSource newSource, @AnimationType int animationType) //更新 InsetsSource 成员
public int requestShow(boolean fromController, @Nullable ImeTracker.Token statsToken) //显示Insets
void requestHide(boolean fromController, @Nullable ImeTracker.Token statsToken) //隐藏Insets
这里消费的意思是什么? 应该是指执行 Insets 的 show or hide 操作。
InsetsController 的另外一个成员 mConsumerCreator 用于生成 InsetsSourceConsumer。mConsumerCreator 在构造函数中初始化。
接下来就来看 onStateChanged 方法的实现:
// # InsetsController
@VisibleForTesting
public boolean onStateChanged(InsetsState state) {
boolean stateChanged = false;
if (!CAPTION_ON_SHELL) {
stateChanged = !mState.equals(state, true /* excludingCaptionInsets */,
false /* excludeInvisibleIme */)
|| captionInsetsUnchanged();
} else {
stateChanged = !mState.equals(state, false /* excludingCaptionInsets */,
false /* excludeInvisibleIme */);
}
if (!stateChanged && mLastDispatchedState.equals(state)) {
return false;
}
if (DEBUG) Log.d(TAG, "onStateChanged: " + state);
mLastDispatchedState.set(state, true /* copySources */);
final InsetsState lastState = new InsetsState(mState, true /* copySources */);
// 关注点
// 把 state 拷贝到 mState 中
updateState(state);
applyLocalVisibilityOverride();
updateCompatSysUiVisibility();
if (!mState.equals(lastState, false /* excludingCaptionInsets */,
true /* excludeInvisibleIme */)) {
if (DEBUG) Log.d(TAG, "onStateChanged, notifyInsetsChanged");
mHost.notifyInsetsChanged();
if (lastState.getDisplayFrame().equals(mState.getDisplayFrame())) {
InsetsState.traverse(lastState, mState, mStartResizingAnimationIfNeeded);
}
}
return true;
}
这里我们主要关心 updateState 方法,该方法会把从 WMS 中获取到的 InsetState 保存到 InsetsController 的成员 private final InsetsState mState 中。
3.2 dispatchApplyInsets 计算和分发 Insets
接下来会调用到 dispatchApplyInsets 方法,从名字就可以看出来,该方法用于分发 Insets:
// # ViewRootImpl
public void dispatchApplyInsets(View host) {
Trace.traceBegin(Trace.TRACE_TAG_VIEW, "dispatchApplyInsets");
mApplyInsetsRequested = false;
// 关注点1,计算 Insets
WindowInsets insets = getWindowInsets(true /* forceConstruct */);
if (!shouldDispatchCutout()) {
// Window is either not laid out in cutout or the status bar inset takes care of
// clearing the cutout, so we don't need to dispatch the cutout to the hierarchy.
insets = insets.consumeDisplayCutout();
}
// 关注点2,分发 Insets
host.dispatchApplyWindowInsets(insets);
mAttachInfo.delayNotifyContentCaptureInsetsEvent(insets.getInsets(Type.all()));
Trace.traceEnd(Trace.TRACE_TAG_VIEW);
}
这里主要两个关键点:
- getWindowInsets,计算 Insets,实际就是将从 WMS 获取到的 InsetsState 转换为 App 需要的 InsetState
- dispatchApplyWindowInsets,将 WindowInsets 分发给 View
3.2.1 getWindowInsets 计算 Insets
getWindowInsets 的实现如下:
// # ViewRootImpl
private WindowInsets mLastWindowInsets;
WindowInsets getWindowInsets(boolean forceConstruct) {
if (mLastWindowInsets == null || forceConstruct) {
final Configuration config = getConfiguration();
// 计算 Insets
mLastWindowInsets = mInsetsController.calculateInsets(
config.isScreenRound(), mAttachInfo.mAlwaysConsumeSystemBars,
mWindowAttributes.type, config.windowConfiguration.getWindowingMode(),
mWindowAttributes.softInputMode, mWindowAttributes.flags,
(mWindowAttributes.systemUiVisibility
| mWindowAttributes.subtreeSystemUiVisibility));
mAttachInfo.mContentInsets.set(mLastWindowInsets.getSystemWindowInsets().toRect());
mAttachInfo.mStableInsets.set(mLastWindowInsets.getStableInsets().toRect());
mAttachInfo.mVisibleInsets.set(mInsetsController.calculateVisibleInsets(
mWindowAttributes.type, config.windowConfiguration.getWindowingMode(),
mWindowAttributes.softInputMode, mWindowAttributes.flags).toRect());
}
return mLastWindowInsets;
}
接着调用 InsetsController::calculateInsets 计算 Insets:
// # InsetsController
private WindowInsets mLastInsets;
private final InsetsState mState = new InsetsState();
@VisibleForTesting
public WindowInsets calculateInsets(boolean isScreenRound, boolean alwaysConsumeSystemBars,
int windowType, int windowingMode, int legacySoftInputMode, int legacyWindowFlags,
int legacySystemUiFlags) {
mWindowType = windowType;
mLastWindowingMode = windowingMode;
mLastLegacySoftInputMode = legacySoftInputMode;
mLastLegacyWindowFlags = legacyWindowFlags;
mLastLegacySystemUiFlags = legacySystemUiFlags;
//
mLastInsets = mState.calculateInsets(mFrame, null /* ignoringVisibilityState*/,
isScreenRound, alwaysConsumeSystemBars, legacySoftInputMode, legacyWindowFlags,
legacySystemUiFlags, windowType, windowingMode, null /* idSideMap */);
return mLastInsets;
}
接着调用 InsetsState::calculateInsets:
// # InsetsState
private final SparseArray<InsetsSource> mSources;
public WindowInsets calculateInsets(Rect frame, @Nullable InsetsState ignoringVisibilityState,
boolean isScreenRound, boolean alwaysConsumeSystemBars,
int legacySoftInputMode, int legacyWindowFlags, int legacySystemUiFlags,
int windowType, @WindowConfiguration.WindowingMode int windowingMode,
@Nullable @InternalInsetsSide SparseIntArray idSideMap) {
Insets[] typeInsetsMap = new Insets[Type.SIZE];
Insets[] typeMaxInsetsMap = new Insets[Type.SIZE];
boolean[] typeVisibilityMap = new boolean[Type.SIZE];
final Rect relativeFrame = new Rect(frame);
final Rect relativeFrameMax = new Rect(frame);
@InsetsType int suppressScrimTypes = 0;
for (int i = mSources.size() - 1; i >= 0; i--) {
final InsetsSource source = mSources.valueAt(i);
if ((source.getFlags() & InsetsSource.FLAG_SUPPRESS_SCRIM) != 0) {
suppressScrimTypes |= source.getType();
}
// 关注点1,InsetsSource 转换为 Insets,保存到 typeInsetsMap 中
processSource(source, relativeFrame, false /* ignoreVisibility */, typeInsetsMap,
idSideMap, typeVisibilityMap);
// 关注点2,输入法单独处理,流程大体一致
// IME won't be reported in max insets as the size depends on the EditorInfo of the IME
// target.
if (source.getType() != WindowInsets.Type.ime()) {
InsetsSource ignoringVisibilitySource = ignoringVisibilityState != null
? ignoringVisibilityState.peekSource(source.getId())
: source;
if (ignoringVisibilitySource == null) {
continue;
}
processSource(ignoringVisibilitySource, relativeFrameMax,
true /* ignoreVisibility */, typeMaxInsetsMap, null /* idSideMap */,
null /* typeVisibilityMap */);
}
}
final int softInputAdjustMode = legacySoftInputMode & SOFT_INPUT_MASK_ADJUST;
@InsetsType int compatInsetsTypes = systemBars() | displayCutout();
if (softInputAdjustMode == SOFT_INPUT_ADJUST_RESIZE) {
compatInsetsTypes |= ime();
}
if ((legacyWindowFlags & FLAG_FULLSCREEN) != 0) {
compatInsetsTypes &= ~statusBars();
}
if (clearsCompatInsets(windowType, legacyWindowFlags, windowingMode)
&& !alwaysConsumeSystemBars) {
compatInsetsTypes = 0;
}
// 关注点,3,new 一个 WindowInsets 返回
return new WindowInsets(typeInsetsMap, typeMaxInsetsMap, typeVisibilityMap, isScreenRound,
alwaysConsumeSystemBars, suppressScrimTypes, calculateRelativeCutout(frame),
calculateRelativeRoundedCorners(frame),
calculateRelativePrivacyIndicatorBounds(frame),
calculateRelativeDisplayShape(frame),
compatInsetsTypes, (legacySystemUiFlags & SYSTEM_UI_FLAG_LAYOUT_STABLE) != 0);
}
接着看
// # InsetsState
private void processSource(InsetsSource source, Rect relativeFrame, boolean ignoreVisibility,
Insets[] typeInsetsMap, @Nullable @InternalInsetsSide SparseIntArray idSideMap,
@Nullable boolean[] typeVisibilityMap) {
Insets insets = source.calculateInsets(relativeFrame, ignoreVisibility);
final int type = source.getType();
processSourceAsPublicType(source, typeInsetsMap, idSideMap, typeVisibilityMap,
insets, type);
if (type == Type.MANDATORY_SYSTEM_GESTURES) {
// Mandatory system gestures are also system gestures.
// TODO: find a way to express this more generally. One option would be to define
// Type.systemGestureInsets() as NORMAL | MANDATORY, but then we lose the
// ability to set systemGestureInsets() independently from
// mandatorySystemGestureInsets() in the Builder.
processSourceAsPublicType(source, typeInsetsMap, idSideMap, typeVisibilityMap,
insets, Type.SYSTEM_GESTURES);
}
if (type == Type.CAPTION_BAR) {
// Caption should also be gesture and tappable elements. This should not be needed when
// the caption is added from the shell, as the shell can add other types at the same
// time.
processSourceAsPublicType(source, typeInsetsMap, idSideMap, typeVisibilityMap,
insets, Type.SYSTEM_GESTURES);
processSourceAsPublicType(source, typeInsetsMap, idSideMap, typeVisibilityMap,
insets, Type.MANDATORY_SYSTEM_GESTURES);
processSourceAsPublicType(source, typeInsetsMap, idSideMap, typeVisibilityMap,
insets, Type.TAPPABLE_ELEMENT);
}
}
接着调用 calculateInsets 方法
// # InsetsSource
private Insets calculateInsets(Rect relativeFrame, Rect frame, boolean ignoreVisibility) {
if (!ignoreVisibility && !mVisible) {
return Insets.NONE;
}
// During drag-move and drag-resizing, the caption insets position may not get updated
// before the app frame get updated. To layout the app content correctly during drag events,
// we always return the insets with the corresponding height covering the top.
if (getType() == WindowInsets.Type.captionBar()) {
return Insets.of(0, frame.height(), 0, 0);
}
// Checks for whether there is shared edge with insets for 0-width/height window.
final boolean hasIntersection = relativeFrame.isEmpty()
? getIntersection(frame, relativeFrame, mTmpFrame)
: mTmpFrame.setIntersect(frame, relativeFrame);
if (!hasIntersection) {
return Insets.NONE;
}
// TODO: Currently, non-floating IME always intersects at bottom due to issues with cutout.
// However, we should let the policy decide from the server.
if (getType() == WindowInsets.Type.ime()) {
return Insets.of(0, 0, 0, mTmpFrame.height());
}
// Intersecting at top/bottom
if (mTmpFrame.width() == relativeFrame.width()) {
if (mTmpFrame.top == relativeFrame.top) {
return Insets.of(0, mTmpFrame.height(), 0, 0);
} else if (mTmpFrame.bottom == relativeFrame.bottom) {
return Insets.of(0, 0, 0, mTmpFrame.height());
}
// TODO: remove when insets are shell-customizable.
// This is a hack that says "if this is a top-inset (eg statusbar), always apply it
// to the top". It is used when adjusting primary split for IME.
if (mTmpFrame.top == 0) {
return Insets.of(0, mTmpFrame.height(), 0, 0);
}
}
// Intersecting at left/right
else if (mTmpFrame.height() == relativeFrame.height()) {
if (mTmpFrame.left == relativeFrame.left) {
return Insets.of(mTmpFrame.width(), 0, 0, 0);
} else if (mTmpFrame.right == relativeFrame.right) {
return Insets.of(0, 0, mTmpFrame.width(), 0);
}
}
return Insets.NONE;
}
不同的条件生成不同的 Insets 返回。
processSourceAsPublicType 将生成的 Insets 插入会数组中。
// # InsetsState
private void processSourceAsPublicType(InsetsSource source, Insets[] typeInsetsMap,
@InternalInsetsSide @Nullable SparseIntArray idSideMap,
@Nullable boolean[] typeVisibilityMap, Insets insets, int type) {
int index = indexOf(type);
Insets existing = typeInsetsMap[index];
if (existing == null) {
typeInsetsMap[index] = insets;
} else {
typeInsetsMap[index] = Insets.max(existing, insets);
}
if (typeVisibilityMap != null) {
typeVisibilityMap[index] = source.isVisible();
}
if (idSideMap != null) {
@InternalInsetsSide int insetSide = getInsetSide(insets);
if (insetSide != ISIDE_UNKNOWN) {
idSideMap.put(source.getId(), insetSide);
}
}
}
3.2.2 dispatchApplyWindowInsets 分发 Insets
我们接着看 dispatchApplyWindowInsets 分发 Insets 的过程:
// # ViewGroup
@Override
public WindowInsets dispatchApplyWindowInsets(WindowInsets insets) {
insets = super.dispatchApplyWindowInsets(insets);
if (insets.isConsumed()) {
return insets;
}
if (View.sBrokenInsetsDispatch) {
return brokenDispatchApplyWindowInsets(insets);
} else {
return newDispatchApplyWindowInsets(insets);
}
}
无论那种情况都会去调用 View::dispatchApplyWindowInsets
// # ViewGroup
private WindowInsets brokenDispatchApplyWindowInsets(WindowInsets insets) {
final int count = getChildCount();
for (int i = 0; i < count; i++) {
insets = getChildAt(i).dispatchApplyWindowInsets(insets);
if (insets.isConsumed()) {
break;
}
}
return insets;
}
private WindowInsets newDispatchApplyWindowInsets(WindowInsets insets) {
final int count = getChildCount();
for (int i = 0; i < count; i++) {
getChildAt(i).dispatchApplyWindowInsets(insets);
}
return insets;
}
View::dispatchApplyWindowInsets 的实现如下:
// # View
public WindowInsets dispatchApplyWindowInsets(WindowInsets insets) {
try {
mPrivateFlags3 |= PFLAG3_APPLYING_INSETS;
if (mListenerInfo != null && mListenerInfo.mOnApplyWindowInsetsListener != null) {
return mListenerInfo.mOnApplyWindowInsetsListener.onApplyWindowInsets(this, insets);
} else {
return onApplyWindowInsets(insets);
}
} finally {
mPrivateFlags3 &= ~PFLAG3_APPLYING_INSETS;
}
}
优先调用 Listener,其次调用覆写方法,二选一,不会被同时调用。
3.3 InsetsSourceControl 的分发
我们接着看 onControlsChanged 分发 InsetsSourceControl 的过程:
// # InsetsController
private final SparseArray<InsetsSourceControl> mTmpControlArray = new SparseArray<>();
private final SparseArray<InsetsSourceConsumer> mSourceConsumers = new SparseArray<>();
public void onControlsChanged(InsetsSourceControl[] activeControls) {
if (activeControls != null) {
for (InsetsSourceControl activeControl : activeControls) {
if (activeControl != null) {
// TODO(b/122982984): Figure out why it can be null.
mTmpControlArray.put(activeControl.getId(), activeControl);
}
}
}
@InsetsType int controllableTypes = 0;
int consumedControlCount = 0;
final @InsetsType int[] showTypes = new int[1];
final @InsetsType int[] hideTypes = new int[1];
// Ensure to update all existing source consumers
for (int i = mSourceConsumers.size() - 1; i >= 0; i--) {
final InsetsSourceConsumer consumer = mSourceConsumers.valueAt(i);
final InsetsSourceControl control = mTmpControlArray.get(consumer.getId());
if (control != null) {
controllableTypes |= control.getType();
consumedControlCount++;
}
// control may be null, but we still need to update the control to null if it got
// revoked.
consumer.setControl(control, showTypes, hideTypes);
}
// Ensure to create source consumers if not available yet.
if (consumedControlCount != mTmpControlArray.size()) {
for (int i = mTmpControlArray.size() - 1; i >= 0; i--) {
final InsetsSourceControl control = mTmpControlArray.valueAt(i);
// Consumer 和 Control 关联起来
getSourceConsumer(control.getId(), control.getType())
.setControl(control, showTypes, hideTypes);
}
}
if (mTmpControlArray.size() > 0) {
// Update surface positions for animations.
for (int i = mRunningAnimations.size() - 1; i >= 0; i--) {
mRunningAnimations.get(i).runner.updateSurfacePosition(mTmpControlArray);
}
}
mTmpControlArray.clear();
// Do not override any animations that the app started in the OnControllableInsetsChanged
// listeners.
int animatingTypes = invokeControllableInsetsChangedListeners();
showTypes[0] &= ~animatingTypes;
hideTypes[0] &= ~animatingTypes;
if (showTypes[0] != 0) {
applyAnimation(showTypes[0], true /* show */, false /* fromIme */,
null /* statsToken */);
}
if (hideTypes[0] != 0) {
applyAnimation(hideTypes[0], false /* show */, false /* fromIme */,
null /* statsToken */);
}
if (mControllableTypes != controllableTypes) {
if (WindowInsets.Type.hasCompatSystemBars(mControllableTypes ^ controllableTypes)) {
mCompatSysUiVisibilityStaled = true;
}
mControllableTypes = controllableTypes;
}
// InsetsSourceConsumer#setControl might change the requested visibility.
reportRequestedVisibleTypes();
}
// .....
}
核心两点:
- 把 Control 保存到 mTmpControlArray 中
- 把 consumer 和 control 关联起来,他两一起负责 Insets 的显示与隐藏
最后给出一个相关类的类图: