summaryrefslogtreecommitdiff
path: root/quickstep/src/com/android/quickstep/util/SwipePipToHomeAnimator.java
blob: 7cc2c468fa1d0c7fd8ec26ed6519f717cea319a6 (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
/*
 * 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 com.android.quickstep.util;

import static com.android.systemui.shared.system.InteractionJankMonitorWrapper.CUJ_APP_CLOSE_TO_PIP;

import android.animation.Animator;
import android.animation.RectEvaluator;
import android.content.ComponentName;
import android.content.Context;
import android.content.pm.ActivityInfo;
import android.graphics.Matrix;
import android.graphics.Rect;
import android.graphics.RectF;
import android.util.Log;
import android.view.Surface;
import android.view.SurfaceControl;
import android.view.View;
import android.window.PictureInPictureSurfaceTransaction;

import androidx.annotation.NonNull;
import androidx.annotation.Nullable;

import com.android.launcher3.anim.AnimationSuccessListener;
import com.android.launcher3.icons.IconProvider;
import com.android.quickstep.TaskAnimationManager;
import com.android.systemui.shared.pip.PipSurfaceTransactionHelper;
import com.android.systemui.shared.system.InteractionJankMonitorWrapper;
import com.android.wm.shell.pip.PipContentOverlay;

/**
 * Subclass of {@link RectFSpringAnim} that animates an Activity to PiP (picture-in-picture) window
 * when swiping up (in gesture navigation mode).
 */
public class SwipePipToHomeAnimator extends RectFSpringAnim {
    private static final String TAG = SwipePipToHomeAnimator.class.getSimpleName();

    private static final float END_PROGRESS = 1.0f;

    private final int mTaskId;
    private final ActivityInfo mActivityInfo;
    private final SurfaceControl mLeash;
    private final Rect mSourceRectHint = new Rect();
    private final Rect mAppBounds = new Rect();
    private final Matrix mHomeToWindowPositionMap = new Matrix();
    private final Rect mStartBounds = new Rect();
    private final RectF mCurrentBoundsF = new RectF();
    private final Rect mCurrentBounds = new Rect();
    private final Rect mDestinationBounds = new Rect();
    private final PipSurfaceTransactionHelper mSurfaceTransactionHelper;

    /**
     * For calculating transform in
     * {@link #onAnimationUpdate(SurfaceControl.Transaction, RectF, float)}
     */
    private final RectEvaluator mInsetsEvaluator = new RectEvaluator(new Rect());
    private final Rect mSourceHintRectInsets;
    private final Rect mSourceInsets = new Rect();

    /** for rotation calculations */
    private final @RecentsOrientedState.SurfaceRotation int mFromRotation;
    private final Rect mDestinationBoundsTransformed = new Rect();

    /**
     * Flag to avoid the double-end problem since the leash would have been released
     * after the first end call and any further operations upon it would lead to NPE.
     */
    private boolean mHasAnimationEnded;

    /**
     * Wrapper of {@link SurfaceControl} that is used when entering PiP without valid
     * source rect hint.
     */
    @Nullable
    private PipContentOverlay mPipContentOverlay;

    /**
     * @param context {@link Context} provides Launcher resources
     * @param taskId Task id associated with this animator, see also {@link #getTaskId()}
     * @param activityInfo {@link ActivityInfo} associated with this animator,
     *                      see also {@link #getComponentName()}
     * @param appIconSizePx The size in pixel for the app icon in content overlay
     * @param leash {@link SurfaceControl} this animator operates on
     * @param sourceRectHint See the definition in {@link android.app.PictureInPictureParams}
     * @param appBounds Bounds of the application, sourceRectHint is based on this bounds
     * @param homeToWindowPositionMap {@link Matrix} to map a Rect from home to window space
     * @param startBounds Bounds of the application when this animator starts. This can be
     *                    different from the appBounds if user has swiped a certain distance and
     *                    Launcher has performed transform on the leash.
     * @param destinationBounds Bounds of the destination this animator ends to
     * @param fromRotation From rotation if different from final rotation, ROTATION_0 otherwise
     * @param destinationBoundsTransformed Destination bounds in window space
     * @param cornerRadius Corner radius in pixel value for PiP window
     * @param shadowRadius Shadow radius in pixel value for PiP window
     * @param view Attached view for logging purpose
     */
    private SwipePipToHomeAnimator(@NonNull Context context,
            int taskId,
            @NonNull ActivityInfo activityInfo,
            int appIconSizePx,
            @NonNull SurfaceControl leash,
            @Nullable Rect sourceRectHint,
            @NonNull Rect appBounds,
            @NonNull Matrix homeToWindowPositionMap,
            @NonNull RectF startBounds,
            @NonNull Rect destinationBounds,
            @RecentsOrientedState.SurfaceRotation int fromRotation,
            @NonNull Rect destinationBoundsTransformed,
            int cornerRadius,
            int shadowRadius,
            @NonNull View view) {
        super(new DefaultSpringConfig(context, null, startBounds,
                new RectF(destinationBoundsTransformed)));
        mTaskId = taskId;
        mActivityInfo = activityInfo;
        mLeash = leash;
        mAppBounds.set(appBounds);
        mHomeToWindowPositionMap.set(homeToWindowPositionMap);
        startBounds.round(mStartBounds);
        mDestinationBounds.set(destinationBounds);
        mFromRotation = fromRotation;
        mDestinationBoundsTransformed.set(destinationBoundsTransformed);
        mSurfaceTransactionHelper = new PipSurfaceTransactionHelper(cornerRadius, shadowRadius);

        if (sourceRectHint != null && (sourceRectHint.width() < destinationBounds.width()
                || sourceRectHint.height() < destinationBounds.height())) {
            // This is a situation in which the source hint rect on at least one axis is smaller
            // than the destination bounds, which presents a problem because we would have to scale
            // up that axis to fit the bounds. So instead, just fallback to the non-source hint
            // animation in this case.
            sourceRectHint = null;
        }

        if (sourceRectHint == null) {
            mSourceRectHint.setEmpty();
            mSourceHintRectInsets = null;

            // Create a new overlay layer. We do not call detach on this instance, it's propagated
            // to other classes like PipTaskOrganizer / RecentsAnimationController to complete
            // the cleanup.
            mPipContentOverlay = new PipContentOverlay.PipAppIconOverlay(view.getContext(),
                    mAppBounds, new IconProvider(context).getIcon(mActivityInfo),
                    appIconSizePx);
            final SurfaceControl.Transaction tx = new SurfaceControl.Transaction();
            mPipContentOverlay.attach(tx, mLeash);
        } else {
            mSourceRectHint.set(sourceRectHint);
            mSourceHintRectInsets = new Rect(sourceRectHint.left - appBounds.left,
                    sourceRectHint.top - appBounds.top,
                    appBounds.right - sourceRectHint.right,
                    appBounds.bottom - sourceRectHint.bottom);
        }

        addAnimatorListener(new AnimationSuccessListener() {
            @Override
            public void onAnimationStart(Animator animation) {
                InteractionJankMonitorWrapper.begin(view, CUJ_APP_CLOSE_TO_PIP);
                super.onAnimationStart(animation);
            }

            @Override
            public void onAnimationCancel(Animator animation) {
                super.onAnimationCancel(animation);
                InteractionJankMonitorWrapper.cancel(CUJ_APP_CLOSE_TO_PIP);
            }

            @Override
            public void onAnimationSuccess(Animator animator) {
                InteractionJankMonitorWrapper.end(CUJ_APP_CLOSE_TO_PIP);
            }

            @Override
            public void onAnimationEnd(Animator animation) {
                if (mHasAnimationEnded) return;
                super.onAnimationEnd(animation);
                mHasAnimationEnded = true;
            }
        });
        addOnUpdateListener(this::onAnimationUpdate);
    }

    private void onAnimationUpdate(RectF currentRect, float progress) {
        if (mHasAnimationEnded) return;
        final SurfaceControl.Transaction tx =
                PipSurfaceTransactionHelper.newSurfaceControlTransaction();
        mHomeToWindowPositionMap.mapRect(mCurrentBoundsF, currentRect);
        onAnimationUpdate(tx, mCurrentBoundsF, progress);
        tx.apply();
    }

    private PictureInPictureSurfaceTransaction onAnimationUpdate(SurfaceControl.Transaction tx,
            RectF currentRect, float progress) {
        currentRect.round(mCurrentBounds);
        if (mPipContentOverlay != null) {
            mPipContentOverlay.onAnimationUpdate(tx, mCurrentBounds, progress);
        }
        final PictureInPictureSurfaceTransaction op;
        if (mSourceHintRectInsets == null) {
            // no source rect hint been set, directly scale the window down
            op = onAnimationScale(progress, tx, mCurrentBounds);
        } else {
            // scale and crop according to the source rect hint
            op = onAnimationScaleAndCrop(progress, tx, mCurrentBounds);
        }
        return op;
    }

    /** scale the window directly with no source rect hint being set */
    private PictureInPictureSurfaceTransaction onAnimationScale(
            float progress, SurfaceControl.Transaction tx, Rect bounds) {
        if (mFromRotation == Surface.ROTATION_90 || mFromRotation == Surface.ROTATION_270) {
            final RotatedPosition rotatedPosition = getRotatedPosition(progress);
            return mSurfaceTransactionHelper.scale(tx, mLeash, mAppBounds, bounds,
                    rotatedPosition.degree, rotatedPosition.positionX, rotatedPosition.positionY);
        } else {
            return mSurfaceTransactionHelper.scale(tx, mLeash, mAppBounds, bounds);
        }
    }

    /** scale and crop the window with source rect hint */
    private PictureInPictureSurfaceTransaction onAnimationScaleAndCrop(
            float progress, SurfaceControl.Transaction tx,
            Rect bounds) {
        final Rect insets = mInsetsEvaluator.evaluate(progress, mSourceInsets,
                mSourceHintRectInsets);
        if (mFromRotation == Surface.ROTATION_90 || mFromRotation == Surface.ROTATION_270) {
            final RotatedPosition rotatedPosition = getRotatedPosition(progress);
            return mSurfaceTransactionHelper.scaleAndRotate(tx, mLeash, mAppBounds, bounds, insets,
                    rotatedPosition.degree, rotatedPosition.positionX, rotatedPosition.positionY);
        } else {
            return mSurfaceTransactionHelper.scaleAndCrop(tx, mLeash, mSourceRectHint, mAppBounds,
                    bounds, insets, progress);
        }
    }

    public int getTaskId() {
        return mTaskId;
    }

    public ComponentName getComponentName() {
        return mActivityInfo.getComponentName();
    }

    public Rect getDestinationBounds() {
        return mDestinationBounds;
    }

    @Nullable
    public SurfaceControl getContentOverlay() {
        return mPipContentOverlay == null ? null : mPipContentOverlay.getLeash();
    }

    /** @return {@link PictureInPictureSurfaceTransaction} for the final leash transaction. */
    public PictureInPictureSurfaceTransaction getFinishTransaction() {
        // get the final leash operations but do not apply to the leash.
        final SurfaceControl.Transaction tx =
                PipSurfaceTransactionHelper.newSurfaceControlTransaction();
        final PictureInPictureSurfaceTransaction pipTx =
                onAnimationUpdate(tx, new RectF(mDestinationBounds), END_PROGRESS);
        pipTx.setShouldDisableCanAffectSystemUiFlags(true);
        return pipTx;
    }

    private RotatedPosition getRotatedPosition(float progress) {
        final float degree, positionX, positionY;
        if (TaskAnimationManager.SHELL_TRANSITIONS_ROTATION) {
            if (mFromRotation == Surface.ROTATION_90) {
                degree = -90 * (1 - progress);
                positionX = progress * (mDestinationBoundsTransformed.left - mStartBounds.left)
                        + mStartBounds.left;
                positionY = progress * (mDestinationBoundsTransformed.top - mStartBounds.top)
                        + mStartBounds.top + mStartBounds.bottom * (1 - progress);
            } else {
                degree = 90 * (1 - progress);
                positionX = progress * (mDestinationBoundsTransformed.left - mStartBounds.left)
                        + mStartBounds.left + mStartBounds.right * (1 - progress);
                positionY = progress * (mDestinationBoundsTransformed.top - mStartBounds.top)
                        + mStartBounds.top;
            }
        } else {
            if (mFromRotation == Surface.ROTATION_90) {
                degree = -90 * progress;
                positionX = progress * (mDestinationBoundsTransformed.left - mStartBounds.left)
                        + mStartBounds.left;
                positionY = progress * (mDestinationBoundsTransformed.bottom - mStartBounds.top)
                        + mStartBounds.top;
            } else {
                degree = 90 * progress;
                positionX = progress * (mDestinationBoundsTransformed.right - mStartBounds.left)
                        + mStartBounds.left;
                positionY = progress * (mDestinationBoundsTransformed.top - mStartBounds.top)
                        + mStartBounds.top;
            }
        }

        return new RotatedPosition(degree, positionX, positionY);
    }

    /** Builder class for {@link SwipePipToHomeAnimator} */
    public static class Builder {
        private Context mContext;
        private int mTaskId;
        private ActivityInfo mActivityInfo;
        private int mAppIconSizePx;
        private SurfaceControl mLeash;
        private Rect mSourceRectHint;
        private Rect mDisplayCutoutInsets;
        private Rect mAppBounds;
        private Matrix mHomeToWindowPositionMap;
        private RectF mStartBounds;
        private Rect mDestinationBounds;
        private int mCornerRadius;
        private int mShadowRadius;
        private View mAttachedView;
        private @RecentsOrientedState.SurfaceRotation int mFromRotation = Surface.ROTATION_0;
        private final Rect mDestinationBoundsTransformed = new Rect();

        public Builder setContext(Context context) {
            mContext = context;
            return this;
        }

        public Builder setTaskId(int taskId) {
            mTaskId = taskId;
            return this;
        }

        public Builder setActivityInfo(ActivityInfo activityInfo) {
            mActivityInfo = activityInfo;
            return this;
        }

        public Builder setAppIconSizePx(int appIconSizePx) {
            mAppIconSizePx = appIconSizePx;
            return this;
        }

        public Builder setLeash(SurfaceControl leash) {
            mLeash = leash;
            return this;
        }

        public Builder setSourceRectHint(Rect sourceRectHint) {
            mSourceRectHint = new Rect(sourceRectHint);
            return this;
        }

        public Builder setAppBounds(Rect appBounds) {
            mAppBounds = new Rect(appBounds);
            return this;
        }

        public Builder setHomeToWindowPositionMap(Matrix homeToWindowPositionMap) {
            mHomeToWindowPositionMap = new Matrix(homeToWindowPositionMap);
            return this;
        }

        public Builder setStartBounds(RectF startBounds) {
            mStartBounds = new RectF(startBounds);
            return this;
        }

        public Builder setDestinationBounds(Rect destinationBounds) {
            mDestinationBounds = new Rect(destinationBounds);
            return this;
        }

        public Builder setCornerRadius(int cornerRadius) {
            mCornerRadius = cornerRadius;
            return this;
        }

        public Builder setShadowRadius(int shadowRadius) {
            mShadowRadius = shadowRadius;
            return this;
        }

        public Builder setAttachedView(View attachedView) {
            mAttachedView = attachedView;
            return this;
        }

        public Builder setFromRotation(TaskViewSimulator taskViewSimulator,
                @RecentsOrientedState.SurfaceRotation int fromRotation,
                Rect displayCutoutInsets) {
            if (fromRotation != Surface.ROTATION_90 && fromRotation != Surface.ROTATION_270) {
                Log.wtf(TAG, "Not a supported rotation, rotation=" + fromRotation);
                return this;
            }
            final Matrix matrix = new Matrix();
            taskViewSimulator.applyWindowToHomeRotation(matrix);

            // map the destination bounds into window space. mDestinationBounds is always calculated
            // in the final home space and the animation runs in original window space.
            final RectF transformed = new RectF(mDestinationBounds);
            matrix.mapRect(transformed, new RectF(mDestinationBounds));
            transformed.round(mDestinationBoundsTransformed);

            mFromRotation = fromRotation;
            if (displayCutoutInsets != null) {
                mDisplayCutoutInsets = new Rect(displayCutoutInsets);
            }
            return this;
        }

        public SwipePipToHomeAnimator build() {
            if (mDestinationBoundsTransformed.isEmpty()) {
                mDestinationBoundsTransformed.set(mDestinationBounds);
            }
            // adjust the mSourceRectHint / mAppBounds by display cutout if applicable.
            if (mSourceRectHint != null && mDisplayCutoutInsets != null) {
                if (mFromRotation == Surface.ROTATION_90) {
                    mSourceRectHint.offset(mDisplayCutoutInsets.left, mDisplayCutoutInsets.top);
                } else if (mFromRotation == Surface.ROTATION_270) {
                    mAppBounds.inset(mDisplayCutoutInsets);
                }
            }
            return new SwipePipToHomeAnimator(mContext, mTaskId, mActivityInfo, mAppIconSizePx,
                    mLeash, mSourceRectHint, mAppBounds,
                    mHomeToWindowPositionMap, mStartBounds, mDestinationBounds,
                    mFromRotation, mDestinationBoundsTransformed,
                    mCornerRadius, mShadowRadius, mAttachedView);
        }
    }

    private static class RotatedPosition {
        private final float degree;
        private final float positionX;
        private final float positionY;

        private RotatedPosition(float degree, float positionX, float positionY) {
            this.degree = degree;
            this.positionX = positionX;
            this.positionY = positionY;
        }
    }
}