blob: dda399357d8c8840e9dcfb1d57a2f690b6e6663b [file] [log] [blame]
/*
* Copyright (C) 2021 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.InsetsSource.ID_IME;
import static android.view.ViewGroup.LayoutParams.MATCH_PARENT;
import static android.view.WindowInsets.Type.navigationBars;
import static android.view.WindowInsets.Type.systemBars;
import static android.view.WindowManager.LayoutParams.FLAG_LAYOUT_INSET_DECOR;
import static android.view.WindowManager.LayoutParams.FLAG_LAYOUT_IN_SCREEN;
import static android.view.WindowManager.LayoutParams.FLAG_LAYOUT_NO_LIMITS;
import static android.view.WindowManager.LayoutParams.FLAG_SCALED;
import static android.view.WindowManager.LayoutParams.LAYOUT_IN_DISPLAY_CUTOUT_MODE_ALWAYS;
import static android.view.WindowManager.LayoutParams.LAYOUT_IN_DISPLAY_CUTOUT_MODE_DEFAULT;
import static android.view.WindowManager.LayoutParams.LAYOUT_IN_DISPLAY_CUTOUT_MODE_SHORT_EDGES;
import static android.view.WindowManager.LayoutParams.PRIVATE_FLAG_INSET_PARENT_FRAME_BY_IME;
import static android.view.WindowManager.LayoutParams.PRIVATE_FLAG_LAYOUT_CHILD_WINDOW_IN_PARENT_FRAME;
import static android.view.WindowManager.LayoutParams.PRIVATE_FLAG_LAYOUT_SIZE_EXTENDED_BY_CUTOUT;
import static android.view.WindowManager.LayoutParams.TYPE_BASE_APPLICATION;
import static android.view.WindowManager.LayoutParams.TYPE_INPUT_METHOD;
import static android.view.WindowManager.LayoutParams.TYPE_SYSTEM_ERROR;
import android.app.WindowConfiguration;
import android.app.WindowConfiguration.WindowingMode;
import android.graphics.Insets;
import android.graphics.Point;
import android.graphics.Rect;
import android.util.Log;
import android.view.WindowInsets.Type.InsetsType;
import android.window.ClientWindowFrames;
/**
* Computes window frames.
* @hide
*/
public class WindowLayout {
private static final String TAG = WindowLayout.class.getSimpleName();
private static final boolean DEBUG = false;
public static final int UNSPECIFIED_LENGTH = -1;
/** These coordinates are the borders of the window layout. */
static final int MIN_X = -100000;
static final int MIN_Y = -100000;
static final int MAX_X = 100000;
static final int MAX_Y = 100000;
private final Rect mTempDisplayCutoutSafeExceptMaybeBarsRect = new Rect();
private final Rect mTempRect = new Rect();
public void computeFrames(WindowManager.LayoutParams attrs, InsetsState state,
Rect displayCutoutSafe, Rect windowBounds, @WindowingMode int windowingMode,
int requestedWidth, int requestedHeight, @InsetsType int requestedVisibleTypes,
float compatScale, ClientWindowFrames frames) {
final int type = attrs.type;
final int fl = attrs.flags;
final int pfl = attrs.privateFlags;
final boolean layoutInScreen = (fl & FLAG_LAYOUT_IN_SCREEN) == FLAG_LAYOUT_IN_SCREEN;
final Rect attachedWindowFrame = frames.attachedFrame;
final Rect outDisplayFrame = frames.displayFrame;
final Rect outParentFrame = frames.parentFrame;
final Rect outFrame = frames.frame;
// Compute bounds restricted by insets
final Insets insets = state.calculateInsets(windowBounds, attrs.getFitInsetsTypes(),
attrs.isFitInsetsIgnoringVisibility());
final @WindowInsets.Side.InsetsSide int sides = attrs.getFitInsetsSides();
final int left = (sides & WindowInsets.Side.LEFT) != 0 ? insets.left : 0;
final int top = (sides & WindowInsets.Side.TOP) != 0 ? insets.top : 0;
final int right = (sides & WindowInsets.Side.RIGHT) != 0 ? insets.right : 0;
final int bottom = (sides & WindowInsets.Side.BOTTOM) != 0 ? insets.bottom : 0;
outDisplayFrame.set(windowBounds.left + left, windowBounds.top + top,
windowBounds.right - right, windowBounds.bottom - bottom);
if (attachedWindowFrame == null) {
outParentFrame.set(outDisplayFrame);
if ((pfl & PRIVATE_FLAG_INSET_PARENT_FRAME_BY_IME) != 0) {
final InsetsSource source = state.peekSource(ID_IME);
if (source != null) {
outParentFrame.inset(source.calculateInsets(
outParentFrame, false /* ignoreVisibility */));
}
}
} else {
outParentFrame.set(!layoutInScreen ? attachedWindowFrame : outDisplayFrame);
}
// Compute bounds restricted by display cutout
final int cutoutMode = attrs.layoutInDisplayCutoutMode;
final DisplayCutout cutout = state.getDisplayCutout();
final Rect displayCutoutSafeExceptMaybeBars = mTempDisplayCutoutSafeExceptMaybeBarsRect;
displayCutoutSafeExceptMaybeBars.set(displayCutoutSafe);
frames.isParentFrameClippedByDisplayCutout = false;
if (cutoutMode != LAYOUT_IN_DISPLAY_CUTOUT_MODE_ALWAYS && !cutout.isEmpty()) {
// Ensure that windows with a non-ALWAYS display cutout mode are laid out in
// the cutout safe zone.
final Rect displayFrame = state.getDisplayFrame();
if (cutoutMode == LAYOUT_IN_DISPLAY_CUTOUT_MODE_SHORT_EDGES) {
if (displayFrame.width() < displayFrame.height()) {
displayCutoutSafeExceptMaybeBars.top = MIN_Y;
displayCutoutSafeExceptMaybeBars.bottom = MAX_Y;
} else {
displayCutoutSafeExceptMaybeBars.left = MIN_X;
displayCutoutSafeExceptMaybeBars.right = MAX_X;
}
}
final boolean layoutInsetDecor = (attrs.flags & FLAG_LAYOUT_INSET_DECOR) != 0;
if (layoutInScreen && layoutInsetDecor
&& (cutoutMode == LAYOUT_IN_DISPLAY_CUTOUT_MODE_DEFAULT
|| cutoutMode == LAYOUT_IN_DISPLAY_CUTOUT_MODE_SHORT_EDGES)) {
final Insets systemBarsInsets = state.calculateInsets(
displayFrame, systemBars(), requestedVisibleTypes);
if (systemBarsInsets.left >= cutout.getSafeInsetLeft()) {
displayCutoutSafeExceptMaybeBars.left = MIN_X;
}
if (systemBarsInsets.top >= cutout.getSafeInsetTop()) {
displayCutoutSafeExceptMaybeBars.top = MIN_Y;
}
if (systemBarsInsets.right >= cutout.getSafeInsetRight()) {
displayCutoutSafeExceptMaybeBars.right = MAX_X;
}
if (systemBarsInsets.bottom >= cutout.getSafeInsetBottom()) {
displayCutoutSafeExceptMaybeBars.bottom = MAX_Y;
}
}
if (type == TYPE_INPUT_METHOD
&& displayCutoutSafeExceptMaybeBars.bottom != MAX_Y
&& state.calculateInsets(displayFrame, navigationBars(), true).bottom > 0) {
// The IME can always extend under the bottom cutout if the navbar is there.
displayCutoutSafeExceptMaybeBars.bottom = MAX_Y;
}
final boolean attachedInParent = attachedWindowFrame != null && !layoutInScreen;
// TYPE_BASE_APPLICATION windows are never considered floating here because they don't
// get cropped / shifted to the displayFrame in WindowState.
final boolean floatingInScreenWindow = !attrs.isFullscreen() && layoutInScreen
&& type != TYPE_BASE_APPLICATION;
// Windows that are attached to a parent and laid out in said parent already avoid
// the cutout according to that parent and don't need to be further constrained.
// Floating IN_SCREEN windows get what they ask for and lay out in the full screen.
// They will later be cropped or shifted using the displayFrame in WindowState,
// which prevents overlap with the DisplayCutout.
if (!attachedInParent && !floatingInScreenWindow) {
mTempRect.set(outParentFrame);
outParentFrame.intersectUnchecked(displayCutoutSafeExceptMaybeBars);
frames.isParentFrameClippedByDisplayCutout = !mTempRect.equals(outParentFrame);
}
outDisplayFrame.intersectUnchecked(displayCutoutSafeExceptMaybeBars);
}
final boolean noLimits = (attrs.flags & FLAG_LAYOUT_NO_LIMITS) != 0;
final boolean inMultiWindowMode = WindowConfiguration.inMultiWindowMode(windowingMode);
// TYPE_SYSTEM_ERROR is above the NavigationBar so it can't be allowed to extend over it.
// Also, we don't allow windows in multi-window mode to extend out of the screen.
if (noLimits && type != TYPE_SYSTEM_ERROR && !inMultiWindowMode) {
outDisplayFrame.left = MIN_X;
outDisplayFrame.top = MIN_Y;
outDisplayFrame.right = MAX_X;
outDisplayFrame.bottom = MAX_Y;
}
final boolean hasCompatScale = compatScale != 1f;
final int pw = outParentFrame.width();
final int ph = outParentFrame.height();
final boolean extendedByCutout =
(attrs.privateFlags & PRIVATE_FLAG_LAYOUT_SIZE_EXTENDED_BY_CUTOUT) != 0;
int rw = requestedWidth;
int rh = requestedHeight;
float x, y;
int w, h;
// If the view hierarchy hasn't been measured, the requested width and height would be
// UNSPECIFIED_LENGTH. This can happen in the first layout of a window or in the simulated
// layout. If extendedByCutout is true, we cannot use the requested lengths. Otherwise,
// the window frame might be extended again because the requested lengths may come from the
// window frame.
if (rw == UNSPECIFIED_LENGTH || extendedByCutout) {
rw = attrs.width >= 0 ? attrs.width : pw;
}
if (rh == UNSPECIFIED_LENGTH || extendedByCutout) {
rh = attrs.height >= 0 ? attrs.height : ph;
}
if ((attrs.flags & FLAG_SCALED) != 0) {
if (attrs.width < 0) {
w = pw;
} else if (hasCompatScale) {
w = (int) (attrs.width * compatScale + .5f);
} else {
w = attrs.width;
}
if (attrs.height < 0) {
h = ph;
} else if (hasCompatScale) {
h = (int) (attrs.height * compatScale + .5f);
} else {
h = attrs.height;
}
} else {
if (attrs.width == MATCH_PARENT) {
w = pw;
} else if (hasCompatScale) {
w = (int) (rw * compatScale + .5f);
} else {
w = rw;
}
if (attrs.height == MATCH_PARENT) {
h = ph;
} else if (hasCompatScale) {
h = (int) (rh * compatScale + .5f);
} else {
h = rh;
}
}
if (hasCompatScale) {
x = attrs.x * compatScale;
y = attrs.y * compatScale;
} else {
x = attrs.x;
y = attrs.y;
}
if (inMultiWindowMode
&& (attrs.privateFlags & PRIVATE_FLAG_LAYOUT_CHILD_WINDOW_IN_PARENT_FRAME) == 0) {
// Make sure window fits in parent frame since it is in a non-fullscreen task as
// required by {@link Gravity#apply} call.
w = Math.min(w, pw);
h = Math.min(h, ph);
}
// We need to fit it to the display if either
// a) The window is in a fullscreen container, or we don't have a task (we assume fullscreen
// for the taskless windows)
// b) If it's a secondary app window, we also need to fit it to the display unless
// FLAG_LAYOUT_NO_LIMITS is set. This is so we place Popups, dialogs, and similar windows on
// screen, but SurfaceViews want to be always at a specific location so we don't fit it to
// the display.
final boolean fitToDisplay = !inMultiWindowMode
|| ((attrs.type != TYPE_BASE_APPLICATION) && !noLimits);
// Set mFrame
Gravity.apply(attrs.gravity, w, h, outParentFrame,
(int) (x + attrs.horizontalMargin * pw),
(int) (y + attrs.verticalMargin * ph), outFrame);
// Now make sure the window fits in the overall display frame.
if (fitToDisplay) {
Gravity.applyDisplay(attrs.gravity, outDisplayFrame, outFrame);
}
if (extendedByCutout) {
extendFrameByCutout(displayCutoutSafe, outDisplayFrame, outFrame,
mTempRect);
}
if (DEBUG) Log.d(TAG, "computeFrames " + attrs.getTitle()
+ " frames=" + frames
+ " windowBounds=" + windowBounds.toShortString()
+ " requestedWidth=" + requestedWidth
+ " requestedHeight=" + requestedHeight
+ " compatScale=" + compatScale
+ " windowingMode=" + WindowConfiguration.windowingModeToString(windowingMode)
+ " displayCutoutSafe=" + displayCutoutSafe
+ " attrs=" + attrs
+ " state=" + state
+ " requestedInvisibleTypes=" + WindowInsets.Type.toString(~requestedVisibleTypes));
}
public static void extendFrameByCutout(Rect displayCutoutSafe,
Rect displayFrame, Rect inOutFrame, Rect tempRect) {
if (displayCutoutSafe.contains(inOutFrame)) {
return;
}
tempRect.set(inOutFrame);
// Move the frame into displayCutoutSafe.
Gravity.applyDisplay(0 /* gravity */, displayCutoutSafe, tempRect);
if (tempRect.intersect(displayFrame)) {
inOutFrame.union(tempRect);
}
}
public static void computeSurfaceSize(WindowManager.LayoutParams attrs, Rect maxBounds,
int requestedWidth, int requestedHeight, Rect winFrame, boolean dragResizing,
Point outSurfaceSize) {
int width;
int height;
if ((attrs.flags & WindowManager.LayoutParams.FLAG_SCALED) != 0) {
// For a scaled surface, we always want the requested size.
width = requestedWidth;
height = requestedHeight;
} else {
// When we're doing a drag-resizing, request a surface that's fullscreen size,
// so that we don't need to reallocate during the process. This also prevents
// buffer drops due to size mismatch.
if (dragResizing) {
// The maxBounds should match the display size which applies fixed-rotation
// transformation if there is any.
width = maxBounds.width();
height = maxBounds.height();
} else {
width = winFrame.width();
height = winFrame.height();
}
}
// This doesn't necessarily mean that there is an error in the system. The sizes might be
// incorrect, because it is before the first layout or draw.
if (width < 1) {
width = 1;
}
if (height < 1) {
height = 1;
}
// Adjust for surface insets.
final Rect surfaceInsets = attrs.surfaceInsets;
width += surfaceInsets.left + surfaceInsets.right;
height += surfaceInsets.top + surfaceInsets.bottom;
outSurfaceSize.set(width, height);
}
}