From f7b7426d81c5365473d00362cf158aa5ae35cee3 Mon Sep 17 00:00:00 2001
From: Adrian Roos
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. + * + *
Example: + *
+ * childView.dispatchApplyWindowInsets(insets.inset( + * childMarginLeft, childMarginTop, childMarginBottom, childMarginRight)); + *+ * + * @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; + } } diff --git a/core/java/com/android/internal/policy/DecorView.java b/core/java/com/android/internal/policy/DecorView.java index 2db573918e8a..465957d7cfd0 100644 --- a/core/java/com/android/internal/policy/DecorView.java +++ b/core/java/com/android/internal/policy/DecorView.java @@ -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) { diff --git a/core/java/com/android/internal/widget/ActionBarOverlayLayout.java b/core/java/com/android/internal/widget/ActionBarOverlayLayout.java index 5d7fa6a742be..4a1c95532ba0 100644 --- a/core/java/com/android/internal/widget/ActionBarOverlayLayout.java +++ b/core/java/com/android/internal/widget/ActionBarOverlayLayout.java @@ -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); diff --git a/core/tests/coretests/src/com/android/internal/widget/ActionBarOverlayLayoutTest.java b/core/tests/coretests/src/com/android/internal/widget/ActionBarOverlayLayoutTest.java new file mode 100644 index 000000000000..cac4e88c8edc --- /dev/null +++ b/core/tests/coretests/src/com/android/internal/widget/ActionBarOverlayLayoutTest.java @@ -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); + } + } +} diff --git a/services/tests/servicestests/src/com/android/server/wm/ScreenDecorWindowTests.java b/services/tests/servicestests/src/com/android/server/wm/ScreenDecorWindowTests.java index 24d925f1bc95..a2ccee46e0c9 100644 --- a/services/tests/servicestests/src/com/android/server/wm/ScreenDecorWindowTests.java +++ b/services/tests/servicestests/src/com/android/server/wm/ScreenDecorWindowTests.java @@ -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); -- 2.11.0