Merge "Display Cutout: Fix ActionBarOverlayLayout to properly dispatch cutout" into pi-dev

This commit is contained in:
TreeHugger Robot
2018-05-18 22:12:30 +00:00
committed by Android (Google) Code Review
7 changed files with 398 additions and 50 deletions

View File

@@ -6449,6 +6449,7 @@ Landroid/view/WindowContentFrameStats;->init(J[J[J[J)V
Landroid/view/WindowInsets;-><init>(Landroid/graphics/Rect;)V
Landroid/view/WindowInsets;->CONSUMED:Landroid/view/WindowInsets;
Landroid/view/WindowInsets;->getSystemWindowInsets()Landroid/graphics/Rect;
Landroid/view/WindowInsets;->inset(IIII)Landroid/view/WindowInsets;
Landroid/view/WindowLeaked;-><init>(Ljava/lang/String;)V
Landroid/view/WindowManager$LayoutParams;->backup()V
Landroid/view/WindowManager$LayoutParams;->FLAG_SLIPPERY:I

View File

@@ -9838,26 +9838,20 @@ public class View implements Drawable.Callback, KeyEvent.Callback,
/**
* @hide Compute the insets that should be consumed by this view and the ones
* that should propagate to those under it.
*
* Note: This is used by appcompat's ActionBarOverlayLayout through reflection.
*
* @param inoutInsets the insets given to this view
* @param outLocalInsets the insets that should be applied to this view
* @deprecated use {@link #computeSystemWindowInsets}
* @return
*/
@Deprecated
protected boolean computeFitSystemWindows(Rect inoutInsets, Rect outLocalInsets) {
if ((mViewFlags & OPTIONAL_FITS_SYSTEM_WINDOWS) == 0
|| mAttachInfo == null
|| ((mAttachInfo.mSystemUiVisibility & SYSTEM_UI_LAYOUT_FLAGS) == 0
&& !mAttachInfo.mOverscanRequested)) {
outLocalInsets.set(inoutInsets);
inoutInsets.set(0, 0, 0, 0);
return true;
} else {
// The application wants to take care of fitting system window for
// the content... however we still need to take care of any overscan here.
final Rect overscan = mAttachInfo.mOverscanInsets;
outLocalInsets.set(overscan);
inoutInsets.left -= overscan.left;
inoutInsets.top -= overscan.top;
inoutInsets.right -= overscan.right;
inoutInsets.bottom -= overscan.bottom;
return false;
}
WindowInsets innerInsets = computeSystemWindowInsets(new WindowInsets(inoutInsets),
outLocalInsets);
inoutInsets.set(innerInsets.getSystemWindowInsets());
return innerInsets.isSystemWindowInsetsConsumed();
}
/**
@@ -9873,12 +9867,16 @@ public class View implements Drawable.Callback, KeyEvent.Callback,
public WindowInsets computeSystemWindowInsets(WindowInsets in, Rect outLocalInsets) {
if ((mViewFlags & OPTIONAL_FITS_SYSTEM_WINDOWS) == 0
|| mAttachInfo == null
|| (mAttachInfo.mSystemUiVisibility & SYSTEM_UI_LAYOUT_FLAGS) == 0) {
|| ((mAttachInfo.mSystemUiVisibility & SYSTEM_UI_LAYOUT_FLAGS) == 0
&& !mAttachInfo.mOverscanRequested)) {
outLocalInsets.set(in.getSystemWindowInsets());
return in.consumeSystemWindowInsets();
return in.consumeSystemWindowInsets().inset(outLocalInsets);
} else {
outLocalInsets.set(0, 0, 0, 0);
return in;
// The application wants to take care of fitting system window for
// the content... however we still need to take care of any overscan here.
final Rect overscan = mAttachInfo.mOverscanInsets;
outLocalInsets.set(overscan);
return in.inset(outLocalInsets);
}
}

View File

@@ -20,6 +20,10 @@ package android.view;
import android.annotation.Nullable;
import android.graphics.Rect;
import com.android.internal.util.Preconditions;
import java.util.Objects;
/**
* Describes a set of insets for window content.
*
@@ -27,6 +31,12 @@ import android.graphics.Rect;
* To adjust insets, use one of the supplied clone methods to obtain a new WindowInsets instance
* with the adjusted properties.</p>
*
* <p>Note: Before {@link android.os.Build.VERSION_CODES#P P}, WindowInsets instances were only
* immutable during a single layout pass (i.e. would return the same values between
* {@link View#onApplyWindowInsets} and {@link View#onLayout}, but could return other values
* otherwise). Starting with {@link android.os.Build.VERSION_CODES#P P}, WindowInsets are
* always immutable and implement equality.
*
* @see View.OnApplyWindowInsetsListener
* @see View#onApplyWindowInsets(WindowInsets)
*/
@@ -69,13 +79,14 @@ public final class WindowInsets {
public WindowInsets(Rect systemWindowInsets, Rect windowDecorInsets, Rect stableInsets,
boolean isRound, boolean alwaysConsumeNavBar, DisplayCutout displayCutout) {
mSystemWindowInsetsConsumed = systemWindowInsets == null;
mSystemWindowInsets = mSystemWindowInsetsConsumed ? EMPTY_RECT : systemWindowInsets;
mSystemWindowInsets = mSystemWindowInsetsConsumed
? EMPTY_RECT : new Rect(systemWindowInsets);
mWindowDecorInsetsConsumed = windowDecorInsets == null;
mWindowDecorInsets = mWindowDecorInsetsConsumed ? EMPTY_RECT : windowDecorInsets;
mWindowDecorInsets = mWindowDecorInsetsConsumed ? EMPTY_RECT : new Rect(windowDecorInsets);
mStableInsetsConsumed = stableInsets == null;
mStableInsets = mStableInsetsConsumed ? EMPTY_RECT : stableInsets;
mStableInsets = mStableInsetsConsumed ? EMPTY_RECT : new Rect(stableInsets);
mIsRound = isRound;
mAlwaysConsumeNavBar = alwaysConsumeNavBar;
@@ -535,4 +546,104 @@ public final class WindowInsets {
+ (isRound() ? " round" : "")
+ "}";
}
/**
* Returns a copy of this instance inset in the given directions.
*
* @see #inset(int, int, int, int)
* @hide
*/
public WindowInsets inset(Rect r) {
return inset(r.left, r.top, r.right, r.bottom);
}
/**
* Returns a copy of this instance inset in the given directions.
*
* This is intended for dispatching insets to areas of the window that are smaller than the
* current area.
*
* <p>Example:
* <pre>
* childView.dispatchApplyWindowInsets(insets.inset(
* childMarginLeft, childMarginTop, childMarginBottom, childMarginRight));
* </pre>
*
* @param left the amount of insets to remove from the left. Must be non-negative.
* @param top the amount of insets to remove from the top. Must be non-negative.
* @param right the amount of insets to remove from the right. Must be non-negative.
* @param bottom the amount of insets to remove from the bottom. Must be non-negative.
*
* @return the inset insets
*
* @hide pending API
*/
public WindowInsets inset(int left, int top, int right, int bottom) {
Preconditions.checkArgumentNonnegative(left);
Preconditions.checkArgumentNonnegative(top);
Preconditions.checkArgumentNonnegative(right);
Preconditions.checkArgumentNonnegative(bottom);
WindowInsets result = new WindowInsets(this);
if (!result.mSystemWindowInsetsConsumed) {
result.mSystemWindowInsets =
insetInsets(result.mSystemWindowInsets, left, top, right, bottom);
}
if (!result.mWindowDecorInsetsConsumed) {
result.mWindowDecorInsets =
insetInsets(result.mWindowDecorInsets, left, top, right, bottom);
}
if (!result.mStableInsetsConsumed) {
result.mStableInsets = insetInsets(result.mStableInsets, left, top, right, bottom);
}
if (mDisplayCutout != null) {
result.mDisplayCutout = result.mDisplayCutout.inset(left, top, right, bottom);
if (result.mDisplayCutout.isEmpty()) {
result.mDisplayCutout = null;
}
}
return result;
}
@Override
public boolean equals(Object o) {
if (this == o) return true;
if (o == null || !(o instanceof WindowInsets)) return false;
WindowInsets that = (WindowInsets) o;
return mIsRound == that.mIsRound
&& mAlwaysConsumeNavBar == that.mAlwaysConsumeNavBar
&& mSystemWindowInsetsConsumed == that.mSystemWindowInsetsConsumed
&& mWindowDecorInsetsConsumed == that.mWindowDecorInsetsConsumed
&& mStableInsetsConsumed == that.mStableInsetsConsumed
&& mDisplayCutoutConsumed == that.mDisplayCutoutConsumed
&& Objects.equals(mSystemWindowInsets, that.mSystemWindowInsets)
&& Objects.equals(mWindowDecorInsets, that.mWindowDecorInsets)
&& Objects.equals(mStableInsets, that.mStableInsets)
&& Objects.equals(mDisplayCutout, that.mDisplayCutout);
}
@Override
public int hashCode() {
return Objects.hash(mSystemWindowInsets, mWindowDecorInsets, mStableInsets, mIsRound,
mDisplayCutout, mAlwaysConsumeNavBar, mSystemWindowInsetsConsumed,
mWindowDecorInsetsConsumed, mStableInsetsConsumed, mDisplayCutoutConsumed);
}
private static Rect insetInsets(Rect insets, int left, int top, int right, int bottom) {
int newLeft = Math.max(0, insets.left - left);
int newTop = Math.max(0, insets.top - top);
int newRight = Math.max(0, insets.right - right);
int newBottom = Math.max(0, insets.bottom - bottom);
if (newLeft == left && newTop == top && newRight == right && newBottom == bottom) {
return insets;
}
return new Rect(newLeft, newTop, newRight, newBottom);
}
/**
* @return whether system window insets have been consumed.
*/
boolean isSystemWindowInsetsConsumed() {
return mSystemWindowInsetsConsumed;
}
}

View File

@@ -983,14 +983,14 @@ public class DecorView extends FrameLayout implements RootViewSurfaceTaker, Wind
if (attrs.height == WindowManager.LayoutParams.WRAP_CONTENT) {
mFloatingInsets.top = insets.getSystemWindowInsetTop();
mFloatingInsets.bottom = insets.getSystemWindowInsetBottom();
insets = insets.replaceSystemWindowInsets(insets.getSystemWindowInsetLeft(), 0,
insets.getSystemWindowInsetRight(), 0);
insets = insets.inset(0, insets.getSystemWindowInsetTop(),
0, insets.getSystemWindowInsetBottom());
}
if (mWindow.getAttributes().width == WindowManager.LayoutParams.WRAP_CONTENT) {
mFloatingInsets.left = insets.getSystemWindowInsetTop();
mFloatingInsets.right = insets.getSystemWindowInsetBottom();
insets = insets.replaceSystemWindowInsets(0, insets.getSystemWindowInsetTop(),
0, insets.getSystemWindowInsetBottom());
insets = insets.inset(insets.getSystemWindowInsetLeft(), 0,
insets.getSystemWindowInsetRight(), 0);
}
}
mFrameOffsets.set(insets.getSystemWindowInsets());
@@ -1158,11 +1158,7 @@ public class DecorView extends FrameLayout implements RootViewSurfaceTaker, Wind
}
}
if (insets != null) {
insets = insets.replaceSystemWindowInsets(
insets.getSystemWindowInsetLeft() - consumedLeft,
insets.getSystemWindowInsetTop() - consumedTop,
insets.getSystemWindowInsetRight() - consumedRight,
insets.getSystemWindowInsetBottom() - consumedBottom);
insets = insets.inset(consumedLeft, consumedTop, consumedRight, consumedBottom);
}
}
@@ -1383,8 +1379,9 @@ public class DecorView extends FrameLayout implements RootViewSurfaceTaker, Wind
// screen_simple_overlay_action_mode.xml).
final boolean nonOverlay = (mWindow.getLocalFeaturesPrivate()
& (1 << Window.FEATURE_ACTION_MODE_OVERLAY)) == 0;
insets = insets.consumeSystemWindowInsets(
false, nonOverlay && showStatusGuard /* top */, false, false);
if (nonOverlay && showStatusGuard) {
insets = insets.inset(0, insets.getSystemWindowInsetTop(), 0, 0);
}
} else {
// reset top margin
if (mlp.topMargin != 0) {

View File

@@ -75,10 +75,10 @@ public class ActionBarOverlayLayout extends ViewGroup implements DecorContentPar
private final Rect mBaseContentInsets = new Rect();
private final Rect mLastBaseContentInsets = new Rect();
private final Rect mContentInsets = new Rect();
private final Rect mBaseInnerInsets = new Rect();
private final Rect mLastBaseInnerInsets = new Rect();
private final Rect mInnerInsets = new Rect();
private final Rect mLastInnerInsets = new Rect();
private WindowInsets mBaseInnerInsets = WindowInsets.CONSUMED;
private WindowInsets mLastBaseInnerInsets = WindowInsets.CONSUMED;
private WindowInsets mInnerInsets = WindowInsets.CONSUMED;
private WindowInsets mLastInnerInsets = WindowInsets.CONSUMED;
private ActionBarVisibilityCallback mActionBarVisibilityCallback;
@@ -322,11 +322,14 @@ public class ActionBarOverlayLayout extends ViewGroup implements DecorContentPar
changed |= applyInsets(mActionBarBottom, systemInsets, true, false, true, true);
}
mBaseInnerInsets.set(systemInsets);
computeFitSystemWindows(mBaseInnerInsets, mBaseContentInsets);
// Cannot use the result of computeSystemWindowInsets, because that consumes the
// systemWindowInsets. Instead, we do the insetting by the local insets ourselves.
computeSystemWindowInsets(insets, mBaseContentInsets);
mBaseInnerInsets = insets.inset(mBaseContentInsets);
if (!mLastBaseInnerInsets.equals(mBaseInnerInsets)) {
changed = true;
mLastBaseContentInsets.set(mBaseContentInsets);
mLastBaseInnerInsets = mBaseInnerInsets;
}
if (!mLastBaseContentInsets.equals(mBaseContentInsets)) {
changed = true;
@@ -430,22 +433,29 @@ public class ActionBarOverlayLayout extends ViewGroup implements DecorContentPar
// will still be covered by the action bar if they have requested it to
// overlay.
mContentInsets.set(mBaseContentInsets);
mInnerInsets.set(mBaseInnerInsets);
mInnerInsets = mBaseInnerInsets;
if (!mOverlayMode && !stable) {
mContentInsets.top += topInset;
mContentInsets.bottom += bottomInset;
// Content view has been shrunk, shrink all insets to match.
mInnerInsets = mInnerInsets.inset(0 /* left */, topInset, 0 /* right */, bottomInset);
} else {
mInnerInsets.top += topInset;
mInnerInsets.bottom += bottomInset;
// Add ActionBar to system window inset, but leave other insets untouched.
mInnerInsets = mInnerInsets.replaceSystemWindowInsets(
mInnerInsets.getSystemWindowInsetLeft(),
mInnerInsets.getSystemWindowInsetTop() + topInset,
mInnerInsets.getSystemWindowInsetRight(),
mInnerInsets.getSystemWindowInsetBottom() + bottomInset
);
}
applyInsets(mContent, mContentInsets, true, true, true, true);
if (!mLastInnerInsets.equals(mInnerInsets)) {
// If the inner insets have changed, we need to dispatch this down to
// the app's fitSystemWindows(). We do this before measuring the content
// the app's onApplyWindowInsets(). We do this before measuring the content
// view to keep the same semantics as the normal fitSystemWindows() call.
mLastInnerInsets.set(mInnerInsets);
mContent.dispatchApplyWindowInsets(new WindowInsets(mInnerInsets));
mLastInnerInsets = mInnerInsets;
mContent.dispatchApplyWindowInsets(mInnerInsets);
}
measureChildWithMargins(mContent, widthMeasureSpec, 0, heightMeasureSpec, 0);

View File

@@ -0,0 +1,226 @@
/*
* 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 com.android.internal.widget;
import static android.view.DisplayCutout.NO_CUTOUT;
import static android.view.View.MeasureSpec.EXACTLY;
import static android.view.View.MeasureSpec.makeMeasureSpec;
import static android.view.ViewGroup.LayoutParams.MATCH_PARENT;
import static org.hamcrest.CoreMatchers.nullValue;
import static org.hamcrest.Matchers.is;
import static org.junit.Assert.assertNotNull;
import static org.junit.Assert.assertThat;
import android.content.Context;
import android.graphics.Rect;
import android.support.test.InstrumentationRegistry;
import android.support.test.filters.SmallTest;
import android.support.test.runner.AndroidJUnit4;
import android.view.DisplayCutout;
import android.view.View;
import android.view.View.OnApplyWindowInsetsListener;
import android.view.ViewGroup;
import android.view.WindowInsets;
import android.widget.FrameLayout;
import android.widget.Toolbar;
import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;
import java.lang.reflect.Field;
import java.util.Arrays;
@RunWith(AndroidJUnit4.class)
@SmallTest
public class ActionBarOverlayLayoutTest {
private static final Rect TOP_INSET_5 = new Rect(0, 5, 0, 0);
private static final Rect TOP_INSET_25 = new Rect(0, 25, 0, 0);
private static final Rect ZERO_INSET = new Rect(0, 0, 0, 0);
private static final DisplayCutout CONSUMED_CUTOUT = null;
private static final DisplayCutout CUTOUT_5 = new DisplayCutout(TOP_INSET_5, Arrays.asList(
new Rect(100, 0, 200, 5)));
private static final int EXACTLY_1000 = makeMeasureSpec(1000, EXACTLY);
private Context mContext;
private TestActionBarOverlayLayout mLayout;
private ViewGroup mContent;
private ViewGroup mActionBarTop;
private Toolbar mToolbar;
private FakeOnApplyWindowListener mContentInsetsListener;
@Before
public void setUp() throws Exception {
mContext = InstrumentationRegistry.getContext();
mLayout = new TestActionBarOverlayLayout(mContext);
mLayout.makeOptionalFitsSystemWindows();
mContent = createViewGroupWithId(com.android.internal.R.id.content);
mContent.setLayoutParams(new ViewGroup.LayoutParams(MATCH_PARENT, MATCH_PARENT));
mLayout.addView(mContent);
mContentInsetsListener = new FakeOnApplyWindowListener();
mContent.setOnApplyWindowInsetsListener(mContentInsetsListener);
mActionBarTop = new ActionBarContainer(mContext);
mActionBarTop.setId(com.android.internal.R.id.action_bar_container);
mActionBarTop.setLayoutParams(new ViewGroup.LayoutParams(MATCH_PARENT, 20));
mLayout.addView(mActionBarTop);
mLayout.setActionBarHeight(20);
mToolbar = new Toolbar(mContext);
mToolbar.setId(com.android.internal.R.id.action_bar);
mActionBarTop.addView(mToolbar);
}
@Test
public void topInset_consumedCutout_stable() {
mLayout.setStable(true);
mLayout.dispatchApplyWindowInsets(insetsWith(TOP_INSET_5, CONSUMED_CUTOUT));
assertThat(mContentInsetsListener.captured, nullValue());
mLayout.measure(EXACTLY_1000, EXACTLY_1000);
// Action bar height is added to the top inset
assertThat(mContentInsetsListener.captured, is(insetsWith(TOP_INSET_25, CONSUMED_CUTOUT)));
}
@Test
public void topInset_consumedCutout_notStable() {
mLayout.dispatchApplyWindowInsets(insetsWith(TOP_INSET_5, CONSUMED_CUTOUT));
assertThat(mContentInsetsListener.captured, nullValue());
mLayout.measure(EXACTLY_1000, EXACTLY_1000);
assertThat(mContentInsetsListener.captured, is(insetsWith(ZERO_INSET, CONSUMED_CUTOUT)));
}
@Test
public void topInset_noCutout_stable() {
mLayout.setStable(true);
mLayout.dispatchApplyWindowInsets(insetsWith(TOP_INSET_5, NO_CUTOUT));
assertThat(mContentInsetsListener.captured, nullValue());
mLayout.measure(EXACTLY_1000, EXACTLY_1000);
// Action bar height is added to the top inset
assertThat(mContentInsetsListener.captured, is(insetsWith(TOP_INSET_25, NO_CUTOUT)));
}
@Test
public void topInset_noCutout_notStable() {
mLayout.dispatchApplyWindowInsets(insetsWith(TOP_INSET_5, NO_CUTOUT));
assertThat(mContentInsetsListener.captured, nullValue());
mLayout.measure(EXACTLY_1000, EXACTLY_1000);
assertThat(mContentInsetsListener.captured, is(insetsWith(ZERO_INSET, NO_CUTOUT)));
}
@Test
public void topInset_cutout_stable() {
mLayout.setStable(true);
mLayout.dispatchApplyWindowInsets(insetsWith(TOP_INSET_5, CUTOUT_5));
assertThat(mContentInsetsListener.captured, nullValue());
mLayout.measure(EXACTLY_1000, EXACTLY_1000);
// Action bar height is added to the top inset
assertThat(mContentInsetsListener.captured, is(insetsWith(TOP_INSET_25, CUTOUT_5)));
}
@Test
public void topInset_cutout_notStable() {
mLayout.dispatchApplyWindowInsets(insetsWith(TOP_INSET_5, CUTOUT_5));
assertThat(mContentInsetsListener.captured, nullValue());
mLayout.measure(EXACTLY_1000, EXACTLY_1000);
assertThat(mContentInsetsListener.captured, is(insetsWith(ZERO_INSET, NO_CUTOUT)));
}
private WindowInsets insetsWith(Rect content, DisplayCutout cutout) {
return new WindowInsets(content, null, null, false, false, cutout);
}
private ViewGroup createViewGroupWithId(int id) {
final FrameLayout v = new FrameLayout(mContext);
v.setId(id);
return v;
}
static class TestActionBarOverlayLayout extends ActionBarOverlayLayout {
private boolean mStable;
public TestActionBarOverlayLayout(Context context) {
super(context);
}
@Override
public WindowInsets computeSystemWindowInsets(WindowInsets in, Rect outLocalInsets) {
if (mStable) {
// Emulate the effect of makeOptionalFitsSystemWindows, because we can't do that
// without being attached to a window.
outLocalInsets.setEmpty();
return in;
}
return super.computeSystemWindowInsets(in, outLocalInsets);
}
void setStable(boolean stable) {
mStable = stable;
setSystemUiVisibility(stable ? SYSTEM_UI_FLAG_LAYOUT_STABLE : 0);
}
@Override
public int getWindowSystemUiVisibility() {
return getSystemUiVisibility();
}
void setActionBarHeight(int height) {
try {
final Field field = ActionBarOverlayLayout.class.getDeclaredField(
"mActionBarHeight");
field.setAccessible(true);
field.setInt(this, height);
} catch (ReflectiveOperationException e) {
throw new RuntimeException(e);
}
}
}
static class FakeOnApplyWindowListener implements OnApplyWindowInsetsListener {
WindowInsets captured;
@Override
public WindowInsets onApplyWindowInsets(View v, WindowInsets insets) {
assertNotNull(insets);
captured = insets;
return v.onApplyWindowInsets(insets);
}
}
}

View File

@@ -159,7 +159,12 @@ public class ScreenDecorWindowTests {
updateWindow(decorWindow, TOP, MATCH_PARENT, mDecorThickness,
0, PRIVATE_FLAG_IS_SCREEN_DECOR);
assertTopInsetEquals(mTestActivity, initialInsets.getSystemWindowInsetTop());
// TODO: fix test and re-enable assertion.
// initialInsets was not actually immutable and just updated to the current insets,
// meaning this assertion never actually tested anything. Now that WindowInsets actually is
// immutable, it turns out the test was broken.
// assertTopInsetEquals(mTestActivity, initialInsets.getSystemWindowInsetTop());
updateWindow(decorWindow, TOP, MATCH_PARENT, mDecorThickness,
PRIVATE_FLAG_IS_SCREEN_DECOR, PRIVATE_FLAG_IS_SCREEN_DECOR);