/* * Copyright (C) 2018 The Android Open Source Project * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package android.view; import static android.view.InsetsSourceProto.FRAME; import static android.view.InsetsSourceProto.TYPE; import static android.view.InsetsSourceProto.TYPE_NUMBER; import static android.view.InsetsSourceProto.VISIBLE; import static android.view.InsetsSourceProto.VISIBLE_FRAME; import static android.view.WindowInsets.Type.captionBar; import static android.view.WindowInsets.Type.ime; import android.annotation.IntDef; import android.annotation.IntRange; import android.annotation.NonNull; import android.annotation.Nullable; import android.graphics.Insets; import android.graphics.Rect; import android.os.Parcel; import android.os.Parcelable; import android.util.proto.ProtoOutputStream; import android.view.WindowInsets.Type.InsetsType; import java.io.PrintWriter; import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; import java.util.ArrayList; import java.util.Arrays; import java.util.Objects; import java.util.StringJoiner; /** * Represents the state of a single entity generating insets for clients. * * @hide */ public class InsetsSource implements Parcelable { @Retention(RetentionPolicy.SOURCE) @IntDef( prefix = "SIDE_", value = {SIDE_NONE, SIDE_LEFT, SIDE_TOP, SIDE_RIGHT, SIDE_BOTTOM, SIDE_UNKNOWN}) public @interface InternalInsetsSide {} static final int SIDE_NONE = 0; static final int SIDE_LEFT = 1; static final int SIDE_TOP = 2; static final int SIDE_RIGHT = 3; static final int SIDE_BOTTOM = 4; static final int SIDE_UNKNOWN = 5; /** The insets source ID of IME */ public static final int ID_IME = createId(null, 0, ime()); /** The insets source ID of the IME caption bar ("fake" IME navigation bar). */ public static final int ID_IME_CAPTION_BAR = InsetsSource.createId(null /* owner */, 1 /* index */, captionBar()); /** * Controls whether this source suppresses the scrim. If the scrim is ignored, the system won't * draw a semi-transparent scrim behind the system bar area even when the bar contrast is * enforced. * * @see android.R.styleable#Window_enforceStatusBarContrast * @see android.R.styleable#Window_enforceNavigationBarContrast */ public static final int FLAG_SUPPRESS_SCRIM = 1; /** * Controls whether the insets frame will be used to move {@link RoundedCorner} inward with the * insets frame size when calculating the rounded corner insets to other windows. * *
For example, task bar will draw fake rounded corners above itself, so we need to move the
* rounded corner up by the task bar insets size to make other windows see a rounded corner
* above the task bar.
*/
public static final int FLAG_INSETS_ROUNDED_CORNER = 1 << 1;
/** Controls whether the insets provided by this source should be forcibly consumed. */
public static final int FLAG_FORCE_CONSUMING = 1 << 2;
/** Controls whether the insets source will play an animation when resizing. */
public static final int FLAG_ANIMATE_RESIZING = 1 << 3;
@Retention(RetentionPolicy.SOURCE)
@IntDef(
flag = true,
prefix = "FLAG_",
value = {
FLAG_SUPPRESS_SCRIM,
FLAG_INSETS_ROUNDED_CORNER,
FLAG_FORCE_CONSUMING,
FLAG_ANIMATE_RESIZING,
})
public @interface Flags {}
/**
* Used when there are no bounding rects to describe an inset, which is only possible when the
* insets itself is {@link Insets#NONE}.
*/
private static final Rect[] NO_BOUNDING_RECTS = new Rect[0];
private @Flags int mFlags;
/** An unique integer to identify this source across processes. */
private final int mId;
private final @InsetsType int mType;
/** Frame of the source in screen coordinate space */
private final Rect mFrame;
private @Nullable Rect mVisibleFrame;
private @Nullable Rect[] mBoundingRects;
private boolean mVisible;
/**
* Used to decide which side of the relative frame should receive insets when the frame fully
* covers the relative frame.
*/
private @InternalInsetsSide int mSideHint = SIDE_NONE;
private final Rect mTmpFrame = new Rect();
private final Rect mTmpBoundingRect = new Rect();
public InsetsSource(int id, @InsetsType int type) {
mId = id;
mType = type;
mFrame = new Rect();
mVisible = (WindowInsets.Type.defaultVisible() & type) != 0;
}
public InsetsSource(InsetsSource other) {
mId = other.mId;
mType = other.mType;
mFrame = new Rect(other.mFrame);
mVisible = other.mVisible;
mVisibleFrame = other.mVisibleFrame != null ? new Rect(other.mVisibleFrame) : null;
mFlags = other.mFlags;
mSideHint = other.mSideHint;
mBoundingRects = other.mBoundingRects != null ? other.mBoundingRects.clone() : null;
}
public void set(InsetsSource other) {
mFrame.set(other.mFrame);
mVisible = other.mVisible;
mVisibleFrame = other.mVisibleFrame != null ? new Rect(other.mVisibleFrame) : null;
mFlags = other.mFlags;
mSideHint = other.mSideHint;
mBoundingRects = other.mBoundingRects != null ? other.mBoundingRects.clone() : null;
}
public InsetsSource setFrame(int left, int top, int right, int bottom) {
mFrame.set(left, top, right, bottom);
return this;
}
public InsetsSource setFrame(Rect frame) {
mFrame.set(frame);
return this;
}
public InsetsSource setVisibleFrame(@Nullable Rect visibleFrame) {
mVisibleFrame = visibleFrame != null ? new Rect(visibleFrame) : null;
return this;
}
public InsetsSource setVisible(boolean visible) {
mVisible = visible;
return this;
}
public InsetsSource setFlags(@Flags int flags) {
mFlags = flags;
return this;
}
public InsetsSource setFlags(@Flags int flags, @Flags int mask) {
mFlags = (mFlags & ~mask) | (flags & mask);
return this;
}
/**
* Updates the side hint which is used to decide which side of the relative frame should receive
* insets when the frame fully covers the relative frame.
*
* @param bounds A rectangle which contains the frame. It will be used to calculate the hint.
*/
public InsetsSource updateSideHint(Rect bounds) {
mSideHint = getInsetSide(calculateInsets(bounds, mFrame, true /* ignoreVisibility */));
return this;
}
/**
* Set the bounding rectangles of this source. They are expected to be relative to the source
* frame.
*/
public InsetsSource setBoundingRects(@Nullable Rect[] rects) {
mBoundingRects = rects != null ? rects.clone() : null;
return this;
}
public int getId() {
return mId;
}
public @InsetsType int getType() {
return mType;
}
public Rect getFrame() {
return mFrame;
}
public @Nullable Rect getVisibleFrame() {
return mVisibleFrame;
}
public boolean isVisible() {
return mVisible;
}
public @Flags int getFlags() {
return mFlags;
}
public boolean hasFlags(int flags) {
return (mFlags & flags) == flags;
}
/** Returns the bounding rectangles of this source. */
public @Nullable Rect[] getBoundingRects() {
return mBoundingRects;
}
/**
* Calculates the insets this source will cause to a client window.
*
* @param relativeFrame The frame to calculate the insets relative to.
* @param ignoreVisibility If true, always reports back insets even if source isn't visible.
* @return The resulting insets. The contract is that only one side will be occupied by a
* source.
*/
public Insets calculateInsets(Rect relativeFrame, boolean ignoreVisibility) {
return calculateInsets(relativeFrame, mFrame, ignoreVisibility);
}
/** Like {@link #calculateInsets(Rect, boolean)}, but will return visible insets. */
public Insets calculateVisibleInsets(Rect relativeFrame) {
return calculateInsets(
relativeFrame,
mVisibleFrame != null ? mVisibleFrame : mFrame,
false /* ignoreVisibility */);
}
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.
// However, with the "fake" IME navigation bar treated as a caption bar, we return the
// insets with the corresponding height the bottom.
if (getType() == WindowInsets.Type.captionBar()) {
return getId() == ID_IME_CAPTION_BAR
? Insets.of(0, 0, 0, frame.height())
: 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());
}
if (mTmpFrame.equals(relativeFrame)) {
// Covering all sides
switch (mSideHint) {
default:
case SIDE_LEFT:
return Insets.of(mTmpFrame.width(), 0, 0, 0);
case SIDE_TOP:
return Insets.of(0, mTmpFrame.height(), 0, 0);
case SIDE_RIGHT:
return Insets.of(0, 0, mTmpFrame.width(), 0);
case SIDE_BOTTOM:
return Insets.of(0, 0, 0, mTmpFrame.height());
}
} else if (mTmpFrame.width() == relativeFrame.width()) {
// Intersecting at top/bottom
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);
}
} else if (mTmpFrame.height() == relativeFrame.height()) {
// Intersecting at left/right
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;
}
/** Calculates the bounding rects the source will cause to a client window. */
public @NonNull Rect[] calculateBoundingRects(Rect relativeFrame, boolean ignoreVisibility) {
if (!ignoreVisibility && !mVisible) {
return NO_BOUNDING_RECTS;
}
final Rect frame = getFrame();
if (mBoundingRects == null) {
// No bounding rects set, make a single bounding rect that covers the intersection of
// the |frame| and the |relativeFrame|. Also make it relative to the window origin.
return mTmpBoundingRect.setIntersect(frame, relativeFrame)
? new Rect[] {
new Rect(
mTmpBoundingRect.left - relativeFrame.left,
mTmpBoundingRect.top - relativeFrame.top,
mTmpBoundingRect.right - relativeFrame.left,
mTmpBoundingRect.bottom - relativeFrame.top)
}
: NO_BOUNDING_RECTS;
}
// Special treatment for captionBar inset type. During drag-resizing, the |frame| and
// |boundingRects| may not get updated as quickly as |relativeFrame|, so just assume the
// |frame| will always be either at the top or bottom of |relativeFrame|. This means some
// calculations to make |boundingRects| relative to |relativeFrame| can be skipped or
// simplified.
// TODO(b/254128050): remove special treatment.
if (getType() == WindowInsets.Type.captionBar()) {
final ArrayList