/* * Copyright (C) 2021 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 androidx.window.extensions.embedding; import static android.graphics.Matrix.MTRANS_X; import static android.graphics.Matrix.MTRANS_Y; import static android.view.RemoteAnimationTarget.MODE_CLOSING; import android.graphics.Point; import android.graphics.Rect; import android.view.Choreographer; import android.view.RemoteAnimationTarget; import android.view.SurfaceControl; import android.view.animation.Animation; import android.view.animation.Transformation; import androidx.annotation.NonNull; /** * Wrapper to handle the TaskFragment animation update in one {@link SurfaceControl.Transaction}. * * The base adapter can be used for {@link RemoteAnimationTarget} that is simple open/close. */ class TaskFragmentAnimationAdapter { /** * If {@link #mOverrideLayer} is set to this value, we don't want to override the surface layer. */ private static final int LAYER_NO_OVERRIDE = -1; @NonNull final Animation mAnimation; @NonNull final RemoteAnimationTarget mTarget; @NonNull final SurfaceControl mLeash; /** Area in absolute coordinate that the animation surface shouldn't go beyond. */ @NonNull private final Rect mWholeAnimationBounds = new Rect(); /** * Area in absolute coordinate that should represent all the content to show for this window. * This should be the end bounds for opening window, and start bounds for closing window in case * the window is resizing during the open/close transition. */ @NonNull private final Rect mContentBounds = new Rect(); /** Offset relative to the window parent surface for {@link #mContentBounds}. */ @NonNull private final Point mContentRelOffset = new Point(); @NonNull final Transformation mTransformation = new Transformation(); @NonNull final float[] mMatrix = new float[9]; @NonNull final float[] mVecs = new float[4]; @NonNull final Rect mRect = new Rect(); private boolean mIsFirstFrame = true; private int mOverrideLayer = LAYER_NO_OVERRIDE; TaskFragmentAnimationAdapter(@NonNull Animation animation, @NonNull RemoteAnimationTarget target) { this(animation, target, target.leash, target.screenSpaceBounds); } /** * @param leash the surface to animate. * @param wholeAnimationBounds area in absolute coordinate that the animation surface shouldn't * go beyond. */ TaskFragmentAnimationAdapter(@NonNull Animation animation, @NonNull RemoteAnimationTarget target, @NonNull SurfaceControl leash, @NonNull Rect wholeAnimationBounds) { mAnimation = animation; mTarget = target; mLeash = leash; mWholeAnimationBounds.set(wholeAnimationBounds); if (target.mode == MODE_CLOSING) { // When it is closing, we want to show the content at the start position in case the // window is resizing as well. For example, when the activities is changing from split // to stack, the bottom TaskFragment will be resized to fullscreen when hiding. final Rect startBounds = target.startBounds; final Rect endBounds = target.screenSpaceBounds; mContentBounds.set(startBounds); mContentRelOffset.set(target.localBounds.left, target.localBounds.top); mContentRelOffset.offset( startBounds.left - endBounds.left, startBounds.top - endBounds.top); } else { mContentBounds.set(target.screenSpaceBounds); mContentRelOffset.set(target.localBounds.left, target.localBounds.top); } } /** * Surface layer to be set at the first frame of the animation. We will not set the layer if it * is set to {@link #LAYER_NO_OVERRIDE}. */ final void overrideLayer(int layer) { mOverrideLayer = layer; } /** Called on frame update. */ final void onAnimationUpdate(@NonNull SurfaceControl.Transaction t, long currentPlayTime) { if (mIsFirstFrame) { t.show(mLeash); if (mOverrideLayer != LAYER_NO_OVERRIDE) { t.setLayer(mLeash, mOverrideLayer); } mIsFirstFrame = false; } // Extract the transformation to the current time. mAnimation.getTransformation(Math.min(currentPlayTime, mAnimation.getDuration()), mTransformation); t.setFrameTimelineVsync(Choreographer.getInstance().getVsyncId()); onAnimationUpdateInner(t); } /** To be overridden by subclasses to adjust the animation surface change. */ void onAnimationUpdateInner(@NonNull SurfaceControl.Transaction t) { // Update the surface position and alpha. mTransformation.getMatrix().postTranslate(mContentRelOffset.x, mContentRelOffset.y); t.setMatrix(mLeash, mTransformation.getMatrix(), mMatrix); t.setAlpha(mLeash, mTransformation.getAlpha()); // Get current surface bounds in absolute coordinate. // positionX/Y are in local coordinate, so minus the local offset to get the slide amount. final int positionX = Math.round(mMatrix[MTRANS_X]); final int positionY = Math.round(mMatrix[MTRANS_Y]); final Rect cropRect = new Rect(mContentBounds); cropRect.offset(positionX - mContentRelOffset.x, positionY - mContentRelOffset.y); // Store the current offset of the surface top left from (0,0) in absolute coordinate. final int offsetX = cropRect.left; final int offsetY = cropRect.top; // Intersect to make sure the animation happens within the whole animation bounds. if (!cropRect.intersect(mWholeAnimationBounds)) { // Hide the surface when it is outside of the animation area. t.setAlpha(mLeash, 0); } // cropRect is in absolute coordinate, so we need to translate it to surface top left. cropRect.offset(-offsetX, -offsetY); t.setCrop(mLeash, cropRect); } /** Called after animation finished. */ final void onAnimationEnd(@NonNull SurfaceControl.Transaction t) { onAnimationUpdate(t, mAnimation.getDuration()); } final long getDurationHint() { return mAnimation.computeDurationHint(); } /** * Should be used for the animation of the snapshot of a {@link RemoteAnimationTarget} that has * size change. */ static class SnapshotAdapter extends TaskFragmentAnimationAdapter { SnapshotAdapter(@NonNull Animation animation, @NonNull RemoteAnimationTarget target) { // Start leash is the snapshot of the starting surface. super(animation, target, target.startLeash, target.screenSpaceBounds); } @Override void onAnimationUpdateInner(@NonNull SurfaceControl.Transaction t) { // Snapshot should always be placed at the top left of the animation leash. mTransformation.getMatrix().postTranslate(0, 0); t.setMatrix(mLeash, mTransformation.getMatrix(), mMatrix); t.setAlpha(mLeash, mTransformation.getAlpha()); } } /** * Should be used for the animation of the {@link RemoteAnimationTarget} that has size change. */ static class BoundsChangeAdapter extends TaskFragmentAnimationAdapter { BoundsChangeAdapter(@NonNull Animation animation, @NonNull RemoteAnimationTarget target) { super(animation, target); } @Override void onAnimationUpdateInner(@NonNull SurfaceControl.Transaction t) { mTransformation.getMatrix().postTranslate( mTarget.localBounds.left, mTarget.localBounds.top); t.setMatrix(mLeash, mTransformation.getMatrix(), mMatrix); t.setAlpha(mLeash, mTransformation.getAlpha()); // The following applies an inverse scale to the clip-rect so that it crops "after" the // scale instead of before. mVecs[1] = mVecs[2] = 0; mVecs[0] = mVecs[3] = 1; mTransformation.getMatrix().mapVectors(mVecs); mVecs[0] = 1.f / mVecs[0]; mVecs[3] = 1.f / mVecs[3]; final Rect clipRect = mTransformation.getClipRect(); mRect.left = (int) (clipRect.left * mVecs[0] + 0.5f); mRect.right = (int) (clipRect.right * mVecs[0] + 0.5f); mRect.top = (int) (clipRect.top * mVecs[3] + 0.5f); mRect.bottom = (int) (clipRect.bottom * mVecs[3] + 0.5f); t.setWindowCrop(mLeash, mRect); } } }