Fix WindowInsetsController lifecycle
Provide a recording insets controller before the window gets created, and replay the commands once a view gets attached. This allows the client to use the controller in Activity.onCreate. Test: WindowInsetsControllerTests Bug: 118118435 Change-Id: I1a825ecc4367c02b27f2d08cd5442325315d4f89
This commit is contained in:
181
core/java/android/view/PendingInsetsController.java
Normal file
181
core/java/android/view/PendingInsetsController.java
Normal file
@@ -0,0 +1,181 @@
|
||||
/*
|
||||
* Copyright (C) 2019 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 android.os.CancellationSignal;
|
||||
import android.view.WindowInsets.Type.InsetsType;
|
||||
import android.view.animation.Interpolator;
|
||||
|
||||
import com.android.internal.annotations.VisibleForTesting;
|
||||
|
||||
import java.util.ArrayList;
|
||||
|
||||
/**
|
||||
* An insets controller that keeps track of pending requests. This is such that an app can freely
|
||||
* use {@link WindowInsetsController} before the view root is attached during activity startup.
|
||||
* @hide
|
||||
*/
|
||||
public class PendingInsetsController implements WindowInsetsController {
|
||||
|
||||
private static final int KEEP_BEHAVIOR = -1;
|
||||
private final ArrayList<PendingRequest> mRequests = new ArrayList<>();
|
||||
private @Appearance int mAppearance;
|
||||
private @Appearance int mAppearanceMask;
|
||||
private @Behavior int mBehavior = KEEP_BEHAVIOR;
|
||||
private final InsetsState mDummyState = new InsetsState();
|
||||
private InsetsController mReplayedInsetsController;
|
||||
|
||||
@Override
|
||||
public void show(int types) {
|
||||
if (mReplayedInsetsController != null) {
|
||||
mReplayedInsetsController.show(types);
|
||||
} else {
|
||||
mRequests.add(new ShowRequest(types));
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void hide(int types) {
|
||||
if (mReplayedInsetsController != null) {
|
||||
mReplayedInsetsController.hide(types);
|
||||
} else {
|
||||
mRequests.add(new HideRequest(types));
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public CancellationSignal controlWindowInsetsAnimation(int types, long durationMillis,
|
||||
Interpolator interpolator,
|
||||
WindowInsetsAnimationControlListener listener) {
|
||||
if (mReplayedInsetsController != null) {
|
||||
return mReplayedInsetsController.controlWindowInsetsAnimation(types, durationMillis,
|
||||
interpolator, listener);
|
||||
} else {
|
||||
listener.onCancelled();
|
||||
CancellationSignal cancellationSignal = new CancellationSignal();
|
||||
cancellationSignal.cancel();
|
||||
return cancellationSignal;
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setSystemBarsAppearance(int appearance, int mask) {
|
||||
if (mReplayedInsetsController != null) {
|
||||
mReplayedInsetsController.setSystemBarsAppearance(appearance, mask);
|
||||
} else {
|
||||
mAppearance = (mAppearance & ~mask) | (appearance & mask);
|
||||
mAppearanceMask |= mask;
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getSystemBarsAppearance() {
|
||||
if (mReplayedInsetsController != null) {
|
||||
return mReplayedInsetsController.getSystemBarsAppearance();
|
||||
}
|
||||
return mAppearance;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setSystemBarsBehavior(int behavior) {
|
||||
if (mReplayedInsetsController != null) {
|
||||
mReplayedInsetsController.setSystemBarsBehavior(behavior);
|
||||
} else {
|
||||
mBehavior = behavior;
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getSystemBarsBehavior() {
|
||||
if (mReplayedInsetsController != null) {
|
||||
return mReplayedInsetsController.getSystemBarsBehavior();
|
||||
}
|
||||
return mBehavior;
|
||||
}
|
||||
|
||||
@Override
|
||||
public InsetsState getState() {
|
||||
return mDummyState;
|
||||
}
|
||||
|
||||
/**
|
||||
* Replays the commands on {@code controller} and attaches it to this instance such that any
|
||||
* calls will be forwarded to the real instance in the future.
|
||||
*/
|
||||
@VisibleForTesting
|
||||
public void replayAndAttach(InsetsController controller) {
|
||||
if (mBehavior != KEEP_BEHAVIOR) {
|
||||
controller.setSystemBarsBehavior(mBehavior);
|
||||
}
|
||||
if (mAppearanceMask != 0) {
|
||||
controller.setSystemBarsAppearance(mAppearance, mAppearanceMask);
|
||||
}
|
||||
int size = mRequests.size();
|
||||
for (int i = 0; i < size; i++) {
|
||||
mRequests.get(i).replay(controller);
|
||||
}
|
||||
|
||||
// Reset all state so it doesn't get applied twice just in case
|
||||
mRequests.clear();
|
||||
mBehavior = KEEP_BEHAVIOR;
|
||||
mAppearance = 0;
|
||||
mAppearanceMask = 0;
|
||||
|
||||
// After replaying, we forward everything directly to the replayed instance.
|
||||
mReplayedInsetsController = controller;
|
||||
}
|
||||
|
||||
/**
|
||||
* Detaches the controller to no longer forward calls to the real instance.
|
||||
*/
|
||||
@VisibleForTesting
|
||||
public void detach() {
|
||||
mReplayedInsetsController = null;
|
||||
}
|
||||
|
||||
private interface PendingRequest {
|
||||
void replay(InsetsController controller);
|
||||
}
|
||||
|
||||
private static class ShowRequest implements PendingRequest {
|
||||
|
||||
private final @InsetsType int mTypes;
|
||||
|
||||
public ShowRequest(int types) {
|
||||
mTypes = types;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void replay(InsetsController controller) {
|
||||
controller.show(mTypes);
|
||||
}
|
||||
}
|
||||
|
||||
private static class HideRequest implements PendingRequest {
|
||||
|
||||
private final @InsetsType int mTypes;
|
||||
|
||||
public HideRequest(int types) {
|
||||
mTypes = types;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void replay(InsetsController controller) {
|
||||
controller.hide(mTypes);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -11415,14 +11415,18 @@ public class View implements Drawable.Callback, KeyEvent.Callback,
|
||||
/**
|
||||
* Retrieves the single {@link WindowInsetsController} of the window this view is attached to.
|
||||
*
|
||||
* @return The {@link WindowInsetsController} or {@code null} if the view isn't attached to a
|
||||
* a window.
|
||||
* @return The {@link WindowInsetsController} or {@code null} if the view is neither attached to
|
||||
* a window nor a view tree with a decor.
|
||||
* @see Window#getInsetsController()
|
||||
*/
|
||||
public @Nullable WindowInsetsController getWindowInsetsController() {
|
||||
if (mAttachInfo != null) {
|
||||
return mAttachInfo.mViewRootImpl.getInsetsController();
|
||||
}
|
||||
ViewParent parent = getParent();
|
||||
if (parent instanceof View) {
|
||||
return ((View) parent).getWindowInsetsController();
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
|
||||
@@ -1117,6 +1117,14 @@ public final class ViewRootImpl implements ViewParent,
|
||||
mFirstInputStage = nativePreImeStage;
|
||||
mFirstPostImeInputStage = earlyPostImeStage;
|
||||
mPendingInputEventQueueLengthCounterName = "aq:pending:" + counterSuffix;
|
||||
|
||||
if (mView instanceof RootViewSurfaceTaker) {
|
||||
PendingInsetsController pendingInsetsController =
|
||||
((RootViewSurfaceTaker) mView).providePendingInsetsController();
|
||||
if (pendingInsetsController != null) {
|
||||
pendingInsetsController.replayAndAttach(mInsetsController);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -78,6 +78,7 @@ import android.view.ContextThemeWrapper;
|
||||
import android.view.Gravity;
|
||||
import android.view.InputQueue;
|
||||
import android.view.InsetsState;
|
||||
import android.view.InsetsController;
|
||||
import android.view.InsetsState.InternalInsetsType;
|
||||
import android.view.KeyEvent;
|
||||
import android.view.KeyboardShortcutGroup;
|
||||
@@ -85,6 +86,7 @@ import android.view.LayoutInflater;
|
||||
import android.view.Menu;
|
||||
import android.view.MenuItem;
|
||||
import android.view.MotionEvent;
|
||||
import android.view.PendingInsetsController;
|
||||
import android.view.ThreadedRenderer;
|
||||
import android.view.View;
|
||||
import android.view.ViewGroup;
|
||||
@@ -286,6 +288,8 @@ public class DecorView extends FrameLayout implements RootViewSurfaceTaker, Wind
|
||||
*/
|
||||
private boolean mUseNewInsetsApi;
|
||||
|
||||
private PendingInsetsController mPendingInsetsController = new PendingInsetsController();
|
||||
|
||||
DecorView(Context context, int featureId, PhoneWindow window,
|
||||
WindowManager.LayoutParams params) {
|
||||
super(context);
|
||||
@@ -1780,6 +1784,8 @@ public class DecorView extends FrameLayout implements RootViewSurfaceTaker, Wind
|
||||
getViewRootImpl().removeWindowCallbacks(this);
|
||||
mWindowResizeCallbacksAdded = false;
|
||||
}
|
||||
|
||||
mPendingInsetsController.detach();
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -1819,6 +1825,11 @@ public class DecorView extends FrameLayout implements RootViewSurfaceTaker, Wind
|
||||
updateColorViewTranslations();
|
||||
}
|
||||
|
||||
@Override
|
||||
public PendingInsetsController providePendingInsetsController() {
|
||||
return mPendingInsetsController;
|
||||
}
|
||||
|
||||
private ActionMode createActionMode(
|
||||
int type, ActionMode.Callback2 callback, View originatingView) {
|
||||
switch (type) {
|
||||
@@ -2539,6 +2550,15 @@ public class DecorView extends FrameLayout implements RootViewSurfaceTaker, Wind
|
||||
return AccessibilityNodeInfo.ROOT_ITEM_ID;
|
||||
}
|
||||
|
||||
@Override
|
||||
public WindowInsetsController getWindowInsetsController() {
|
||||
if (isAttachedToWindow()) {
|
||||
return super.getWindowInsetsController();
|
||||
} else {
|
||||
return mPendingInsetsController;
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
return "DecorView@" + Integer.toHexString(this.hashCode()) + "["
|
||||
|
||||
@@ -15,8 +15,11 @@
|
||||
*/
|
||||
package com.android.internal.view;
|
||||
|
||||
import android.annotation.Nullable;
|
||||
import android.view.InputQueue;
|
||||
import android.view.PendingInsetsController;
|
||||
import android.view.SurfaceHolder;
|
||||
import android.view.WindowInsetsController;
|
||||
|
||||
/** hahahah */
|
||||
public interface RootViewSurfaceTaker {
|
||||
@@ -26,4 +29,5 @@ public interface RootViewSurfaceTaker {
|
||||
void setSurfaceKeepScreenOn(boolean keepOn);
|
||||
InputQueue.Callback willYouTakeTheInputQueue();
|
||||
void onRootViewScrollYChanged(int scrollY);
|
||||
@Nullable PendingInsetsController providePendingInsetsController();
|
||||
}
|
||||
|
||||
@@ -0,0 +1,193 @@
|
||||
/*
|
||||
* Copyright (C) 2020 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.WindowInsets.Type.navigationBars;
|
||||
import static android.view.WindowInsets.Type.systemBars;
|
||||
import static android.view.WindowInsetsController.APPEARANCE_LIGHT_STATUS_BARS;
|
||||
import static android.view.WindowInsetsController.BEHAVIOR_SHOW_TRANSIENT_BARS_BY_SWIPE;
|
||||
import static org.junit.Assert.assertEquals;
|
||||
import static org.junit.Assert.assertTrue;
|
||||
import static org.mockito.ArgumentMatchers.any;
|
||||
import static org.mockito.ArgumentMatchers.eq;
|
||||
import static org.mockito.Mockito.mock;
|
||||
import static org.mockito.Mockito.verify;
|
||||
import static org.mockito.Mockito.verifyZeroInteractions;
|
||||
import static org.mockito.Mockito.when;
|
||||
|
||||
import android.os.CancellationSignal;
|
||||
import android.platform.test.annotations.Presubmit;
|
||||
import android.view.animation.LinearInterpolator;
|
||||
|
||||
import org.junit.Before;
|
||||
import org.junit.Test;
|
||||
import org.junit.runner.RunWith;
|
||||
|
||||
import androidx.test.runner.AndroidJUnit4;
|
||||
|
||||
/**
|
||||
* Tests for {@link PendingInsetsControllerTest}.
|
||||
*
|
||||
* <p>Build/Install/Run:
|
||||
* atest FrameworksCoreTests:PendingInsetsControllerTest
|
||||
*
|
||||
* <p>This test class is a part of Window Manager Service tests and specified in
|
||||
* {@link com.android.server.wm.test.filters.FrameworksTestsFilter}.
|
||||
*/
|
||||
@Presubmit
|
||||
@RunWith(AndroidJUnit4.class)
|
||||
public class PendingInsetsControllerTest {
|
||||
|
||||
private PendingInsetsController mPendingInsetsController = new PendingInsetsController();
|
||||
private InsetsController mReplayedController;
|
||||
|
||||
@Before
|
||||
public void setUp() {
|
||||
mPendingInsetsController = new PendingInsetsController();
|
||||
mReplayedController = mock(InsetsController.class);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testShow() {
|
||||
mPendingInsetsController.show(systemBars());
|
||||
mPendingInsetsController.replayAndAttach(mReplayedController);
|
||||
verify(mReplayedController).show(eq(systemBars()));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testShow_direct() {
|
||||
mPendingInsetsController.replayAndAttach(mReplayedController);
|
||||
mPendingInsetsController.show(systemBars());
|
||||
verify(mReplayedController).show(eq(systemBars()));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testHide() {
|
||||
mPendingInsetsController.hide(systemBars());
|
||||
mPendingInsetsController.replayAndAttach(mReplayedController);
|
||||
verify(mReplayedController).hide(eq(systemBars()));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testHide_direct() {
|
||||
mPendingInsetsController.replayAndAttach(mReplayedController);
|
||||
mPendingInsetsController.hide(systemBars());
|
||||
verify(mReplayedController).hide(eq(systemBars()));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testControl() {
|
||||
WindowInsetsAnimationControlListener listener =
|
||||
mock(WindowInsetsAnimationControlListener.class);
|
||||
CancellationSignal signal = mPendingInsetsController.controlWindowInsetsAnimation(
|
||||
systemBars(), 0, new LinearInterpolator(), listener);
|
||||
verify(listener).onCancelled();
|
||||
assertTrue(signal.isCanceled());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testControl_direct() {
|
||||
WindowInsetsAnimationControlListener listener =
|
||||
mock(WindowInsetsAnimationControlListener.class);
|
||||
mPendingInsetsController.replayAndAttach(mReplayedController);
|
||||
mPendingInsetsController.controlWindowInsetsAnimation(
|
||||
systemBars(), 0L, new LinearInterpolator(), listener);
|
||||
verify(mReplayedController).controlWindowInsetsAnimation(eq(systemBars()), eq(0L), any(),
|
||||
eq(listener));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testBehavior() {
|
||||
mPendingInsetsController.setSystemBarsBehavior(BEHAVIOR_SHOW_TRANSIENT_BARS_BY_SWIPE);
|
||||
mPendingInsetsController.replayAndAttach(mReplayedController);
|
||||
verify(mReplayedController).setSystemBarsBehavior(
|
||||
eq(BEHAVIOR_SHOW_TRANSIENT_BARS_BY_SWIPE));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testBehavior_direct() {
|
||||
mPendingInsetsController.replayAndAttach(mReplayedController);
|
||||
mPendingInsetsController.setSystemBarsBehavior(BEHAVIOR_SHOW_TRANSIENT_BARS_BY_SWIPE);
|
||||
verify(mReplayedController).setSystemBarsBehavior(
|
||||
eq(BEHAVIOR_SHOW_TRANSIENT_BARS_BY_SWIPE));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testBehavior_direct_get() {
|
||||
when(mReplayedController.getSystemBarsBehavior())
|
||||
.thenReturn(BEHAVIOR_SHOW_TRANSIENT_BARS_BY_SWIPE);
|
||||
mPendingInsetsController.replayAndAttach(mReplayedController);
|
||||
assertEquals(mPendingInsetsController.getSystemBarsBehavior(),
|
||||
BEHAVIOR_SHOW_TRANSIENT_BARS_BY_SWIPE);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testAppearance() {
|
||||
mPendingInsetsController.setSystemBarsAppearance(
|
||||
APPEARANCE_LIGHT_STATUS_BARS, APPEARANCE_LIGHT_STATUS_BARS);
|
||||
mPendingInsetsController.replayAndAttach(mReplayedController);
|
||||
verify(mReplayedController).setSystemBarsAppearance(eq(APPEARANCE_LIGHT_STATUS_BARS),
|
||||
eq(APPEARANCE_LIGHT_STATUS_BARS));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testAppearance_direct() {
|
||||
mPendingInsetsController.replayAndAttach(mReplayedController);
|
||||
mPendingInsetsController.setSystemBarsAppearance(
|
||||
APPEARANCE_LIGHT_STATUS_BARS, APPEARANCE_LIGHT_STATUS_BARS);
|
||||
verify(mReplayedController).setSystemBarsAppearance(eq(APPEARANCE_LIGHT_STATUS_BARS),
|
||||
eq(APPEARANCE_LIGHT_STATUS_BARS));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testAppearance_direct_get() {
|
||||
when(mReplayedController.getSystemBarsAppearance())
|
||||
.thenReturn(APPEARANCE_LIGHT_STATUS_BARS);
|
||||
mPendingInsetsController.replayAndAttach(mReplayedController);
|
||||
assertEquals(mPendingInsetsController.getSystemBarsAppearance(),
|
||||
APPEARANCE_LIGHT_STATUS_BARS);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testReplayTwice() {
|
||||
mPendingInsetsController.show(systemBars());
|
||||
mPendingInsetsController.setSystemBarsBehavior(BEHAVIOR_SHOW_TRANSIENT_BARS_BY_SWIPE);
|
||||
mPendingInsetsController.setSystemBarsAppearance(APPEARANCE_LIGHT_STATUS_BARS,
|
||||
APPEARANCE_LIGHT_STATUS_BARS);
|
||||
mPendingInsetsController.replayAndAttach(mReplayedController);
|
||||
InsetsController secondController = mock(InsetsController.class);
|
||||
mPendingInsetsController.replayAndAttach(secondController);
|
||||
verify(mReplayedController).show(eq(systemBars()));
|
||||
verifyZeroInteractions(secondController);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testDetachReattach() {
|
||||
mPendingInsetsController.show(systemBars());
|
||||
mPendingInsetsController.setSystemBarsBehavior(BEHAVIOR_SHOW_TRANSIENT_BARS_BY_SWIPE);
|
||||
mPendingInsetsController.setSystemBarsAppearance(APPEARANCE_LIGHT_STATUS_BARS,
|
||||
APPEARANCE_LIGHT_STATUS_BARS);
|
||||
mPendingInsetsController.replayAndAttach(mReplayedController);
|
||||
mPendingInsetsController.detach();
|
||||
mPendingInsetsController.show(navigationBars());
|
||||
InsetsController secondController = mock(InsetsController.class);
|
||||
mPendingInsetsController.replayAndAttach(secondController);
|
||||
|
||||
verify(mReplayedController).show(eq(systemBars()));
|
||||
verify(secondController).show(eq(navigationBars()));
|
||||
}
|
||||
}
|
||||
@@ -46,7 +46,8 @@ public final class FrameworksTestsFilter extends SelectTest {
|
||||
"android.view.InsetsSourceTest",
|
||||
"android.view.InsetsSourceConsumerTest",
|
||||
"android.view.InsetsStateTest",
|
||||
"android.view.WindowMetricsTest"
|
||||
"android.view.WindowMetricsTest",
|
||||
"android.view.PendingInsetsControllerTest"
|
||||
};
|
||||
|
||||
public FrameworksTestsFilter(Bundle testArgs) {
|
||||
|
||||
Reference in New Issue
Block a user