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  */
16 package com.android.quickstep.util;
17 
18 import static com.android.launcher3.config.FeatureFlags.ENABLE_QUICKSTEP_LIVE_TILE;
19 import static com.android.systemui.shared.system.QuickStepContract.getWindowCornerRadius;
20 import static com.android.systemui.shared.system.QuickStepContract.supportsRoundedCornersOnWindows;
21 import static com.android.systemui.shared.system.RemoteAnimationTargetCompat.MODE_CLOSING;
22 import static com.android.systemui.shared.system.RemoteAnimationTargetCompat.MODE_OPENING;
23 
24 import android.annotation.TargetApi;
25 import android.content.Context;
26 import android.graphics.Matrix;
27 import android.graphics.Matrix.ScaleToFit;
28 import android.graphics.Rect;
29 import android.graphics.RectF;
30 import android.os.Build;
31 import android.os.RemoteException;
32 
33 import androidx.annotation.Nullable;
34 
35 import com.android.launcher3.BaseDraggingActivity;
36 import com.android.launcher3.DeviceProfile;
37 import com.android.launcher3.LauncherState;
38 import com.android.launcher3.R;
39 import com.android.launcher3.Utilities;
40 import com.android.launcher3.anim.Interpolators;
41 import com.android.launcher3.views.BaseDragLayer;
42 import com.android.quickstep.RecentsModel;
43 import com.android.quickstep.views.RecentsView;
44 import com.android.quickstep.views.TaskThumbnailView;
45 import com.android.quickstep.views.TaskView;
46 import com.android.systemui.shared.recents.ISystemUiProxy;
47 import com.android.systemui.shared.recents.utilities.RectFEvaluator;
48 import com.android.systemui.shared.system.RemoteAnimationTargetCompat;
49 import com.android.systemui.shared.system.SyncRtSurfaceTransactionApplierCompat;
50 import com.android.systemui.shared.system.SyncRtSurfaceTransactionApplierCompat.SurfaceParams;
51 import com.android.systemui.shared.system.TransactionCompat;
52 import com.android.systemui.shared.system.WindowManagerWrapper;
53 
54 /**
55  * Utility class to handle window clip animation
56  */
57 @TargetApi(Build.VERSION_CODES.P)
58 public class ClipAnimationHelper {
59 
60     // The bounds of the source app in device coordinates
61     private final Rect mSourceStackBounds = new Rect();
62     // The insets of the source app
63     private final Rect mSourceInsets = new Rect();
64     // The source app bounds with the source insets applied, in the source app window coordinates
65     private final RectF mSourceRect = new RectF();
66     // The bounds of the task view in launcher window coordinates
67     private final RectF mTargetRect = new RectF();
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     // The insets to be used for clipping the app window. For live tile, we don't transform the clip
73     // relative to the target rect.
74     private final RectF mSourceWindowClipInsetsForLiveTile = new RectF();
75 
76     // The bounds of launcher (not including insets) in device coordinates
77     public final Rect mHomeStackBounds = new Rect();
78 
79     // The clip rect in source app window coordinates
80     private final RectF mClipRectF = new RectF();
81     private final RectFEvaluator mRectFEvaluator = new RectFEvaluator();
82     private final Matrix mTmpMatrix = new Matrix();
83     private final Rect mTmpRect = new Rect();
84     private final RectF mTmpRectF = new RectF();
85     private final RectF mCurrentRectWithInsets = new RectF();
86     // Corner radius of windows, in pixels
87     private final float mWindowCornerRadius;
88     // Corner radius of windows when they're in overview mode.
89     private final float mTaskCornerRadius;
90     // If windows can have real time rounded corners.
91     private final boolean mSupportsRoundedCornersOnWindows;
92     // Whether or not to actually use the rounded cornders on windows
93     private boolean mUseRoundedCornersOnWindows;
94 
95     // Corner radius currently applied to transformed window.
96     private float mCurrentCornerRadius;
97 
98     // Whether to boost the opening animation target layers, or the closing
99     private int mBoostModeTargetLayers = -1;
100 
101     private TargetAlphaProvider mTaskAlphaCallback = (t, a) -> a;
102     private TargetAlphaProvider mBaseAlphaCallback = (t, a) -> 1;
103 
ClipAnimationHelper(Context context)104     public ClipAnimationHelper(Context context) {
105         mWindowCornerRadius = getWindowCornerRadius(context.getResources());
106         mSupportsRoundedCornersOnWindows = supportsRoundedCornersOnWindows(context.getResources());
107         mTaskCornerRadius = TaskCornerRadius.get(context);
108         mUseRoundedCornersOnWindows = mSupportsRoundedCornersOnWindows;
109     }
110 
updateSourceStack(RemoteAnimationTargetCompat target)111     private void updateSourceStack(RemoteAnimationTargetCompat target) {
112         mSourceInsets.set(target.contentInsets);
113         mSourceStackBounds.set(target.sourceContainerBounds);
114 
115         // TODO: Should sourceContainerBounds already have this offset?
116         mSourceStackBounds.offsetTo(target.position.x, target.position.y);
117 
118     }
119 
updateSource(Rect homeStackBounds, RemoteAnimationTargetCompat target)120     public void updateSource(Rect homeStackBounds, RemoteAnimationTargetCompat target) {
121         updateSourceStack(target);
122         updateHomeBounds(homeStackBounds);
123     }
124 
updateHomeBounds(Rect homeStackBounds)125     public void updateHomeBounds(Rect homeStackBounds) {
126         mHomeStackBounds.set(homeStackBounds);
127     }
128 
updateTargetRect(Rect targetRect)129     public void updateTargetRect(Rect targetRect) {
130         mSourceRect.set(mSourceInsets.left, mSourceInsets.top,
131                 mSourceStackBounds.width() - mSourceInsets.right,
132                 mSourceStackBounds.height() - mSourceInsets.bottom);
133         mTargetRect.set(targetRect);
134         mTargetRect.offset(mHomeStackBounds.left - mSourceStackBounds.left,
135                 mHomeStackBounds.top - mSourceStackBounds.top);
136 
137         // Calculate the clip based on the target rect (since the content insets and the
138         // launcher insets may differ, so the aspect ratio of the target rect can differ
139         // from the source rect. The difference between the target rect (scaled to the
140         // source rect) is the amount to clip on each edge.
141         RectF scaledTargetRect = new RectF(mTargetRect);
142         Utilities.scaleRectFAboutCenter(scaledTargetRect,
143                 mSourceRect.width() / mTargetRect.width());
144         scaledTargetRect.offsetTo(mSourceRect.left, mSourceRect.top);
145         mSourceWindowClipInsets.set(
146                 Math.max(scaledTargetRect.left, 0),
147                 Math.max(scaledTargetRect.top, 0),
148                 Math.max(mSourceStackBounds.width() - scaledTargetRect.right, 0),
149                 Math.max(mSourceStackBounds.height() - scaledTargetRect.bottom, 0));
150         mSourceWindowClipInsetsForLiveTile.set(mSourceWindowClipInsets);
151         mSourceRect.set(scaledTargetRect);
152     }
153 
prepareAnimation(DeviceProfile dp, boolean isOpening)154     public void prepareAnimation(DeviceProfile dp, boolean isOpening) {
155         mBoostModeTargetLayers = isOpening ? MODE_OPENING : MODE_CLOSING;
156         mUseRoundedCornersOnWindows = mSupportsRoundedCornersOnWindows && !dp.isMultiWindowMode;
157     }
158 
applyTransform(RemoteAnimationTargetSet targetSet, TransformParams params)159     public RectF applyTransform(RemoteAnimationTargetSet targetSet, TransformParams params) {
160         return applyTransform(targetSet, params, true /* launcherOnTop */);
161     }
162 
applyTransform(RemoteAnimationTargetSet targetSet, TransformParams params, boolean launcherOnTop)163     public RectF applyTransform(RemoteAnimationTargetSet targetSet, TransformParams params,
164             boolean launcherOnTop) {
165         float progress = params.progress;
166         if (params.currentRect == null) {
167             RectF currentRect;
168             mTmpRectF.set(mTargetRect);
169             Utilities.scaleRectFAboutCenter(mTmpRectF, params.offsetScale);
170             currentRect = mRectFEvaluator.evaluate(progress, mSourceRect, mTmpRectF);
171             currentRect.offset(params.offsetX, 0);
172 
173             // Don't clip past progress > 1.
174             progress = Math.min(1, progress);
175             final RectF sourceWindowClipInsets = params.forLiveTile
176                     ? mSourceWindowClipInsetsForLiveTile : mSourceWindowClipInsets;
177             mClipRectF.left = sourceWindowClipInsets.left * progress;
178             mClipRectF.top = sourceWindowClipInsets.top * progress;
179             mClipRectF.right =
180                     mSourceStackBounds.width() - (sourceWindowClipInsets.right * progress);
181             mClipRectF.bottom =
182                     mSourceStackBounds.height() - (sourceWindowClipInsets.bottom * progress);
183             params.setCurrentRectAndTargetAlpha(currentRect, 1);
184         }
185 
186         SurfaceParams[] surfaceParams = new SurfaceParams[targetSet.unfilteredApps.length];
187         for (int i = 0; i < targetSet.unfilteredApps.length; i++) {
188             RemoteAnimationTargetCompat app = targetSet.unfilteredApps[i];
189             mTmpMatrix.setTranslate(app.position.x, app.position.y);
190             Rect crop = mTmpRect;
191             crop.set(app.sourceContainerBounds);
192             crop.offsetTo(0, 0);
193             float alpha;
194             int layer = RemoteAnimationProvider.getLayer(app, mBoostModeTargetLayers);
195             float cornerRadius = 0f;
196             float scale = Math.max(params.currentRect.width(), mTargetRect.width()) / crop.width();
197             if (app.mode == targetSet.targetMode) {
198                 alpha = mTaskAlphaCallback.getAlpha(app, params.targetAlpha);
199                 if (app.activityType != RemoteAnimationTargetCompat.ACTIVITY_TYPE_HOME) {
200                     mTmpMatrix.setRectToRect(mSourceRect, params.currentRect, ScaleToFit.FILL);
201                     mTmpMatrix.postTranslate(app.position.x, app.position.y);
202                     mClipRectF.roundOut(crop);
203                     if (mSupportsRoundedCornersOnWindows) {
204                         if (params.cornerRadius > -1) {
205                             cornerRadius = params.cornerRadius;
206                             scale = params.currentRect.width() / crop.width();
207                         } else {
208                             float windowCornerRadius = mUseRoundedCornersOnWindows
209                                     ? mWindowCornerRadius : 0;
210                             cornerRadius = Utilities.mapRange(progress, windowCornerRadius,
211                                     mTaskCornerRadius);
212                         }
213                         mCurrentCornerRadius = cornerRadius;
214                     }
215                     // Fade out Assistant overlay.
216                     if (app.activityType == RemoteAnimationTargetCompat.ACTIVITY_TYPE_ASSISTANT
217                             && app.isNotInRecents) {
218                         alpha = 1 - Interpolators.DEACCEL_2_5.getInterpolation(progress);
219                     }
220                 } else if (targetSet.hasRecents) {
221                     // If home has a different target then recents, reverse anim the
222                     // home target.
223                     alpha = 1 - (progress * params.targetAlpha);
224                 }
225             } else {
226                 alpha = mBaseAlphaCallback.getAlpha(app, progress);
227                 if (ENABLE_QUICKSTEP_LIVE_TILE.get() && launcherOnTop) {
228                     crop = null;
229                     layer = Integer.MAX_VALUE;
230                 }
231             }
232 
233             // Since radius is in Surface space, but we draw the rounded corners in screen space, we
234             // have to undo the scale.
235             surfaceParams[i] = new SurfaceParams(app.leash, alpha, mTmpMatrix, crop, layer,
236                     cornerRadius / scale);
237         }
238         applySurfaceParams(params.syncTransactionApplier, surfaceParams);
239         return params.currentRect;
240     }
241 
getCurrentRectWithInsets()242     public RectF getCurrentRectWithInsets() {
243         mTmpMatrix.mapRect(mCurrentRectWithInsets, mClipRectF);
244         return mCurrentRectWithInsets;
245     }
246 
applySurfaceParams(@ullable SyncRtSurfaceTransactionApplierCompat syncTransactionApplier, SurfaceParams[] params)247     private void applySurfaceParams(@Nullable SyncRtSurfaceTransactionApplierCompat
248             syncTransactionApplier, SurfaceParams[] params) {
249         if (syncTransactionApplier != null) {
250             syncTransactionApplier.scheduleApply(params);
251         } else {
252             TransactionCompat t = new TransactionCompat();
253             for (SurfaceParams param : params) {
254                 SyncRtSurfaceTransactionApplierCompat.applyParams(t, param);
255             }
256             t.setEarlyWakeup();
257             t.apply();
258         }
259     }
260 
setTaskAlphaCallback(TargetAlphaProvider callback)261     public void setTaskAlphaCallback(TargetAlphaProvider callback) {
262         mTaskAlphaCallback = callback;
263     }
264 
setBaseAlphaCallback(TargetAlphaProvider callback)265     public void setBaseAlphaCallback(TargetAlphaProvider callback) {
266         mBaseAlphaCallback = callback;
267     }
268 
fromTaskThumbnailView(TaskThumbnailView ttv, RecentsView rv)269     public void fromTaskThumbnailView(TaskThumbnailView ttv, RecentsView rv) {
270         fromTaskThumbnailView(ttv, rv, null);
271     }
272 
fromTaskThumbnailView(TaskThumbnailView ttv, RecentsView rv, @Nullable RemoteAnimationTargetCompat target)273     public void fromTaskThumbnailView(TaskThumbnailView ttv, RecentsView rv,
274             @Nullable RemoteAnimationTargetCompat target) {
275         BaseDraggingActivity activity = BaseDraggingActivity.fromContext(ttv.getContext());
276         BaseDragLayer dl = activity.getDragLayer();
277 
278         int[] pos = new int[2];
279         dl.getLocationOnScreen(pos);
280         mHomeStackBounds.set(0, 0, dl.getWidth(), dl.getHeight());
281         mHomeStackBounds.offset(pos[0], pos[1]);
282 
283         if (target != null) {
284             updateSourceStack(target);
285         } else  if (rv.shouldUseMultiWindowTaskSizeStrategy()) {
286             updateStackBoundsToMultiWindowTaskSize(activity);
287         } else {
288             mSourceStackBounds.set(mHomeStackBounds);
289             Rect fallback = dl.getInsets();
290             mSourceInsets.set(ttv.getInsets(fallback));
291         }
292 
293         Rect targetRect = new Rect();
294         dl.getDescendantRectRelativeToSelf(ttv, targetRect);
295         updateTargetRect(targetRect);
296 
297         if (target == null) {
298             // Transform the clip relative to the target rect. Only do this in the case where we
299             // aren't applying the insets to the app windows (where the clip should be in target app
300             // space)
301             float scale = mTargetRect.width() / mSourceRect.width();
302             mSourceWindowClipInsets.left = mSourceWindowClipInsets.left * scale;
303             mSourceWindowClipInsets.top = mSourceWindowClipInsets.top * scale;
304             mSourceWindowClipInsets.right = mSourceWindowClipInsets.right * scale;
305             mSourceWindowClipInsets.bottom = mSourceWindowClipInsets.bottom * scale;
306         }
307     }
308 
309     /**
310      * Compute scale and translation y such that the specified task view fills the screen.
311      */
updateForFullscreenOverview(TaskView v)312     public ClipAnimationHelper updateForFullscreenOverview(TaskView v) {
313         TaskThumbnailView thumbnailView = v.getThumbnail();
314         RecentsView recentsView = v.getRecentsView();
315         fromTaskThumbnailView(thumbnailView, recentsView);
316         Rect taskSize = new Rect();
317         recentsView.getTaskSize(taskSize);
318         updateTargetRect(taskSize);
319         return this;
320     }
321 
322     /**
323      * @return The source rect's scale and translation relative to the target rect.
324      */
getScaleAndTranslation()325     public LauncherState.ScaleAndTranslation getScaleAndTranslation() {
326         float scale = mSourceRect.width() / mTargetRect.width();
327         float translationY = mSourceRect.centerY() - mSourceRect.top - mTargetRect.centerY();
328         return new LauncherState.ScaleAndTranslation(scale, 0, translationY);
329     }
330 
updateStackBoundsToMultiWindowTaskSize(BaseDraggingActivity activity)331     private void updateStackBoundsToMultiWindowTaskSize(BaseDraggingActivity activity) {
332         ISystemUiProxy sysUiProxy = RecentsModel.INSTANCE.get(activity).getSystemUiProxy();
333         if (sysUiProxy != null) {
334             try {
335                 mSourceStackBounds.set(sysUiProxy.getNonMinimizedSplitScreenSecondaryBounds());
336                 return;
337             } catch (RemoteException e) {
338                 // Use half screen size
339             }
340         }
341 
342         // Assume that the task size is half screen size (minus the insets and the divider size)
343         DeviceProfile fullDp = activity.getDeviceProfile().getFullScreenProfile();
344         // Use availableWidthPx and availableHeightPx instead of widthPx and heightPx to
345         // account for system insets
346         int taskWidth = fullDp.availableWidthPx;
347         int taskHeight = fullDp.availableHeightPx;
348         int halfDividerSize = activity.getResources()
349                 .getDimensionPixelSize(R.dimen.multi_window_task_divider_size) / 2;
350 
351         Rect insets = new Rect();
352         WindowManagerWrapper.getInstance().getStableInsets(insets);
353         if (fullDp.isLandscape) {
354             taskWidth = taskWidth / 2 - halfDividerSize;
355         } else {
356             taskHeight = taskHeight / 2 - halfDividerSize;
357         }
358 
359         // Align the task to bottom left/right edge (closer to nav bar).
360         int left = activity.getDeviceProfile().isSeascape() ? insets.left
361                 : (insets.left + fullDp.availableWidthPx - taskWidth);
362         mSourceStackBounds.set(0, 0, taskWidth, taskHeight);
363         mSourceStackBounds.offset(left, insets.top + fullDp.availableHeightPx - taskHeight);
364     }
365 
getTargetRect()366     public RectF getTargetRect() {
367         return mTargetRect;
368     }
369 
getCurrentCornerRadius()370     public float getCurrentCornerRadius() {
371         return mCurrentCornerRadius;
372     }
373 
374     public interface TargetAlphaProvider {
getAlpha(RemoteAnimationTargetCompat target, float expectedAlpha)375         float getAlpha(RemoteAnimationTargetCompat target, float expectedAlpha);
376     }
377 
378     public static class TransformParams {
379         float progress;
380         public float offsetX;
381         public float offsetScale;
382         @Nullable RectF currentRect;
383         float targetAlpha;
384         boolean forLiveTile;
385         float cornerRadius;
386 
387         SyncRtSurfaceTransactionApplierCompat syncTransactionApplier;
388 
TransformParams()389         public TransformParams() {
390             progress = 0;
391             offsetX = 0;
392             offsetScale = 1;
393             currentRect = null;
394             targetAlpha = 0;
395             forLiveTile = false;
396             cornerRadius = -1;
397         }
398 
setProgress(float progress)399         public TransformParams setProgress(float progress) {
400             this.progress = progress;
401             this.currentRect = null;
402             return this;
403         }
404 
getProgress()405         public float getProgress() {
406             return progress;
407         }
408 
setCornerRadius(float cornerRadius)409         public TransformParams setCornerRadius(float cornerRadius) {
410             this.cornerRadius = cornerRadius;
411             return this;
412         }
413 
setCurrentRectAndTargetAlpha(RectF currentRect, float targetAlpha)414         public TransformParams setCurrentRectAndTargetAlpha(RectF currentRect, float targetAlpha) {
415             this.currentRect = currentRect;
416             this.targetAlpha = targetAlpha;
417             return this;
418         }
419 
setOffsetX(float offsetX)420         public TransformParams setOffsetX(float offsetX) {
421             this.offsetX = offsetX;
422             return this;
423         }
424 
setOffsetScale(float offsetScale)425         public TransformParams setOffsetScale(float offsetScale) {
426             this.offsetScale = offsetScale;
427             return this;
428         }
429 
setForLiveTile(boolean forLiveTile)430         public TransformParams setForLiveTile(boolean forLiveTile) {
431             this.forLiveTile = forLiveTile;
432             return this;
433         }
434 
setSyncTransactionApplier( SyncRtSurfaceTransactionApplierCompat applier)435         public TransformParams setSyncTransactionApplier(
436                 SyncRtSurfaceTransactionApplierCompat applier) {
437             this.syncTransactionApplier = applier;
438             return this;
439         }
440     }
441 }
442 
443