1/*
2 * Copyright (C) 2018 The Android Open Source Project
3 *
4 * Licensed under the Apache License, Version 2.0 (the "License");
5 * you may not use this file except in compliance with the License.
6 * You may obtain a copy of the License at
7 *
8 *      http://www.apache.org/licenses/LICENSE-2.0
9 *
10 * Unless required by applicable law or agreed to in writing, software
11 * distributed under the License is distributed on an "AS IS" BASIS,
12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 * See the License for the specific language governing permissions and
14 * limitations under the License.
15 */
16package com.android.quickstep.util;
17
18import static com.android.launcher3.anim.Interpolators.LINEAR;
19import static com.android.quickstep.QuickScrubController.QUICK_SCRUB_TRANSLATION_Y_FACTOR;
20import static com.android.systemui.shared.system.RemoteAnimationTargetCompat.MODE_CLOSING;
21import static com.android.systemui.shared.system.RemoteAnimationTargetCompat.MODE_OPENING;
22
23import android.annotation.TargetApi;
24import android.graphics.Canvas;
25import android.graphics.Matrix;
26import android.graphics.Matrix.ScaleToFit;
27import android.graphics.PointF;
28import android.graphics.Rect;
29import android.graphics.RectF;
30import android.os.Build;
31import android.os.RemoteException;
32import android.support.annotation.Nullable;
33import android.view.animation.Interpolator;
34
35import com.android.launcher3.BaseDraggingActivity;
36import com.android.launcher3.DeviceProfile;
37import com.android.launcher3.R;
38import com.android.launcher3.Utilities;
39import com.android.launcher3.anim.Interpolators;
40import com.android.launcher3.views.BaseDragLayer;
41import com.android.quickstep.RecentsModel;
42import com.android.quickstep.views.RecentsView;
43import com.android.quickstep.views.TaskThumbnailView;
44import com.android.systemui.shared.recents.ISystemUiProxy;
45import com.android.systemui.shared.recents.utilities.RectFEvaluator;
46import com.android.systemui.shared.system.RemoteAnimationTargetCompat;
47import com.android.systemui.shared.system.TransactionCompat;
48import com.android.systemui.shared.system.WindowManagerWrapper;
49
50import java.util.function.BiConsumer;
51
52/**
53 * Utility class to handle window clip animation
54 */
55@TargetApi(Build.VERSION_CODES.P)
56public class ClipAnimationHelper {
57
58    // The bounds of the source app in device coordinates
59    private final Rect mSourceStackBounds = new Rect();
60    // The insets of the source app
61    private final Rect mSourceInsets = new Rect();
62    // The source app bounds with the source insets applied, in the source app window coordinates
63    private final RectF mSourceRect = new RectF();
64    // The bounds of the task view in launcher window coordinates
65    private final RectF mTargetRect = new RectF();
66    // Set when the final window destination is changed, such as offsetting for quick scrub
67    private final PointF mTargetOffset = new PointF();
68    // The insets to be used for clipping the app window, which can be larger than mSourceInsets
69    // if the aspect ratio of the target is smaller than the aspect ratio of the source rect. In
70    // app window coordinates.
71    private final RectF mSourceWindowClipInsets = new RectF();
72
73    // The bounds of launcher (not including insets) in device coordinates
74    public final Rect mHomeStackBounds = new Rect();
75
76    // The clip rect in source app window coordinates
77    private final Rect mClipRect = new Rect();
78    private final RectFEvaluator mRectFEvaluator = new RectFEvaluator();
79    private final Matrix mTmpMatrix = new Matrix();
80    private final RectF mTmpRectF = new RectF();
81
82    private float mTargetScale = 1f;
83    private float mOffsetScale = 1f;
84    private Interpolator mInterpolator = LINEAR;
85    // We translate y slightly faster than the rest of the animation for quick scrub.
86    private Interpolator mOffsetYInterpolator = LINEAR;
87
88    // Whether to boost the opening animation target layers, or the closing
89    private int mBoostModeTargetLayers = -1;
90    // Wether or not applyTransform has been called yet since prepareAnimation()
91    private boolean mIsFirstFrame = true;
92
93    private BiConsumer<TransactionCompat, RemoteAnimationTargetCompat> mTaskTransformCallback =
94            (t, a) -> { };
95
96    private void updateSourceStack(RemoteAnimationTargetCompat target) {
97        mSourceInsets.set(target.contentInsets);
98        mSourceStackBounds.set(target.sourceContainerBounds);
99
100        // TODO: Should sourceContainerBounds already have this offset?
101        mSourceStackBounds.offsetTo(target.position.x, target.position.y);
102
103    }
104
105    public void updateSource(Rect homeStackBounds, RemoteAnimationTargetCompat target) {
106        mHomeStackBounds.set(homeStackBounds);
107        updateSourceStack(target);
108    }
109
110    public void updateTargetRect(TransformedRect targetRect) {
111        mOffsetScale = targetRect.scale;
112        mSourceRect.set(mSourceInsets.left, mSourceInsets.top,
113                mSourceStackBounds.width() - mSourceInsets.right,
114                mSourceStackBounds.height() - mSourceInsets.bottom);
115        mTargetRect.set(targetRect.rect);
116        Utilities.scaleRectFAboutCenter(mTargetRect, targetRect.scale);
117        mTargetRect.offset(mHomeStackBounds.left - mSourceStackBounds.left,
118                mHomeStackBounds.top - mSourceStackBounds.top);
119
120        // Calculate the clip based on the target rect (since the content insets and the
121        // launcher insets may differ, so the aspect ratio of the target rect can differ
122        // from the source rect. The difference between the target rect (scaled to the
123        // source rect) is the amount to clip on each edge.
124        RectF scaledTargetRect = new RectF(mTargetRect);
125        Utilities.scaleRectFAboutCenter(scaledTargetRect,
126                mSourceRect.width() / mTargetRect.width());
127        scaledTargetRect.offsetTo(mSourceRect.left, mSourceRect.top);
128        mSourceWindowClipInsets.set(
129                Math.max(scaledTargetRect.left, 0),
130                Math.max(scaledTargetRect.top, 0),
131                Math.max(mSourceStackBounds.width() - scaledTargetRect.right, 0),
132                Math.max(mSourceStackBounds.height() - scaledTargetRect.bottom, 0));
133        mSourceRect.set(scaledTargetRect);
134    }
135
136    public void prepareAnimation(boolean isOpening) {
137        mIsFirstFrame = true;
138        mBoostModeTargetLayers = isOpening ? MODE_OPENING : MODE_CLOSING;
139    }
140
141    public RectF applyTransform(RemoteAnimationTargetSet targetSet, float progress) {
142        RectF currentRect;
143        mTmpRectF.set(mTargetRect);
144        Utilities.scaleRectFAboutCenter(mTmpRectF, mTargetScale);
145        float offsetYProgress = mOffsetYInterpolator.getInterpolation(progress);
146        progress = mInterpolator.getInterpolation(progress);
147        currentRect =  mRectFEvaluator.evaluate(progress, mSourceRect, mTmpRectF);
148
149        synchronized (mTargetOffset) {
150            // Stay lined up with the center of the target, since it moves for quick scrub.
151            currentRect.offset(mTargetOffset.x * mOffsetScale * progress,
152                    mTargetOffset.y  * offsetYProgress);
153        }
154
155        mClipRect.left = (int) (mSourceWindowClipInsets.left * progress);
156        mClipRect.top = (int) (mSourceWindowClipInsets.top * progress);
157        mClipRect.right = (int)
158                (mSourceStackBounds.width() - (mSourceWindowClipInsets.right * progress));
159        mClipRect.bottom = (int)
160                (mSourceStackBounds.height() - (mSourceWindowClipInsets.bottom * progress));
161
162        TransactionCompat transaction = new TransactionCompat();
163        if (mIsFirstFrame) {
164            RemoteAnimationProvider.prepareTargetsForFirstFrame(targetSet.unfilteredApps,
165                    transaction, mBoostModeTargetLayers);
166            mIsFirstFrame = false;
167        }
168        for (RemoteAnimationTargetCompat app : targetSet.apps) {
169            if (app.activityType != RemoteAnimationTargetCompat.ACTIVITY_TYPE_HOME) {
170                mTmpMatrix.setRectToRect(mSourceRect, currentRect, ScaleToFit.FILL);
171                mTmpMatrix.postTranslate(app.position.x, app.position.y);
172                transaction.setMatrix(app.leash, mTmpMatrix)
173                        .setWindowCrop(app.leash, mClipRect);
174            }
175
176            if (app.isNotInRecents
177                    || app.activityType == RemoteAnimationTargetCompat.ACTIVITY_TYPE_HOME) {
178                transaction.setAlpha(app.leash, 1 - progress);
179            }
180
181            mTaskTransformCallback.accept(transaction, app);
182        }
183        transaction.setEarlyWakeup();
184        transaction.apply();
185        return currentRect;
186    }
187
188    public void setTaskTransformCallback
189            (BiConsumer<TransactionCompat, RemoteAnimationTargetCompat> callback) {
190        mTaskTransformCallback = callback;
191    }
192
193    public void offsetTarget(float scale, float offsetX, float offsetY, Interpolator interpolator) {
194        synchronized (mTargetOffset) {
195            mTargetOffset.set(offsetX, offsetY);
196        }
197        mTargetScale = scale;
198        mInterpolator = interpolator;
199        mOffsetYInterpolator = Interpolators.clampToProgress(mInterpolator, 0,
200                QUICK_SCRUB_TRANSLATION_Y_FACTOR);
201    }
202
203    public void fromTaskThumbnailView(TaskThumbnailView ttv, RecentsView rv) {
204        fromTaskThumbnailView(ttv, rv, null);
205    }
206
207    public void fromTaskThumbnailView(TaskThumbnailView ttv, RecentsView rv,
208            @Nullable RemoteAnimationTargetCompat target) {
209        BaseDraggingActivity activity = BaseDraggingActivity.fromContext(ttv.getContext());
210        BaseDragLayer dl = activity.getDragLayer();
211
212        int[] pos = new int[2];
213        dl.getLocationOnScreen(pos);
214        mHomeStackBounds.set(0, 0, dl.getWidth(), dl.getHeight());
215        mHomeStackBounds.offset(pos[0], pos[1]);
216
217        if (target != null) {
218            updateSourceStack(target);
219        } else  if (rv.shouldUseMultiWindowTaskSizeStrategy()) {
220            updateStackBoundsToMultiWindowTaskSize(activity);
221        } else {
222            mSourceStackBounds.set(mHomeStackBounds);
223            mSourceInsets.set(activity.getDeviceProfile().getInsets());
224        }
225
226        TransformedRect targetRect = new TransformedRect();
227        dl.getDescendantRectRelativeToSelf(ttv, targetRect.rect);
228        updateTargetRect(targetRect);
229
230        if (target == null) {
231            // Transform the clip relative to the target rect. Only do this in the case where we
232            // aren't applying the insets to the app windows (where the clip should be in target app
233            // space)
234            float scale = mTargetRect.width() / mSourceRect.width();
235            mSourceWindowClipInsets.left = mSourceWindowClipInsets.left * scale;
236            mSourceWindowClipInsets.top = mSourceWindowClipInsets.top * scale;
237            mSourceWindowClipInsets.right = mSourceWindowClipInsets.right * scale;
238            mSourceWindowClipInsets.bottom = mSourceWindowClipInsets.bottom * scale;
239        }
240    }
241
242    private void updateStackBoundsToMultiWindowTaskSize(BaseDraggingActivity activity) {
243        ISystemUiProxy sysUiProxy = RecentsModel.getInstance(activity).getSystemUiProxy();
244        if (sysUiProxy != null) {
245            try {
246                mSourceStackBounds.set(sysUiProxy.getNonMinimizedSplitScreenSecondaryBounds());
247                return;
248            } catch (RemoteException e) {
249                // Use half screen size
250            }
251        }
252
253        // Assume that the task size is half screen size (minus the insets and the divider size)
254        DeviceProfile fullDp = activity.getDeviceProfile().getFullScreenProfile();
255        // Use availableWidthPx and availableHeightPx instead of widthPx and heightPx to
256        // account for system insets
257        int taskWidth = fullDp.availableWidthPx;
258        int taskHeight = fullDp.availableHeightPx;
259        int halfDividerSize = activity.getResources()
260                .getDimensionPixelSize(R.dimen.multi_window_task_divider_size) / 2;
261
262        Rect insets = new Rect();
263        WindowManagerWrapper.getInstance().getStableInsets(insets);
264        if (fullDp.isLandscape) {
265            taskWidth = taskWidth / 2 - halfDividerSize;
266        } else {
267            taskHeight = taskHeight / 2 - halfDividerSize;
268        }
269
270        // Align the task to bottom left/right edge (closer to nav bar).
271        int left = activity.getDeviceProfile().isSeascape() ? insets.left
272                : (insets.left + fullDp.availableWidthPx - taskWidth);
273        mSourceStackBounds.set(0, 0, taskWidth, taskHeight);
274        mSourceStackBounds.offset(left, insets.top + fullDp.availableHeightPx - taskHeight);
275    }
276
277    public void drawForProgress(TaskThumbnailView ttv, Canvas canvas, float progress) {
278        RectF currentRect =  mRectFEvaluator.evaluate(progress, mSourceRect, mTargetRect);
279        canvas.translate(mSourceStackBounds.left - mHomeStackBounds.left,
280                mSourceStackBounds.top - mHomeStackBounds.top);
281        mTmpMatrix.setRectToRect(mTargetRect, currentRect, ScaleToFit.FILL);
282
283        canvas.concat(mTmpMatrix);
284        canvas.translate(mTargetRect.left, mTargetRect.top);
285
286        float insetProgress = (1 - progress);
287        ttv.drawOnCanvas(canvas,
288                -mSourceWindowClipInsets.left * insetProgress,
289                -mSourceWindowClipInsets.top * insetProgress,
290                ttv.getMeasuredWidth() + mSourceWindowClipInsets.right * insetProgress,
291                ttv.getMeasuredHeight() + mSourceWindowClipInsets.bottom * insetProgress,
292                ttv.getCornerRadius() * progress);
293    }
294
295    public RectF getTargetRect() {
296        return mTargetRect;
297    }
298
299    public RectF getSourceRect() {
300        return mSourceRect;
301    }
302}
303