1 package com.android.quickstep.views;
2 
3 import static com.android.launcher3.anim.Interpolators.ACCEL;
4 import static com.android.launcher3.anim.Interpolators.DEACCEL_3;
5 import static com.android.launcher3.anim.Interpolators.LINEAR;
6 import static com.android.systemui.shared.system.QuickStepContract.supportsRoundedCornersOnWindows;
7 
8 import android.animation.ValueAnimator;
9 import android.content.Context;
10 import android.graphics.Rect;
11 import android.graphics.RectF;
12 import android.util.AttributeSet;
13 import android.view.View;
14 import android.view.ViewGroup;
15 import android.widget.FrameLayout;
16 import android.widget.ImageView;
17 
18 import androidx.annotation.Nullable;
19 
20 import com.android.launcher3.BaseActivity;
21 import com.android.launcher3.BaseDraggingActivity;
22 import com.android.launcher3.InsettableFrameLayout;
23 import com.android.launcher3.LauncherAnimUtils;
24 import com.android.launcher3.R;
25 import com.android.launcher3.Utilities;
26 import com.android.launcher3.anim.PendingAnimation;
27 import com.android.launcher3.statemanager.StatefulActivity;
28 import com.android.launcher3.touch.PagedOrientationHandler;
29 import com.android.launcher3.views.BaseDragLayer;
30 import com.android.quickstep.util.MultiValueUpdateListener;
31 
32 /**
33  * Create an instance via {@link #getFloatingTaskView(StatefulActivity, TaskView, RectF)} to
34  * which will have the thumbnail from the provided existing TaskView overlaying the taskview itself.
35  *
36  * Can then animate the taskview using
37  * {@link #addAnimation(PendingAnimation, RectF, Rect, View, boolean)}
38  * giving a starting and ending bounds. Currently this is set to use the split placeholder view,
39  * but it could be generified.
40  *
41  * TODO: Figure out how to copy thumbnail data from existing TaskView to this view.
42  */
43 public class FloatingTaskView extends FrameLayout {
44 
45     private SplitPlaceholderView mSplitPlaceholderView;
46     private RectF mStartingPosition;
47     private final BaseDraggingActivity mActivity;
48     private final boolean mIsRtl;
49     private final Rect mOutline = new Rect();
50     private PagedOrientationHandler mOrientationHandler;
51     private ImageView mImageView;
52 
FloatingTaskView(Context context)53     public FloatingTaskView(Context context) {
54         this(context, null);
55     }
56 
FloatingTaskView(Context context, @Nullable AttributeSet attrs)57     public FloatingTaskView(Context context, @Nullable AttributeSet attrs) {
58         this(context, attrs, 0);
59     }
60 
FloatingTaskView(Context context, @Nullable AttributeSet attrs, int defStyleAttr)61     public FloatingTaskView(Context context, @Nullable AttributeSet attrs, int defStyleAttr) {
62         super(context, attrs, defStyleAttr);
63         mActivity = BaseActivity.fromContext(context);
64         mIsRtl = Utilities.isRtl(getResources());
65     }
66 
67     @Override
onFinishInflate()68     protected void onFinishInflate() {
69         super.onFinishInflate();
70         mImageView = findViewById(R.id.thumbnail);
71         mImageView.setScaleType(ImageView.ScaleType.CENTER_CROP);
72         mImageView.setLayerType(LAYER_TYPE_HARDWARE, null);
73         mSplitPlaceholderView = findViewById(R.id.split_placeholder);
74         mSplitPlaceholderView.setAlpha(0);
75     }
76 
init(StatefulActivity launcher, TaskView originalView, RectF positionOut)77     private void init(StatefulActivity launcher, TaskView originalView, RectF positionOut) {
78         mStartingPosition = positionOut;
79         updateInitialPositionForView(originalView);
80         final InsettableFrameLayout.LayoutParams lp =
81                 (InsettableFrameLayout.LayoutParams) getLayoutParams();
82 
83         mSplitPlaceholderView.setLayoutParams(new FrameLayout.LayoutParams(lp.width, lp.height));
84         positionOut.round(mOutline);
85         setPivotX(0);
86         setPivotY(0);
87 
88         // Copy bounds of exiting thumbnail into ImageView
89         TaskThumbnailView thumbnail = originalView.getThumbnail();
90         mImageView.setImageBitmap(thumbnail.getThumbnail());
91         mImageView.setVisibility(VISIBLE);
92 
93         mOrientationHandler = originalView.getRecentsView().getPagedOrientationHandler();
94         mSplitPlaceholderView.setIconView(originalView.getIconView(),
95                 launcher.getDeviceProfile().overviewTaskIconDrawableSizePx);
96         mSplitPlaceholderView.getIconView().setRotation(mOrientationHandler.getDegreesRotated());
97     }
98 
99     /**
100      * Configures and returns a an instance of {@link FloatingTaskView} initially matching the
101      * appearance of {@code originalView}.
102      */
getFloatingTaskView(StatefulActivity launcher, TaskView originalView, RectF positionOut)103     public static FloatingTaskView getFloatingTaskView(StatefulActivity launcher,
104             TaskView originalView, RectF positionOut) {
105         final BaseDragLayer dragLayer = launcher.getDragLayer();
106         ViewGroup parent = (ViewGroup) dragLayer.getParent();
107         final FloatingTaskView floatingView = (FloatingTaskView) launcher.getLayoutInflater()
108                 .inflate(R.layout.floating_split_select_view, parent, false);
109 
110         floatingView.init(launcher, originalView, positionOut);
111         parent.addView(floatingView);
112         return floatingView;
113     }
114 
updateInitialPositionForView(TaskView originalView)115     public void updateInitialPositionForView(TaskView originalView) {
116         View thumbnail = originalView.getThumbnail();
117         Rect viewBounds = new Rect(0, 0, thumbnail.getWidth(), thumbnail.getHeight());
118         Utilities.getBoundsForViewInDragLayer(mActivity.getDragLayer(), thumbnail, viewBounds,
119                 true /* ignoreTransform */, null /* recycle */,
120                 mStartingPosition);
121         mStartingPosition.offset(originalView.getTranslationX(), originalView.getTranslationY());
122         final InsettableFrameLayout.LayoutParams lp = new InsettableFrameLayout.LayoutParams(
123                 Math.round(mStartingPosition.width()),
124                 Math.round(mStartingPosition.height()));
125         initPosition(mStartingPosition, lp);
126         setLayoutParams(lp);
127     }
128 
129     // TODO(194414938) set correct corner radii
update(RectF position, float progress, float windowRadius)130     public void update(RectF position, float progress, float windowRadius) {
131         MarginLayoutParams lp = (MarginLayoutParams) getLayoutParams();
132 
133         float dX = position.left - mStartingPosition.left;
134         float dY = position.top - lp.topMargin;
135 
136         setTranslationX(dX);
137         setTranslationY(dY);
138 
139         float scaleX = position.width() / lp.width;
140         float scaleY = position.height() / lp.height;
141         setScaleX(scaleX);
142         setScaleY(scaleY);
143         float childScaleX = 1f / scaleX;
144         float childScaleY = 1f / scaleY;
145 
146         invalidate();
147         // TODO(194414938) seems like this scale value could be fine tuned, some stretchiness
148         mImageView.setScaleX(1f / scaleX + scaleX * progress);
149         mImageView.setScaleY(1f / scaleY + scaleY * progress);
150         mOrientationHandler.setPrimaryScale(mSplitPlaceholderView.getIconView(), childScaleX);
151         mOrientationHandler.setSecondaryScale(mSplitPlaceholderView.getIconView(), childScaleY);
152     }
153 
updateOrientationHandler(PagedOrientationHandler orientationHandler)154     public void updateOrientationHandler(PagedOrientationHandler orientationHandler) {
155         mOrientationHandler = orientationHandler;
156         mSplitPlaceholderView.getIconView().setRotation(mOrientationHandler.getDegreesRotated());
157     }
158 
initPosition(RectF pos, InsettableFrameLayout.LayoutParams lp)159     protected void initPosition(RectF pos, InsettableFrameLayout.LayoutParams lp) {
160         mStartingPosition.set(pos);
161         lp.ignoreInsets = true;
162         // Position the floating view exactly on top of the original
163         lp.topMargin = Math.round(pos.top);
164         if (mIsRtl) {
165             lp.setMarginStart(mActivity.getDeviceProfile().widthPx - Math.round(pos.right));
166         } else {
167             lp.setMarginStart(Math.round(pos.left));
168         }
169 
170         // Set the properties here already to make sure they are available when running the first
171         // animation frame.
172         int left = (int) pos.left;
173         layout(left, lp.topMargin, left + lp.width, lp.topMargin + lp.height);
174     }
175 
addAnimation(PendingAnimation animation, RectF startingBounds, Rect endBounds, View viewToCover, boolean fadeWithThumbnail)176     public void addAnimation(PendingAnimation animation, RectF startingBounds, Rect endBounds,
177             View viewToCover, boolean fadeWithThumbnail) {
178         final BaseDragLayer dragLayer = mActivity.getDragLayer();
179         int[] dragLayerBounds = new int[2];
180         dragLayer.getLocationOnScreen(dragLayerBounds);
181         SplitOverlayProperties prop = new SplitOverlayProperties(endBounds,
182                 startingBounds, viewToCover, dragLayerBounds[0],
183                 dragLayerBounds[1]);
184 
185         ValueAnimator transitionAnimator = ValueAnimator.ofFloat(0, 1);
186         animation.add(transitionAnimator);
187         long animDuration = animation.getDuration();
188         Rect crop = new Rect();
189         RectF floatingTaskViewBounds = new RectF();
190         final float initialWindowRadius = supportsRoundedCornersOnWindows(getResources())
191                 ? Math.max(crop.width(), crop.height()) / 2f
192                 : 0f;
193 
194         if (fadeWithThumbnail) {
195             animation.addFloat(mSplitPlaceholderView, SplitPlaceholderView.ALPHA_FLOAT,
196                     0, 1, ACCEL);
197             animation.addFloat(mImageView, LauncherAnimUtils.VIEW_ALPHA,
198                     1, 0, DEACCEL_3);
199         }
200 
201         MultiValueUpdateListener listener = new MultiValueUpdateListener() {
202             final FloatProp mWindowRadius = new FloatProp(initialWindowRadius,
203                     initialWindowRadius, 0, animDuration, LINEAR);
204             final FloatProp mDx = new FloatProp(0, prop.dX, 0, animDuration, LINEAR);
205             final FloatProp mDy = new FloatProp(0, prop.dY, 0, animDuration, LINEAR);
206             final FloatProp mTaskViewScaleX = new FloatProp(prop.initialTaskViewScaleX,
207                     prop.finalTaskViewScaleX, 0, animDuration, LINEAR);
208             final FloatProp mTaskViewScaleY = new FloatProp(prop.initialTaskViewScaleY,
209                     prop.finalTaskViewScaleY, 0, animDuration, LINEAR);
210             @Override
211             public void onUpdate(float percent, boolean initOnly) {
212                 // Calculate the icon position.
213                 floatingTaskViewBounds.set(startingBounds);
214                 floatingTaskViewBounds.offset(mDx.value, mDy.value);
215                 Utilities.scaleRectFAboutCenter(floatingTaskViewBounds, mTaskViewScaleX.value,
216                         mTaskViewScaleY.value);
217 
218                 update(floatingTaskViewBounds, percent, mWindowRadius.value * 1);
219             }
220         };
221         transitionAnimator.addUpdateListener(listener);
222     }
223 
224     private static class SplitOverlayProperties {
225 
226         private final float initialTaskViewScaleX;
227         private final float initialTaskViewScaleY;
228         private final float finalTaskViewScaleX;
229         private final float finalTaskViewScaleY;
230         private final float dX;
231         private final float dY;
232 
SplitOverlayProperties(Rect endBounds, RectF startTaskViewBounds, View view, int dragLayerLeft, int dragLayerTop)233         SplitOverlayProperties(Rect endBounds, RectF startTaskViewBounds, View view,
234                 int dragLayerLeft, int dragLayerTop) {
235             float maxScaleX = endBounds.width() / startTaskViewBounds.width();
236             float maxScaleY = endBounds.height() / startTaskViewBounds.height();
237 
238             initialTaskViewScaleX = view.getScaleX();
239             initialTaskViewScaleY = view.getScaleY();
240             finalTaskViewScaleX = maxScaleX;
241             finalTaskViewScaleY = maxScaleY;
242 
243             // Animate the app icon to the center of the window bounds in screen coordinates.
244             float centerX = endBounds.centerX() - dragLayerLeft;
245             float centerY = endBounds.centerY() - dragLayerTop;
246 
247             dX = centerX - startTaskViewBounds.centerX();
248             dY = centerY - startTaskViewBounds.centerY();
249         }
250     }
251 }
252