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