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