diff --git a/core/java/com/android/internal/policy/DecorView.java b/core/java/com/android/internal/policy/DecorView.java index eadefc9199344..cfc5c4cebea96 100644 --- a/core/java/com/android/internal/policy/DecorView.java +++ b/core/java/com/android/internal/policy/DecorView.java @@ -310,10 +310,8 @@ public class DecorView extends FrameLayout implements RootViewSurfaceTaker, Wind public void onDraw(Canvas c) { super.onDraw(c); - // When we are resizing, we need the fallback background to cover the area where we have our - // system bar background views as the navigation bar will be hidden during resizing. - mBackgroundFallback.draw(isResizing() ? this : mContentRoot, mContentRoot, c, - mWindow.mContentParent); + mBackgroundFallback.draw(this, mContentRoot, c, mWindow.mContentParent, + mStatusColorViewState.view, mNavigationColorViewState.view); } @Override diff --git a/core/java/com/android/internal/widget/BackgroundFallback.java b/core/java/com/android/internal/widget/BackgroundFallback.java index 309f80cb0d52f..2b05f1ecbc70a 100644 --- a/core/java/com/android/internal/widget/BackgroundFallback.java +++ b/core/java/com/android/internal/widget/BackgroundFallback.java @@ -46,8 +46,11 @@ public class BackgroundFallback { * @param root The view group containing the content. * @param c The canvas to draw the background onto. * @param content The view where the actual app content is contained in. + * @param coveringView1 A potentially opaque view drawn atop the content + * @param coveringView2 A potentially opaque view drawn atop the content */ - public void draw(ViewGroup boundsView, ViewGroup root, Canvas c, View content) { + public void draw(ViewGroup boundsView, ViewGroup root, Canvas c, View content, + View coveringView1, View coveringView2) { if (!hasFallback()) { return; } @@ -55,6 +58,10 @@ public class BackgroundFallback { // Draw the fallback in the padding. final int width = boundsView.getWidth(); final int height = boundsView.getHeight(); + + final int rootOffsetX = root.getLeft(); + final int rootOffsetY = root.getTop(); + int left = width; int top = height; int right = 0; @@ -71,17 +78,58 @@ public class BackgroundFallback { ((ViewGroup) child).getChildCount() == 0) { continue; } - } else if (child.getVisibility() != View.VISIBLE || childBg == null || - childBg.getOpacity() != PixelFormat.OPAQUE) { + } else if (child.getVisibility() != View.VISIBLE || !isOpaque(childBg)) { // Potentially translucent or invisible children don't count, and we assume // the content view will cover the whole area if we're in a background // fallback situation. continue; } - left = Math.min(left, child.getLeft()); - top = Math.min(top, child.getTop()); - right = Math.max(right, child.getRight()); - bottom = Math.max(bottom, child.getBottom()); + left = Math.min(left, rootOffsetX + child.getLeft()); + top = Math.min(top, rootOffsetY + child.getTop()); + right = Math.max(right, rootOffsetX + child.getRight()); + bottom = Math.max(bottom, rootOffsetY + child.getBottom()); + } + + // If one of the bar backgrounds is a solid color and covers the entire padding on a side + // we can drop that padding. + boolean eachBarCoversTopInY = true; + for (int i = 0; i < 2; i++) { + View v = (i == 0) ? coveringView1 : coveringView2; + if (v == null || v.getVisibility() != View.VISIBLE + || v.getAlpha() != 1f || !isOpaque(v.getBackground())) { + eachBarCoversTopInY = false; + continue; + } + + // Bar covers entire left padding + if (v.getTop() <= 0 && v.getBottom() >= height + && v.getLeft() <= 0 && v.getRight() >= left) { + left = 0; + } + // Bar covers entire right padding + if (v.getTop() <= 0 && v.getBottom() >= height + && v.getLeft() <= right && v.getRight() >= width) { + right = width; + } + // Bar covers entire top padding + if (v.getTop() <= 0 && v.getBottom() >= top + && v.getLeft() <= 0 && v.getRight() >= width) { + top = 0; + } + // Bar covers entire bottom padding + if (v.getTop() <= bottom && v.getBottom() >= height + && v.getLeft() <= 0 && v.getRight() >= width) { + bottom = height; + } + + eachBarCoversTopInY &= v.getTop() <= 0 && v.getBottom() >= top; + } + + // Special case: Sometimes, both covering views together may cover the top inset, but + // neither does on its own. + if (eachBarCoversTopInY && (viewsCoverEntireWidth(coveringView1, coveringView2, width) + || viewsCoverEntireWidth(coveringView2, coveringView1, width))) { + top = 0; } if (left >= right || top >= bottom) { @@ -106,4 +154,24 @@ public class BackgroundFallback { mBackgroundFallback.draw(c); } } + + private boolean isOpaque(Drawable childBg) { + return childBg != null && childBg.getOpacity() == PixelFormat.OPAQUE; + } + + /** + * Returns true if {@code view1} starts before or on {@code 0} and extends at least + * up to {@code view2}, and that view extends at least to {@code width}. + * + * @param view1 the first view to check if it covers the width + * @param view2 the second view to check if it covers the width + * @param width the width to check for + * @return returns true if both views together cover the entire width (and view1 is to the left + * of view2) + */ + private boolean viewsCoverEntireWidth(View view1, View view2, int width) { + return view1.getLeft() <= 0 + && view1.getRight() >= view2.getLeft() + && view2.getRight() >= width; + } } diff --git a/core/tests/coretests/src/com/android/internal/widget/BackgroundFallbackTest.java b/core/tests/coretests/src/com/android/internal/widget/BackgroundFallbackTest.java new file mode 100644 index 0000000000000..e21143d4392fb --- /dev/null +++ b/core/tests/coretests/src/com/android/internal/widget/BackgroundFallbackTest.java @@ -0,0 +1,262 @@ +/* + * 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.View.VISIBLE; + +import static org.hamcrest.Matchers.is; +import static org.junit.Assert.assertThat; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.Mockito.atLeastOnce; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.verifyNoMoreInteractions; + +import static java.util.Arrays.asList; +import static java.util.Collections.emptyList; + +import android.graphics.Color; +import android.graphics.drawable.ColorDrawable; +import android.graphics.drawable.Drawable; +import android.support.test.InstrumentationRegistry; +import android.view.ViewGroup; +import android.widget.FrameLayout; + +import org.junit.Before; +import org.junit.Test; + +import java.util.List; + +public class BackgroundFallbackTest { + + private static final int NAVBAR_BOTTOM = 0; + private static final int NAVBAR_LEFT = 1; + private static final int NAVBAR_RIGHT = 2; + + private static final int SCREEN_HEIGHT = 2000; + private static final int SCREEN_WIDTH = 1000; + private static final int STATUS_HEIGHT = 100; + private static final int NAV_SIZE = 200; + + private static final boolean INSET_CONTENT_VIEWS = true; + private static final boolean DONT_INSET_CONTENT_VIEWS = false; + + BackgroundFallback mFallback; + Drawable mDrawableMock; + + ViewGroup mStatusBarView; + ViewGroup mNavigationBarView; + + ViewGroup mDecorViewMock; + ViewGroup mContentRootMock; + ViewGroup mContentContainerMock; + ViewGroup mContentMock; + + int mLastTop = 0; + + @Before + public void setUp() throws Exception { + mFallback = new BackgroundFallback(); + mDrawableMock = mock(Drawable.class); + + mFallback.setDrawable(mDrawableMock); + + } + + @Test + public void hasFallback_withDrawable_true() { + mFallback.setDrawable(mDrawableMock); + assertThat(mFallback.hasFallback(), is(true)); + } + + @Test + public void hasFallback_withoutDrawable_false() { + mFallback.setDrawable(null); + assertThat(mFallback.hasFallback(), is(false)); + } + + @Test + public void draw_portrait_noFallback() { + setUpViewHierarchy(INSET_CONTENT_VIEWS, NAVBAR_BOTTOM); + + mFallback.draw(mDecorViewMock, mContentRootMock, null /* canvas */, mContentContainerMock, + mStatusBarView, mNavigationBarView); + + verifyNoMoreInteractions(mDrawableMock); + } + + @Test + public void draw_portrait_translucentBars_fallback() { + setUpViewHierarchy(INSET_CONTENT_VIEWS, NAVBAR_BOTTOM); + setTranslucent(mStatusBarView); + setTranslucent(mNavigationBarView); + + mFallback.draw(mDecorViewMock, mContentRootMock, null /* canvas */, mContentContainerMock, + mStatusBarView, mNavigationBarView); + + verifyFallbackTop(STATUS_HEIGHT); + verifyFallbackBottom(NAV_SIZE); + verifyNoMoreInteractions(mDrawableMock); + } + + @Test + public void draw_landscape_translucentBars_fallback() { + setUpViewHierarchy(INSET_CONTENT_VIEWS, NAVBAR_RIGHT); + setTranslucent(mStatusBarView); + setTranslucent(mNavigationBarView); + + mFallback.draw(mDecorViewMock, mContentRootMock, null /* canvas */, mContentContainerMock, + mStatusBarView, mNavigationBarView); + + verifyFallbackTop(STATUS_HEIGHT); + verifyFallbackRight(NAV_SIZE); + verifyNoMoreInteractions(mDrawableMock); + } + + @Test + public void draw_seascape_translucentBars_fallback() { + setUpViewHierarchy(INSET_CONTENT_VIEWS, NAVBAR_LEFT); + setTranslucent(mStatusBarView); + setTranslucent(mNavigationBarView); + + mFallback.draw(mDecorViewMock, mContentRootMock, null /* canvas */, mContentContainerMock, + mStatusBarView, mNavigationBarView); + + verifyFallbackTop(STATUS_HEIGHT); + verifyFallbackLeft(NAV_SIZE); + verifyNoMoreInteractions(mDrawableMock); + } + + @Test + public void draw_landscape_noFallback() { + setUpViewHierarchy(INSET_CONTENT_VIEWS, NAVBAR_RIGHT); + + mFallback.draw(mDecorViewMock, mContentRootMock, null /* canvas */, mContentContainerMock, + mStatusBarView, mNavigationBarView); + + verifyNoMoreInteractions(mDrawableMock); + } + + @Test + public void draw_seascape_noFallback() { + setUpViewHierarchy(INSET_CONTENT_VIEWS, NAVBAR_LEFT); + + mFallback.draw(mDecorViewMock, mContentRootMock, null /* canvas */, mContentContainerMock, + mStatusBarView, mNavigationBarView); + + verifyNoMoreInteractions(mDrawableMock); + } + + @Test + public void draw_seascape_translucentBars_noInsets_noFallback() { + setUpViewHierarchy(DONT_INSET_CONTENT_VIEWS, NAVBAR_LEFT); + setTranslucent(mStatusBarView); + setTranslucent(mNavigationBarView); + + mFallback.draw(mDecorViewMock, mContentRootMock, null /* canvas */, mContentContainerMock, + mStatusBarView, mNavigationBarView); + + verifyNoMoreInteractions(mDrawableMock); + } + + private void verifyFallbackTop(int size) { + verify(mDrawableMock).setBounds(0, 0, SCREEN_WIDTH, size); + verify(mDrawableMock, atLeastOnce()).draw(any()); + mLastTop = size; + } + + private void verifyFallbackLeft(int size) { + verify(mDrawableMock).setBounds(0, mLastTop, size, SCREEN_HEIGHT); + verify(mDrawableMock, atLeastOnce()).draw(any()); + } + + private void verifyFallbackRight(int size) { + verify(mDrawableMock).setBounds(SCREEN_WIDTH - size, mLastTop, SCREEN_WIDTH, SCREEN_HEIGHT); + verify(mDrawableMock, atLeastOnce()).draw(any()); + } + + private void verifyFallbackBottom(int size) { + verify(mDrawableMock).setBounds(0, SCREEN_HEIGHT - size, SCREEN_WIDTH, SCREEN_HEIGHT); + verify(mDrawableMock, atLeastOnce()).draw(any()); + } + + private void setUpViewHierarchy(boolean insetContentViews, int navBarPosition) { + int insetLeft = 0; + int insetTop = 0; + int insetRight = 0; + int insetBottom = 0; + + mStatusBarView = mockView(0, 0, SCREEN_WIDTH, STATUS_HEIGHT, + new ColorDrawable(Color.BLACK), VISIBLE, emptyList()); + if (insetContentViews) { + insetTop = STATUS_HEIGHT; + } + + switch (navBarPosition) { + case NAVBAR_BOTTOM: + mNavigationBarView = mockView(0, SCREEN_HEIGHT - NAV_SIZE, SCREEN_WIDTH, + SCREEN_HEIGHT, new ColorDrawable(Color.BLACK), VISIBLE, emptyList()); + if (insetContentViews) { + insetBottom = NAV_SIZE; + } + break; + case NAVBAR_LEFT: + mNavigationBarView = mockView(0, 0, NAV_SIZE, SCREEN_HEIGHT, + new ColorDrawable(Color.BLACK), VISIBLE, emptyList()); + if (insetContentViews) { + insetLeft = NAV_SIZE; + } + break; + case NAVBAR_RIGHT: + mNavigationBarView = mockView(SCREEN_WIDTH - NAV_SIZE, 0, SCREEN_WIDTH, + SCREEN_HEIGHT, new ColorDrawable(Color.BLACK), VISIBLE, emptyList()); + if (insetContentViews) { + insetRight = NAV_SIZE; + } + break; + } + + mContentMock = mockView(0, 0, SCREEN_WIDTH - insetLeft - insetRight, + SCREEN_HEIGHT - insetTop - insetBottom, null, VISIBLE, emptyList()); + mContentContainerMock = mockView(0, 0, SCREEN_WIDTH - insetLeft - insetRight, + SCREEN_HEIGHT - insetTop - insetBottom, null, VISIBLE, asList(mContentMock)); + mContentRootMock = mockView(insetLeft, insetTop, SCREEN_WIDTH - insetRight, + SCREEN_HEIGHT - insetBottom, null, VISIBLE, asList(mContentContainerMock)); + + mDecorViewMock = mockView(0, 0, SCREEN_WIDTH, SCREEN_HEIGHT, null, VISIBLE, + asList(mContentRootMock, mStatusBarView, mNavigationBarView)); + } + + private void setTranslucent(ViewGroup bar) { + bar.setBackground(new ColorDrawable(Color.TRANSPARENT)); + } + + private ViewGroup mockView(int left, int top, int right, int bottom, Drawable background, + int visibility, List children) { + final ViewGroup v = new FrameLayout(InstrumentationRegistry.getTargetContext()); + + v.layout(left, top, right, bottom); + v.setBackground(background); + v.setVisibility(visibility); + + for (ViewGroup c : children) { + v.addView(c); + } + + return v; + } +} \ No newline at end of file