Donate to e Foundation | Murena handsets with /e/OS | Own a part of Murena! Learn more

Commit e698208f authored by Riddle Hsu's avatar Riddle Hsu
Browse files

Revert^2 "Extra common logic of default transition animation"

Extra common logic of default transition animation

Make it more flexible to run other animation implementation.

This reverts commit 625ff9d8.
Reason for revert: reland

The previous patch has a problem by calling
AnimationAdapter#onAnimationUpdate again at the end of animation.
Originally it uses va.getDuration() as currentPlayTime, which
will apply the end state of transformation.

The problem is that the adapter uses animator.getCurrentPlayTime(),
which will be 0 if the animation is finished. That causes the
transformation to apply the start state. Then the target surface
could move to original offset which may be outside screen.

The fix is to check if it is animating the last frame, then
use va.getDuration() as currentPlayTime. This also reduces
duplicated surface transactions for the last frame if the animation
ends normally.

Note that the animated fraction will be 1 if the animation plays
to end or ValueAnimator#end is called before animation finishes.
Only ValueAnimator#cancel may satisfy the condition
"va.getAnimatedFraction() < 1f". So far there is no cancel usages.
Just make sure that end state is always applied.

Bug: 326331384
Flag: com.android.window.flags.common_surface_animator
Test: atest DefaultTransitionHandler#testBuildSurfaceAnimation
Change-Id: I082bfe2e57e4374cca9fb30c260051fbafbd21f7
parent 07769937
Loading
Loading
Loading
Loading
+11 −0
Original line number Diff line number Diff line
@@ -69,6 +69,17 @@ flag {
  bug: "291870756"
}

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"
+188 −0
Original line number Diff line number Diff line
/*
 * 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);
                }
            }
        }
    }
}
+5 −0
Original line number Diff line number Diff line
@@ -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();
+34 −0
Original line number Diff line number Diff line
@@ -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.
@@ -186,6 +192,34 @@ public class DefaultTransitionHandlerTest extends ShellTestCase {
        verify(startT, never()).setColor(any(), any());
    }

    @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)