Replace setFinalCrop with temporary layer
- Creates a temporary animation bounds layer when setting up the
animation in AppWindowToken
- The leash is parented to this new layer if the appStackClipMode
is set to STACK_CLIP_AFTER_ANIM
Test: Manual test - open activity in split screen
Test: atest FrameworksServicesTests:AppWindowTokenAnimationTests
Change-Id: I6a9cf99832ee868a6e65da0150291d521f5eca35
diff --git a/services/core/java/com/android/server/wm/AppWindowToken.java b/services/core/java/com/android/server/wm/AppWindowToken.java
index d9ddf9f..fe8ca28 100644
--- a/services/core/java/com/android/server/wm/AppWindowToken.java
+++ b/services/core/java/com/android/server/wm/AppWindowToken.java
@@ -78,6 +78,7 @@
import static com.android.server.wm.AppWindowTokenProto.STARTING_WINDOW;
import static com.android.server.wm.AppWindowTokenProto.THUMBNAIL;
import static com.android.server.wm.AppWindowTokenProto.WINDOW_TOKEN;
+import static com.android.server.wm.WindowStateAnimator.STACK_CLIP_AFTER_ANIM;
import android.annotation.CallSuper;
import android.app.Activity;
@@ -264,6 +265,12 @@
*/
private boolean mWillCloseOrEnterPip;
+ /** Layer used to constrain the animation to a token's stack bounds. */
+ SurfaceControl mAnimationBoundsLayer;
+
+ /** Whether this token needs to create mAnimationBoundsLayer for cropping animations. */
+ boolean mNeedsAnimationBoundsLayer;
+
AppWindowToken(WindowManagerService service, IApplicationToken token, boolean voiceInteraction,
DisplayContent dc, long inputDispatchingTimeoutNanos, boolean fullscreen,
boolean showForAllUsers, int targetSdk, int orientation, int rotationAnimationHint,
@@ -1720,6 +1727,20 @@
return !isSplitScreenPrimary || allowSplitScreenPrimaryAnimation;
}
+ /**
+ * Creates a layer to apply crop to an animation.
+ */
+ private SurfaceControl createAnimationBoundsLayer(Transaction t) {
+ if (DEBUG_APP_TRANSITIONS || DEBUG_ANIM) Slog.i(TAG, "Creating animation bounds layer");
+ final SurfaceControl.Builder builder = makeAnimationLeash()
+ .setParent(getAnimationLeashParent())
+ .setName(getSurfaceControl() + " - animation-bounds")
+ .setSize(getSurfaceWidth(), getSurfaceHeight());
+ final SurfaceControl boundsLayer = builder.build();
+ t.show(boundsLayer);
+ return boundsLayer;
+ }
+
boolean applyAnimationLocked(WindowManager.LayoutParams lp, int transit, boolean enter,
boolean isVoiceInteraction) {
@@ -1753,12 +1774,15 @@
adapter = mService.mAppTransition.getRemoteAnimationController()
.createAnimationAdapter(this, mTmpPoint, mTmpRect);
} else {
+ final int appStackClipMode = mService.mAppTransition.getAppStackClipMode();
+ mNeedsAnimationBoundsLayer = (appStackClipMode == STACK_CLIP_AFTER_ANIM);
+
final Animation a = loadAnimation(lp, transit, enter, isVoiceInteraction);
if (a != null) {
adapter = new LocalAnimationAdapter(
new WindowAnimationSpec(a, mTmpPoint, mTmpRect,
mService.mAppTransition.canSkipFirstFrame(),
- mService.mAppTransition.getAppStackClipMode(),
+ appStackClipMode,
true /* isAppAnimation */),
mService.mSurfaceAnimationRunner);
if (a.getZAdjustment() == Animation.ZORDER_TOP) {
@@ -1858,6 +1882,11 @@
@Override
public void onAnimationLeashDestroyed(Transaction t) {
super.onAnimationLeashDestroyed(t);
+ if (mAnimationBoundsLayer != null) {
+ t.destroy(mAnimationBoundsLayer);
+ mAnimationBoundsLayer = null;
+ }
+
if (mAnimatingAppWindowTokenRegistry != null) {
mAnimatingAppWindowTokenRegistry.notifyFinished(this);
}
@@ -1908,6 +1937,26 @@
if (mAnimatingAppWindowTokenRegistry != null) {
mAnimatingAppWindowTokenRegistry.notifyStarting(this);
}
+
+ // If the animation needs to be cropped then an animation bounds layer is created as a child
+ // of the pinned stack or animation layer. The leash is then reparented to this new layer.
+ if (mNeedsAnimationBoundsLayer) {
+ final TaskStack stack = getStack();
+ if (stack == null) {
+ return;
+ }
+ mAnimationBoundsLayer = createAnimationBoundsLayer(t);
+
+ // Set clip rect to stack bounds.
+ mTmpRect.setEmpty();
+ stack.getBounds(mTmpRect);
+
+ // Crop to stack bounds.
+ t.setWindowCrop(mAnimationBoundsLayer, mTmpRect);
+
+ // Reparent leash to animation bounds layer.
+ t.reparent(leash, mAnimationBoundsLayer.getHandle());
+ }
}
/**
@@ -1927,6 +1976,7 @@
mTransit = TRANSIT_UNSET;
mTransitFlags = 0;
mNeedsZBoost = false;
+ mNeedsAnimationBoundsLayer = false;
setAppLayoutChanges(FINISH_LAYOUT_REDO_ANIM | FINISH_LAYOUT_REDO_WALLPAPER,
"AppWindowToken");
diff --git a/services/core/java/com/android/server/wm/WindowAnimationSpec.java b/services/core/java/com/android/server/wm/WindowAnimationSpec.java
index 548e23a..825255e 100644
--- a/services/core/java/com/android/server/wm/WindowAnimationSpec.java
+++ b/services/core/java/com/android/server/wm/WindowAnimationSpec.java
@@ -99,13 +99,7 @@
tmp.transformation.getMatrix().postTranslate(mPosition.x, mPosition.y);
t.setMatrix(leash, tmp.transformation.getMatrix(), tmp.floats);
t.setAlpha(leash, tmp.transformation.getAlpha());
- if (mStackClipMode == STACK_CLIP_NONE) {
- t.setWindowCrop(leash, tmp.transformation.getClipRect());
- } else if (mStackClipMode == STACK_CLIP_AFTER_ANIM) {
- mTmpRect.set(mStackBounds);
- // Offset stack bounds to stack position so the final crop is in screen space.
- mTmpRect.offsetTo(mPosition.x, mPosition.y);
- t.setFinalCrop(leash, mTmpRect);
+ if (mStackClipMode == STACK_CLIP_NONE || mStackClipMode == STACK_CLIP_AFTER_ANIM) {
t.setWindowCrop(leash, tmp.transformation.getClipRect());
} else {
mTmpRect.set(mStackBounds);
diff --git a/services/tests/servicestests/src/com/android/server/wm/AppWindowTokenAnimationTests.java b/services/tests/servicestests/src/com/android/server/wm/AppWindowTokenAnimationTests.java
new file mode 100644
index 0000000..c3907ff
--- /dev/null
+++ b/services/tests/servicestests/src/com/android/server/wm/AppWindowTokenAnimationTests.java
@@ -0,0 +1,118 @@
+/*
+ * 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.server.wm;
+
+import static android.app.WindowConfiguration.ACTIVITY_TYPE_STANDARD;
+import static android.app.WindowConfiguration.WINDOWING_MODE_FULLSCREEN;
+import static android.view.SurfaceControl.Transaction;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.ArgumentMatchers.eq;
+import static org.mockito.Mockito.verify;
+
+import android.support.test.filters.SmallTest;
+import android.support.test.runner.AndroidJUnit4;
+import android.view.SurfaceControl;
+
+import com.android.server.wm.WindowTestUtils.TestAppWindowToken;
+
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.mockito.ArgumentCaptor;
+import org.mockito.Mock;
+import org.mockito.MockitoAnnotations;
+
+/**
+ * Animation related tests for the {@link AppWindowToken} class.
+ *
+ * Build/Install/Run:
+ * atest FrameworksServicesTests:com.android.server.wm.AppWindowTokenAnimationTests
+ */
+@SmallTest
+@RunWith(AndroidJUnit4.class)
+public class AppWindowTokenAnimationTests extends WindowTestsBase {
+
+ private TestAppWindowToken mToken;
+
+ @Mock
+ private Transaction mTransaction;
+ @Mock
+ private AnimationAdapter mSpec;
+
+ @Override
+ public void setUp() throws Exception {
+ super.setUp();
+ MockitoAnnotations.initMocks(this);
+
+ mToken = createTestAppWindowToken(mDisplayContent, WINDOWING_MODE_FULLSCREEN,
+ ACTIVITY_TYPE_STANDARD);
+ mToken.setPendingTransaction(mTransaction);
+ }
+
+ @Test
+ public void clipAfterAnim_boundsLayerIsCreated() throws Exception {
+ mToken.mNeedsAnimationBoundsLayer = true;
+
+ mToken.mSurfaceAnimator.startAnimation(mTransaction, mSpec, true /* hidden */);
+ verify(mTransaction).reparent(eq(mToken.getSurfaceControl()),
+ eq(mToken.mSurfaceAnimator.mLeash.getHandle()));
+ verify(mTransaction).reparent(eq(mToken.mSurfaceAnimator.mLeash),
+ eq(mToken.mAnimationBoundsLayer.getHandle()));
+ }
+
+ @Test
+ public void clipAfterAnim_boundsLayerIsDestroyed() throws Exception {
+ mToken.mNeedsAnimationBoundsLayer = true;
+ mToken.mSurfaceAnimator.startAnimation(mTransaction, mSpec, true /* hidden */);
+ final SurfaceControl leash = mToken.mSurfaceAnimator.mLeash;
+ final SurfaceControl animationBoundsLayer = mToken.mAnimationBoundsLayer;
+ final ArgumentCaptor<SurfaceAnimator.OnAnimationFinishedCallback> callbackCaptor =
+ ArgumentCaptor.forClass(
+ SurfaceAnimator.OnAnimationFinishedCallback.class);
+ verify(mSpec).startAnimation(any(), any(), callbackCaptor.capture());
+
+ callbackCaptor.getValue().onAnimationFinished(mSpec);
+ verify(mTransaction).destroy(eq(leash));
+ verify(mTransaction).destroy(eq(animationBoundsLayer));
+ assertThat(mToken.mNeedsAnimationBoundsLayer).isFalse();
+ }
+
+ @Test
+ public void clipAfterAnimCancelled_boundsLayerIsDestroyed() throws Exception {
+ mToken.mNeedsAnimationBoundsLayer = true;
+ mToken.mSurfaceAnimator.startAnimation(mTransaction, mSpec, true /* hidden */);
+ final SurfaceControl leash = mToken.mSurfaceAnimator.mLeash;
+ final SurfaceControl animationBoundsLayer = mToken.mAnimationBoundsLayer;
+
+ mToken.mSurfaceAnimator.cancelAnimation();
+ verify(mTransaction).destroy(eq(leash));
+ verify(mTransaction).destroy(eq(animationBoundsLayer));
+ assertThat(mToken.mNeedsAnimationBoundsLayer).isFalse();
+ }
+
+ @Test
+ public void clipNoneAnim_boundsLayerIsNotCreated() throws Exception {
+ mToken.mNeedsAnimationBoundsLayer = false;
+
+ mToken.mSurfaceAnimator.startAnimation(mTransaction, mSpec, true /* hidden */);
+ verify(mTransaction).reparent(eq(mToken.getSurfaceControl()),
+ eq(mToken.mSurfaceAnimator.mLeash.getHandle()));
+ assertThat(mToken.mAnimationBoundsLayer).isNull();
+ }
+}
diff --git a/services/tests/servicestests/src/com/android/server/wm/WindowAnimationSpecTest.java b/services/tests/servicestests/src/com/android/server/wm/WindowAnimationSpecTest.java
index ca520ed..9dc0025 100644
--- a/services/tests/servicestests/src/com/android/server/wm/WindowAnimationSpecTest.java
+++ b/services/tests/servicestests/src/com/android/server/wm/WindowAnimationSpecTest.java
@@ -70,8 +70,6 @@
true /* isAppAnimation */);
windowAnimationSpec.apply(mTransaction, mSurfaceControl, 0);
verify(mTransaction).setWindowCrop(eq(mSurfaceControl), argThat(Rect::isEmpty));
- verify(mTransaction).setFinalCrop(eq(mSurfaceControl),
- argThat(rect -> rect.equals(mStackBounds)));
}
@Test
@@ -83,9 +81,6 @@
true /* isAppAnimation */);
windowAnimationSpec.apply(mTransaction, mSurfaceControl, 0);
verify(mTransaction).setWindowCrop(eq(mSurfaceControl), argThat(Rect::isEmpty));
- verify(mTransaction).setFinalCrop(eq(mSurfaceControl),
- argThat(rect -> rect.left == 20 && rect.top == 40 && rect.right == 30
- && rect.bottom == 50));
}
@Test
diff --git a/services/tests/servicestests/src/com/android/server/wm/WindowTestUtils.java b/services/tests/servicestests/src/com/android/server/wm/WindowTestUtils.java
index 2e4740b..a48a3b9 100644
--- a/services/tests/servicestests/src/com/android/server/wm/WindowTestUtils.java
+++ b/services/tests/servicestests/src/com/android/server/wm/WindowTestUtils.java
@@ -24,6 +24,8 @@
import android.os.IBinder;
import android.view.IApplicationToken;
import android.view.IWindow;
+import android.view.SurfaceControl;
+import android.view.SurfaceControl.Transaction;
import android.view.WindowManager;
import static android.app.AppOpsManager.OP_NONE;
@@ -113,6 +115,7 @@
/** Used so we can gain access to some protected members of the {@link AppWindowToken} class. */
public static class TestAppWindowToken extends AppWindowToken {
boolean mOnTop = false;
+ private Transaction mPendingTransactionOverride;
private TestAppWindowToken(DisplayContent dc) {
super(dc.mService, new IApplicationToken.Stub() {
@@ -158,6 +161,17 @@
boolean isOnTop() {
return mOnTop;
}
+
+ void setPendingTransaction(Transaction transaction) {
+ mPendingTransactionOverride = transaction;
+ }
+
+ @Override
+ public Transaction getPendingTransaction() {
+ return mPendingTransactionOverride == null
+ ? super.getPendingTransaction()
+ : mPendingTransactionOverride;
+ }
}
static TestWindowToken createTestWindowToken(int type, DisplayContent dc) {
diff --git a/services/tests/servicestests/src/com/android/server/wm/WindowTestsBase.java b/services/tests/servicestests/src/com/android/server/wm/WindowTestsBase.java
index 473a287..ef019fe 100644
--- a/services/tests/servicestests/src/com/android/server/wm/WindowTestsBase.java
+++ b/services/tests/servicestests/src/com/android/server/wm/WindowTestsBase.java
@@ -231,6 +231,11 @@
}
AppWindowToken createAppWindowToken(DisplayContent dc, int windowingMode, int activityType) {
+ return createTestAppWindowToken(dc, windowingMode, activityType);
+ }
+
+ WindowTestUtils.TestAppWindowToken createTestAppWindowToken(DisplayContent dc, int
+ windowingMode, int activityType) {
final TaskStack stack = createStackControllerOnStackOnDisplay(windowingMode, activityType,
dc).mContainer;
final Task task = createTaskInStack(stack, 0 /* userId */);