diff options
4 files changed, 238 insertions, 0 deletions
diff --git a/core/java/android/window/flags/windowing_frontend.aconfig b/core/java/android/window/flags/windowing_frontend.aconfig index b6aad1145880..25af5d1456a3 100644 --- a/core/java/android/window/flags/windowing_frontend.aconfig +++ b/core/java/android/window/flags/windowing_frontend.aconfig @@ -70,6 +70,17 @@ flag { } flag { + name: "common_surface_animator" + namespace: "windowing_frontend" + description: "A reusable surface animator for default transition" + bug: "326331384" + is_fixed_read_only: true + metadata { + purpose: PURPOSE_BUGFIX + } +} + +flag { name: "reduce_keyguard_transitions" namespace: "windowing_frontend" description: "Avoid setting keyguard transitions ready unless there are no other changes" diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/transition/DefaultSurfaceAnimator.java b/libs/WindowManager/Shell/src/com/android/wm/shell/transition/DefaultSurfaceAnimator.java new file mode 100644 index 000000000000..a1a9ca9fd2bd --- /dev/null +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/transition/DefaultSurfaceAnimator.java @@ -0,0 +1,188 @@ +/* + * Copyright (C) 2024 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.wm.shell.transition; + +import android.animation.Animator; +import android.animation.AnimatorListenerAdapter; +import android.animation.ValueAnimator; +import android.annotation.NonNull; +import android.annotation.Nullable; +import android.graphics.Insets; +import android.graphics.Point; +import android.graphics.Rect; +import android.view.Choreographer; +import android.view.SurfaceControl; +import android.view.animation.Animation; +import android.view.animation.Transformation; + +import com.android.wm.shell.common.ShellExecutor; +import com.android.wm.shell.shared.TransactionPool; + +import java.util.ArrayList; + +public class DefaultSurfaceAnimator { + + /** Builds an animator for the surface and adds it to the `animations` list. */ + static void buildSurfaceAnimation(@NonNull ArrayList<Animator> animations, + @NonNull Animation anim, @NonNull SurfaceControl leash, + @NonNull Runnable finishCallback, @NonNull TransactionPool pool, + @NonNull ShellExecutor mainExecutor, @Nullable Point position, float cornerRadius, + @Nullable Rect clipRect, boolean isActivity) { + final DefaultAnimationAdapter adapter = new DefaultAnimationAdapter(anim, leash, + position, clipRect, cornerRadius, isActivity); + buildSurfaceAnimation(animations, anim, finishCallback, pool, mainExecutor, adapter); + } + + /** Builds an animator for the surface and adds it to the `animations` list. */ + static void buildSurfaceAnimation(@NonNull ArrayList<Animator> animations, + @NonNull Animation anim, @NonNull Runnable finishCallback, + @NonNull TransactionPool pool, @NonNull ShellExecutor mainExecutor, + @NonNull AnimationAdapter updateListener) { + final SurfaceControl.Transaction transaction = pool.acquire(); + updateListener.setTransaction(transaction); + final ValueAnimator va = ValueAnimator.ofFloat(0f, 1f); + // Animation length is already expected to be scaled. + va.overrideDurationScale(1.0f); + va.setDuration(anim.computeDurationHint()); + va.addUpdateListener(updateListener); + va.addListener(new AnimatorListenerAdapter() { + // It is possible for the end/cancel to be called more than once, which may cause + // issues if the animating surface has already been released. Track the finished + // state here to skip duplicate callbacks. See b/252872225. + private boolean mFinished; + + @Override + public void onAnimationEnd(Animator animation) { + onFinish(); + } + + @Override + public void onAnimationCancel(Animator animation) { + onFinish(); + } + + private void onFinish() { + if (mFinished) return; + mFinished = true; + // Apply transformation of end state in case the animation is canceled. + if (va.getAnimatedFraction() < 1f) { + va.setCurrentFraction(1f); + } + + pool.release(transaction); + mainExecutor.execute(() -> { + animations.remove(va); + finishCallback.run(); + }); + // The update listener can continue to be called after the animation has ended if + // end() is called manually again before the finisher removes the animation. + // Remove it manually here to prevent animating a released surface. + // See b/252872225. + va.removeUpdateListener(updateListener); + } + }); + animations.add(va); + } + + /** The animation adapter for buildSurfaceAnimation. */ + abstract static class AnimationAdapter implements ValueAnimator.AnimatorUpdateListener { + @NonNull final SurfaceControl mLeash; + @NonNull SurfaceControl.Transaction mTransaction; + private Choreographer mChoreographer; + + AnimationAdapter(@NonNull SurfaceControl leash) { + mLeash = leash; + } + + void setTransaction(@NonNull SurfaceControl.Transaction transaction) { + mTransaction = transaction; + } + + @Override + public void onAnimationUpdate(@NonNull ValueAnimator animator) { + // The finish callback in buildSurfaceAnimation will ensure that the animation ends + // with fraction 1. + final long currentPlayTime = animator.getAnimatedFraction() >= 1f + ? animator.getDuration() + : Math.min(animator.getDuration(), animator.getCurrentPlayTime()); + applyTransformation(animator, currentPlayTime); + if (mChoreographer == null) { + mChoreographer = Choreographer.getInstance(); + } + mTransaction.setFrameTimelineVsync(mChoreographer.getVsyncId()); + mTransaction.apply(); + } + + abstract void applyTransformation(@NonNull ValueAnimator animator, long currentPlayTime); + } + + private static class DefaultAnimationAdapter extends AnimationAdapter { + final Transformation mTransformation = new Transformation(); + final float[] mMatrix = new float[9]; + @NonNull final Animation mAnim; + @Nullable final Point mPosition; + @Nullable final Rect mClipRect; + final float mCornerRadius; + final boolean mIsActivity; + + DefaultAnimationAdapter(@NonNull Animation anim, @NonNull SurfaceControl leash, + @Nullable Point position, @Nullable Rect clipRect, float cornerRadius, + boolean isActivity) { + super(leash); + mAnim = anim; + mPosition = (position != null && (position.x != 0 || position.y != 0)) + ? position : null; + mClipRect = (clipRect != null && !clipRect.isEmpty()) ? clipRect : null; + mCornerRadius = cornerRadius; + mIsActivity = isActivity; + } + + @Override + void applyTransformation(@NonNull ValueAnimator animator, long currentPlayTime) { + final Transformation transformation = mTransformation; + final SurfaceControl.Transaction t = mTransaction; + final SurfaceControl leash = mLeash; + transformation.clear(); + mAnim.getTransformation(currentPlayTime, transformation); + if (com.android.graphics.libgui.flags.Flags.edgeExtensionShader() + && mIsActivity && mAnim.getExtensionEdges() != 0) { + t.setEdgeExtensionEffect(leash, mAnim.getExtensionEdges()); + } + if (mPosition != null) { + transformation.getMatrix().postTranslate(mPosition.x, mPosition.y); + } + t.setMatrix(leash, transformation.getMatrix(), mMatrix); + t.setAlpha(leash, transformation.getAlpha()); + + if (mClipRect != null) { + Rect clipRect = mClipRect; + final Insets extensionInsets = Insets.min(transformation.getInsets(), Insets.NONE); + if (!extensionInsets.equals(Insets.NONE)) { + // Clip out any overflowing edge extension. + clipRect = new Rect(mClipRect); + clipRect.inset(extensionInsets); + t.setCrop(leash, clipRect); + } + if (mCornerRadius > 0 && mAnim.hasRoundedCorners()) { + // Rounded corner can only be applied if a crop is set. + t.setCrop(leash, clipRect); + t.setCornerRadius(leash, mCornerRadius); + } + } + } + } +} diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/transition/DefaultTransitionHandler.java b/libs/WindowManager/Shell/src/com/android/wm/shell/transition/DefaultTransitionHandler.java index f40e0bac1b4e..d3bed59f7994 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/transition/DefaultTransitionHandler.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/transition/DefaultTransitionHandler.java @@ -826,6 +826,11 @@ public class DefaultTransitionHandler implements Transitions.TransitionHandler { @NonNull Runnable finishCallback, @NonNull TransactionPool pool, @NonNull ShellExecutor mainExecutor, @Nullable Point position, float cornerRadius, @Nullable Rect clipRect, boolean isActivity) { + if (Flags.commonSurfaceAnimator()) { + DefaultSurfaceAnimator.buildSurfaceAnimation(animations, anim, leash, finishCallback, + pool, mainExecutor, position, cornerRadius, clipRect, isActivity); + return; + } final SurfaceControl.Transaction transaction = pool.acquire(); final ValueAnimator va = ValueAnimator.ofFloat(0f, 1f); final Transformation transformation = new Transformation(); diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/transition/DefaultTransitionHandlerTest.java b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/transition/DefaultTransitionHandlerTest.java index 0c18229f38d0..e540322a96a1 100644 --- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/transition/DefaultTransitionHandlerTest.java +++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/transition/DefaultTransitionHandlerTest.java @@ -25,11 +25,14 @@ import static android.view.WindowManager.TRANSIT_TO_BACK; import static android.window.TransitionInfo.FLAG_SYNC; import static android.window.TransitionInfo.FLAG_TRANSLUCENT; +import static org.junit.Assert.assertEquals; import static org.mockito.ArgumentMatchers.any; import static org.mockito.Mockito.mock; import static org.mockito.Mockito.never; import static org.mockito.Mockito.verify; +import android.animation.Animator; +import android.animation.ValueAnimator; import android.app.ActivityManager.RunningTaskInfo; import android.content.Context; import android.os.Binder; @@ -37,6 +40,7 @@ import android.os.Handler; import android.os.IBinder; import android.os.Looper; import android.view.SurfaceControl; +import android.view.animation.AlphaAnimation; import android.window.TransitionInfo; import android.window.WindowContainerToken; @@ -56,6 +60,8 @@ import org.junit.Before; import org.junit.Test; import org.junit.runner.RunWith; +import java.util.ArrayList; + /** * Tests for the default animation handler that is used if no other special-purpose handler picks * up an animation request. @@ -187,6 +193,34 @@ public class DefaultTransitionHandlerTest extends ShellTestCase { } @Test + public void testBuildSurfaceAnimation() { + final ArrayList<Animator> animators = new ArrayList<>(); + final AlphaAnimation animation = new AlphaAnimation(0, 1); + final long durationMs = 500; + animation.setDuration(durationMs); + final long[] lastCurrentPlayTime = new long[1]; + final int[] finishCount = new int[1]; + final Runnable finishCallback = () -> finishCount[0]++; + DefaultSurfaceAnimator.buildSurfaceAnimation(animators, animation, finishCallback, + mTransactionPool, mMainExecutor, + new DefaultSurfaceAnimator.AnimationAdapter(mock(SurfaceControl.class)) { + @Override + void applyTransformation(ValueAnimator animator, long currentPlayTime) { + lastCurrentPlayTime[0] = currentPlayTime; + } + }); + final ValueAnimator animator = (ValueAnimator) animators.get(0); + mAnimExecutor.execute(() -> { + animator.start(); + animator.end(); + }); + flushHandlers(); + assertEquals(durationMs, lastCurrentPlayTime[0]); + assertEquals(1f, animator.getAnimatedFraction(), 0f /* delta */); + assertEquals(1, finishCount[0]); + } + + @Test public void startAnimation_freeformOpenChange_doesntReparentTask() { final TransitionInfo.Change openChange = new ChangeBuilder(TRANSIT_OPEN) .setTask(createTaskInfo( |