1 /*
2  * Copyright (C) 2023 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.views;
17 
18 import static com.android.app.animation.Interpolators.LINEAR;
19 import static com.android.app.animation.Interpolators.clampToProgress;
20 import static com.android.launcher3.AbstractFloatingView.TYPE_TASK_MENU;
21 
22 import android.animation.ValueAnimator;
23 import android.content.Context;
24 import android.graphics.Bitmap;
25 import android.graphics.Canvas;
26 import android.graphics.Paint;
27 import android.graphics.Rect;
28 import android.graphics.RectF;
29 import android.graphics.drawable.Drawable;
30 import android.util.AttributeSet;
31 import android.util.FloatProperty;
32 import android.view.View;
33 import android.view.ViewGroup;
34 import android.widget.FrameLayout;
35 
36 import androidx.annotation.Nullable;
37 
38 import com.android.launcher3.AbstractFloatingView;
39 import com.android.launcher3.InsettableFrameLayout;
40 import com.android.launcher3.R;
41 import com.android.launcher3.Utilities;
42 import com.android.launcher3.anim.PendingAnimation;
43 import com.android.launcher3.taskbar.TaskbarActivityContext;
44 import com.android.launcher3.util.SplitConfigurationOptions;
45 import com.android.launcher3.views.BaseDragLayer;
46 import com.android.quickstep.orientation.RecentsPagedOrientationHandler;
47 import com.android.quickstep.util.AnimUtils;
48 import com.android.quickstep.util.MultiValueUpdateListener;
49 import com.android.quickstep.util.SplitAnimationTimings;
50 import com.android.quickstep.util.TaskCornerRadius;
51 import com.android.systemui.shared.system.QuickStepContract;
52 
53 /**
54  * Create an instance via
55  * {@link #getFloatingTaskView(RecentsViewContainer, View, Bitmap, Drawable, RectF)} to
56  * which will have the thumbnail from the provided existing TaskView overlaying the taskview itself.
57  *
58  * Can then animate the taskview using
59  * {@link #addStagingAnimation(PendingAnimation, RectF, Rect, boolean, boolean)} or
60  * {@link #addConfirmAnimation(PendingAnimation, RectF, Rect, boolean, boolean)}
61  * giving a starting and ending bounds. Currently this is set to use the split placeholder view,
62  * but it could be generified.
63  */
64 public class FloatingTaskView extends FrameLayout {
65 
66     public static final FloatProperty<FloatingTaskView> PRIMARY_TRANSLATE_OFFSCREEN =
67             new FloatProperty<FloatingTaskView>("floatingTaskPrimaryTranslateOffscreen") {
68                 @Override
69                 public void setValue(FloatingTaskView view, float translation) {
70                     ((RecentsView) view.mContainer.getOverviewPanel()).getPagedOrientationHandler()
71                             .setFloatingTaskPrimaryTranslation(
72                                     view,
73                                     translation,
74                                     view.mContainer.getDeviceProfile()
75                             );
76                 }
77 
78                 @Override
79                 public Float get(FloatingTaskView view) {
80                         return ((RecentsView) view.mContainer.getOverviewPanel())
81                                 .getPagedOrientationHandler()
82                                 .getFloatingTaskPrimaryTranslation(
83                                         view,
84                                         view.mContainer.getDeviceProfile()
85                                 );
86                 }
87             };
88 
89     private int mSplitHolderSize;
90     private FloatingTaskThumbnailView mThumbnailView;
91     private SplitPlaceholderView mSplitPlaceholderView;
92     private RectF mStartingPosition;
93     private final RecentsViewContainer mContainer;
94     private final boolean mIsRtl;
95     private final FullscreenDrawParams mFullscreenParams;
96     private RecentsPagedOrientationHandler mOrientationHandler;
97     @SplitConfigurationOptions.StagePosition
98     private int mStagePosition;
99     private final Rect mTmpRect = new Rect();
100 
FloatingTaskView(Context context)101     public FloatingTaskView(Context context) {
102         this(context, null);
103     }
104 
FloatingTaskView(Context context, @Nullable AttributeSet attrs)105     public FloatingTaskView(Context context, @Nullable AttributeSet attrs) {
106         this(context, attrs, 0);
107     }
108 
FloatingTaskView(Context context, @Nullable AttributeSet attrs, int defStyleAttr)109     public FloatingTaskView(Context context, @Nullable AttributeSet attrs, int defStyleAttr) {
110         super(context, attrs, defStyleAttr);
111         mContainer = RecentsViewContainer.containerFromContext(context);
112         mIsRtl = Utilities.isRtl(getResources());
113         mFullscreenParams = new FullscreenDrawParams(context);
114 
115         mSplitHolderSize = context.getResources().getDimensionPixelSize(
116                 R.dimen.split_placeholder_icon_size);
117     }
118 
119     @Override
onFinishInflate()120     protected void onFinishInflate() {
121         super.onFinishInflate();
122         mThumbnailView = findViewById(R.id.thumbnail);
123         mSplitPlaceholderView = findViewById(R.id.split_placeholder);
124         mSplitPlaceholderView.setAlpha(0);
125     }
126 
init(RecentsViewContainer launcher, View originalView, @Nullable Bitmap thumbnail, Drawable icon, RectF positionOut)127     private void init(RecentsViewContainer launcher, View originalView, @Nullable Bitmap thumbnail,
128             Drawable icon, RectF positionOut) {
129         mStartingPosition = positionOut;
130         updateInitialPositionForView(originalView);
131         final InsettableFrameLayout.LayoutParams lp =
132                 (InsettableFrameLayout.LayoutParams) getLayoutParams();
133 
134         mSplitPlaceholderView.setLayoutParams(new FrameLayout.LayoutParams(lp.width, lp.height));
135         setPivotX(0);
136         setPivotY(0);
137 
138         // Copy bounds of exiting thumbnail into ImageView
139         mThumbnailView.setThumbnail(thumbnail);
140 
141         mThumbnailView.setVisibility(VISIBLE);
142 
143         RecentsView recentsView = launcher.getOverviewPanel();
144         mOrientationHandler = recentsView.getPagedOrientationHandler();
145         mStagePosition = recentsView.getSplitSelectController().getActiveSplitStagePosition();
146         mSplitPlaceholderView.setIcon(icon, mSplitHolderSize);
147         mSplitPlaceholderView.getIconView().setRotation(mOrientationHandler.getDegreesRotated());
148     }
149 
150     /**
151      * Configures and returns a an instance of {@link FloatingTaskView} initially matching the
152      * appearance of {@code originalView}.
153      */
getFloatingTaskView(RecentsViewContainer launcher, View originalView, @Nullable Bitmap thumbnail, Drawable icon, RectF positionOut)154     public static FloatingTaskView getFloatingTaskView(RecentsViewContainer launcher,
155             View originalView, @Nullable Bitmap thumbnail, Drawable icon, RectF positionOut) {
156         final ViewGroup dragLayer = launcher.getDragLayer();
157         final FloatingTaskView floatingView = (FloatingTaskView) launcher.getLayoutInflater()
158                 .inflate(R.layout.floating_split_select_view, dragLayer, false);
159 
160         floatingView.init(launcher, originalView, thumbnail, icon, positionOut);
161         // Add this animating view underneath the existing open task menu view (if there is one)
162         View openTaskView = AbstractFloatingView.getOpenView(launcher, TYPE_TASK_MENU);
163         int openTaskViewIndex = dragLayer.indexOfChild(openTaskView);
164         if (openTaskViewIndex == -1) {
165             // Add to top if not
166             openTaskViewIndex = dragLayer.getChildCount();
167         }
168         dragLayer.addView(floatingView, openTaskViewIndex);
169         return floatingView;
170     }
171 
updateInitialPositionForView(View originalView)172     public void updateInitialPositionForView(View originalView) {
173         if (originalView.getContext() instanceof TaskbarActivityContext) {
174             // If original View is a button on the Taskbar, find the on-screen bounds and calculate
175             // the equivalent bounds in the DragLayer, so we can set the initial position of
176             // this FloatingTaskView and start the split animation at the correct spot.
177             originalView.getBoundsOnScreen(mTmpRect);
178             mStartingPosition.set(mTmpRect);
179             int[] dragLayerPositionRelativeToScreen =
180                     mContainer.getDragLayer().getLocationOnScreen();
181             mStartingPosition.offset(
182                     -dragLayerPositionRelativeToScreen[0],
183                     -dragLayerPositionRelativeToScreen[1]);
184         } else {
185             Rect viewBounds = new Rect(0, 0, originalView.getWidth(), originalView.getHeight());
186             Utilities.getBoundsForViewInDragLayer(mContainer.getDragLayer(), originalView,
187                     viewBounds, false /* ignoreTransform */, null /* recycle */,
188                     mStartingPosition);
189         }
190         // In some cases originalView is off-screen so we don't get a valid starting position
191         // ex. on rotation
192         // TODO(b/345556328) We shouldn't be animating if starting position of view isn't ready
193         if (mStartingPosition.isEmpty()) {
194             // Set to non empty for now so calculations in #update() don't break
195             mStartingPosition.set(0, 0, 1, 1);
196         }
197         final BaseDragLayer.LayoutParams lp = new BaseDragLayer.LayoutParams(
198                 Math.round(mStartingPosition.width()),
199                 Math.round(mStartingPosition.height()));
200         initPosition(mStartingPosition, lp);
201         setLayoutParams(lp);
202     }
203 
update(RectF bounds, float progress)204     public void update(RectF bounds, float progress) {
205         MarginLayoutParams lp = (MarginLayoutParams) getLayoutParams();
206 
207         float dX = bounds.left - mStartingPosition.left;
208         float dY = bounds.top - lp.topMargin;
209         float scaleX = bounds.width() / lp.width;
210         float scaleY = bounds.height() / lp.height;
211 
212         mFullscreenParams.updateParams(bounds, progress, scaleX, scaleY);
213 
214         setTranslationX(dX);
215         setTranslationY(dY);
216         setScaleX(scaleX);
217         setScaleY(scaleY);
218         mSplitPlaceholderView.invalidate();
219         mThumbnailView.invalidate();
220 
221         float childScaleX = 1f / scaleX;
222         float childScaleY = 1f / scaleY;
223         mOrientationHandler.setPrimaryScale(mSplitPlaceholderView.getIconView(), childScaleX);
224         mOrientationHandler.setSecondaryScale(mSplitPlaceholderView.getIconView(), childScaleY);
225     }
226 
updateOrientationHandler(RecentsPagedOrientationHandler orientationHandler)227     public void updateOrientationHandler(RecentsPagedOrientationHandler orientationHandler) {
228         mOrientationHandler = orientationHandler;
229         mSplitPlaceholderView.getIconView().setRotation(mOrientationHandler.getDegreesRotated());
230     }
231 
setIcon(Drawable drawable)232     public void setIcon(Drawable drawable) {
233         mSplitPlaceholderView.setIcon(drawable, mSplitHolderSize);
234     }
235 
initPosition(RectF pos, InsettableFrameLayout.LayoutParams lp)236     protected void initPosition(RectF pos, InsettableFrameLayout.LayoutParams lp) {
237         mStartingPosition.set(pos);
238         lp.ignoreInsets = true;
239         // Position the floating view exactly on top of the original
240         lp.topMargin = Math.round(pos.top);
241         if (mIsRtl) {
242             lp.setMarginStart(mContainer.getDeviceProfile().widthPx - Math.round(pos.right));
243         } else {
244             lp.setMarginStart(Math.round(pos.left));
245         }
246 
247         // Set the properties here already to make sure they are available when running the first
248         // animation frame.
249         int left = (int) pos.left;
250         layout(left, lp.topMargin, left + lp.width, lp.topMargin + lp.height);
251     }
252 
253     /**
254      * Animates a FloatingTaskThumbnailView and its overlapping SplitPlaceholderView when a split
255      * is staged.
256      */
addStagingAnimation(PendingAnimation animation, RectF startingBounds, Rect endBounds, boolean fadeWithThumbnail, boolean isStagedTask)257     public void addStagingAnimation(PendingAnimation animation, RectF startingBounds,
258             Rect endBounds, boolean fadeWithThumbnail, boolean isStagedTask) {
259         boolean isTablet = mContainer.getDeviceProfile().isTablet;
260         boolean splittingFromOverview = fadeWithThumbnail;
261         SplitAnimationTimings timings;
262 
263         if (isTablet && splittingFromOverview) {
264             timings = SplitAnimationTimings.TABLET_OVERVIEW_TO_SPLIT;
265         } else if (!isTablet && splittingFromOverview) {
266             timings = SplitAnimationTimings.PHONE_OVERVIEW_TO_SPLIT;
267         } else {
268             // Splitting from Home is currently only available on tablets
269             timings = SplitAnimationTimings.TABLET_HOME_TO_SPLIT;
270         }
271 
272         addAnimation(animation, startingBounds, endBounds, fadeWithThumbnail, isStagedTask,
273                 timings);
274     }
275 
276     /**
277      * Animates the FloatingTaskThumbnailView and SplitPlaceholderView for the two thumbnails
278      * when a split is confirmed.
279      */
addConfirmAnimation(PendingAnimation animation, RectF startingBounds, Rect endBounds, boolean fadeWithThumbnail, boolean isStagedTask)280     public void addConfirmAnimation(PendingAnimation animation, RectF startingBounds,
281             Rect endBounds, boolean fadeWithThumbnail, boolean isStagedTask) {
282         SplitAnimationTimings timings =
283                 AnimUtils.getDeviceSplitToConfirmTimings(mContainer.getDeviceProfile().isTablet);
284 
285         addAnimation(animation, startingBounds, endBounds, fadeWithThumbnail, isStagedTask,
286                 timings);
287     }
288 
289     /**
290      * Sets up and builds a split staging animation.
291      * Called by {@link #addStagingAnimation(PendingAnimation, RectF, Rect, boolean, boolean)} and
292      * {@link #addConfirmAnimation(PendingAnimation, RectF, Rect, boolean, boolean)}.
293      */
addAnimation(PendingAnimation animation, RectF startingBounds, Rect endBounds, boolean fadeWithThumbnail, boolean isStagedTask, SplitAnimationTimings timings)294     public void addAnimation(PendingAnimation animation, RectF startingBounds,
295             Rect endBounds, boolean fadeWithThumbnail, boolean isStagedTask,
296             SplitAnimationTimings timings) {
297         mFullscreenParams.setIsStagedTask(isStagedTask);
298         final BaseDragLayer dragLayer = mContainer.getDragLayer();
299         int[] dragLayerBounds = new int[2];
300         dragLayer.getLocationOnScreen(dragLayerBounds);
301         SplitOverlayProperties prop = new SplitOverlayProperties(endBounds,
302                 startingBounds, dragLayerBounds[0], dragLayerBounds[1]);
303 
304         ValueAnimator transitionAnimator = ValueAnimator.ofFloat(0, 1);
305         animation.add(transitionAnimator);
306         RectF floatingTaskViewBounds = new RectF();
307 
308         if (fadeWithThumbnail) {
309             // This code block runs for the placeholder view during Overview > OverviewSplitSelect
310             // and for the selected (secondary) thumbnail during OverviewSplitSelect > Confirmed
311 
312             // FloatingTaskThumbnailView: thumbnail fades out to transparent
313             animation.setViewAlpha(mThumbnailView, 0, clampToProgress(LINEAR,
314                     timings.getPlaceholderFadeInStartOffset(),
315                     timings.getPlaceholderFadeInEndOffset()));
316 
317             // SplitPlaceholderView: gray background fades in at same time, then new icon fades in
318             fadeInSplitPlaceholder(animation, timings);
319         } else if (isStagedTask) {
320             // This code block runs for the placeholder view during Normal > OverviewSplitSelect
321             // and for the placeholder (primary) thumbnail during OverviewSplitSelect > Confirmed
322 
323             // Fade in the placeholder view during Normal > OverviewSplitSelect
324             if (mSplitPlaceholderView.getAlpha() == 0) {
325                 mSplitPlaceholderView.getIconView().setContentAlpha(0);
326                 fadeInSplitPlaceholder(animation, timings);
327             }
328 
329             // No-op for placeholder during OverviewSplitSelect > Confirmed, alpha should be set
330         }
331 
332         MultiValueUpdateListener listener = new MultiValueUpdateListener() {
333             // SplitPlaceholderView: rectangle translates and stretches to new position
334             final FloatProp mDx = new FloatProp(0, prop.dX,
335                     clampToProgress(timings.getStagedRectXInterpolator(),
336                             timings.getStagedRectSlideStartOffset(),
337                             timings.getStagedRectSlideEndOffset()));
338             final FloatProp mDy = new FloatProp(0, prop.dY,
339                     clampToProgress(timings.getStagedRectYInterpolator(),
340                             timings.getStagedRectSlideStartOffset(),
341                             timings.getStagedRectSlideEndOffset()));
342             final FloatProp mTaskViewScaleX = new FloatProp(1f, prop.finalTaskViewScaleX,
343                     clampToProgress(timings.getStagedRectScaleXInterpolator(),
344                     timings.getStagedRectSlideStartOffset(),
345                     timings.getStagedRectSlideEndOffset()));
346             final FloatProp mTaskViewScaleY = new FloatProp(1f, prop.finalTaskViewScaleY,
347                     clampToProgress(timings.getStagedRectScaleYInterpolator(),
348                     timings.getStagedRectSlideStartOffset(),
349                     timings.getStagedRectSlideEndOffset()));
350             @Override
351             public void onUpdate(float percent, boolean initOnly) {
352                 // Calculate the icon position.
353                 floatingTaskViewBounds.set(startingBounds);
354                 floatingTaskViewBounds.offset(mDx.value, mDy.value);
355                 Utilities.scaleRectFAboutCenter(floatingTaskViewBounds, mTaskViewScaleX.value,
356                         mTaskViewScaleY.value);
357 
358                 update(floatingTaskViewBounds, percent);
359             }
360         };
361 
362         transitionAnimator.addUpdateListener(listener);
363     }
364 
fadeInSplitPlaceholder(PendingAnimation animation, SplitAnimationTimings timings)365     void fadeInSplitPlaceholder(PendingAnimation animation, SplitAnimationTimings timings) {
366         animation.setViewAlpha(mSplitPlaceholderView, 1, clampToProgress(LINEAR,
367                 timings.getPlaceholderFadeInStartOffset(),
368                 timings.getPlaceholderFadeInEndOffset()));
369         animation.setViewAlpha(mSplitPlaceholderView.getIconView(), 1, clampToProgress(LINEAR,
370                 timings.getPlaceholderIconFadeInStartOffset(),
371                 timings.getPlaceholderIconFadeInEndOffset()));
372     }
373 
drawRoundedRect(Canvas canvas, Paint paint)374     void drawRoundedRect(Canvas canvas, Paint paint) {
375         if (mFullscreenParams == null) {
376             return;
377         }
378 
379         canvas.drawRoundRect(0, 0, getMeasuredWidth(), getMeasuredHeight(),
380                 mFullscreenParams.mCurrentDrawnCornerRadius / mFullscreenParams.mScaleX,
381                 mFullscreenParams.mCurrentDrawnCornerRadius / mFullscreenParams.mScaleY,
382                 paint);
383     }
384 
385     /**
386      * When a split is staged, center the icon in the staging area. Accounts for device insets.
387      * @param iconView The icon that should be centered.
388      * @param onScreenRectCenterX The x-center of the on-screen staging area (most of the Rect is
389      *                        offscreen).
390      * @param onScreenRectCenterY The y-center of the on-screen staging area (most of the Rect is
391      *                        offscreen).
392      */
centerIconView(IconView iconView, float onScreenRectCenterX, float onScreenRectCenterY)393     void centerIconView(IconView iconView, float onScreenRectCenterX, float onScreenRectCenterY) {
394         mOrientationHandler.updateSplitIconParams(iconView, onScreenRectCenterX,
395                 onScreenRectCenterY, mFullscreenParams.mScaleX, mFullscreenParams.mScaleY,
396                 iconView.getDrawableWidth(), iconView.getDrawableHeight(),
397                 mContainer.getDeviceProfile(), mStagePosition);
398     }
399 
getStagePosition()400     public int getStagePosition() {
401         return mStagePosition;
402     }
403 
404     private static class SplitOverlayProperties {
405 
406         private final float finalTaskViewScaleX;
407         private final float finalTaskViewScaleY;
408         private final float dX;
409         private final float dY;
410 
SplitOverlayProperties(Rect endBounds, RectF startTaskViewBounds, int dragLayerLeft, int dragLayerTop)411         SplitOverlayProperties(Rect endBounds, RectF startTaskViewBounds,
412                 int dragLayerLeft, int dragLayerTop) {
413             float maxScaleX = endBounds.width() / startTaskViewBounds.width();
414             float maxScaleY = endBounds.height() / startTaskViewBounds.height();
415 
416             finalTaskViewScaleX = maxScaleX;
417             finalTaskViewScaleY = maxScaleY;
418 
419             // Animate to the center of the window bounds in screen coordinates.
420             float centerX = endBounds.centerX() - dragLayerLeft;
421             float centerY = endBounds.centerY() - dragLayerTop;
422 
423             dX = centerX - startTaskViewBounds.centerX();
424             dY = centerY - startTaskViewBounds.centerY();
425         }
426     }
427 
428     public static class FullscreenDrawParams {
429 
430         private final float mCornerRadius;
431         private final float mWindowCornerRadius;
432         public boolean mIsStagedTask;
433         public final RectF mBounds = new RectF();
434         public float mCurrentDrawnCornerRadius;
435         public float mScaleX = 1;
436         public float mScaleY = 1;
437 
FullscreenDrawParams(Context context)438         public FullscreenDrawParams(Context context) {
439             mCornerRadius = TaskCornerRadius.get(context);
440             mWindowCornerRadius = QuickStepContract.getWindowCornerRadius(context);
441 
442             mCurrentDrawnCornerRadius = mCornerRadius;
443         }
444 
updateParams(RectF bounds, float progress, float scaleX, float scaleY)445         public void updateParams(RectF bounds, float progress, float scaleX, float scaleY) {
446             mBounds.set(bounds);
447             mScaleX = scaleX;
448             mScaleY = scaleY;
449             mCurrentDrawnCornerRadius = mIsStagedTask ? mWindowCornerRadius :
450                     Utilities.mapRange(progress, mCornerRadius, mWindowCornerRadius);
451         }
452 
setIsStagedTask(boolean isStagedTask)453         public void setIsStagedTask(boolean isStagedTask) {
454             mIsStagedTask = isStagedTask;
455         }
456     }
457 }
458