1 /*
2  * Copyright (C) 2008 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 
17 package com.android.launcher3.dragndrop;
18 
19 import static android.view.View.MeasureSpec.EXACTLY;
20 import static android.view.View.MeasureSpec.makeMeasureSpec;
21 
22 import static com.android.launcher3.LauncherAnimUtils.VIEW_ALPHA;
23 import static com.android.launcher3.Utilities.getBadge;
24 import static com.android.launcher3.util.Executors.MODEL_EXECUTOR;
25 
26 import android.animation.Animator;
27 import android.animation.AnimatorListenerAdapter;
28 import android.animation.AnimatorSet;
29 import android.animation.ObjectAnimator;
30 import android.animation.ValueAnimator;
31 import android.animation.ValueAnimator.AnimatorUpdateListener;
32 import android.annotation.TargetApi;
33 import android.content.Context;
34 import android.graphics.Bitmap;
35 import android.graphics.Canvas;
36 import android.graphics.Color;
37 import android.graphics.Path;
38 import android.graphics.Picture;
39 import android.graphics.Point;
40 import android.graphics.Rect;
41 import android.graphics.drawable.AdaptiveIconDrawable;
42 import android.graphics.drawable.ColorDrawable;
43 import android.graphics.drawable.Drawable;
44 import android.graphics.drawable.PictureDrawable;
45 import android.os.Build;
46 import android.os.Handler;
47 import android.os.Looper;
48 import android.view.View;
49 import android.view.ViewGroup;
50 import android.widget.FrameLayout;
51 import android.widget.ImageView;
52 
53 import androidx.annotation.Nullable;
54 import androidx.dynamicanimation.animation.FloatPropertyCompat;
55 import androidx.dynamicanimation.animation.SpringAnimation;
56 import androidx.dynamicanimation.animation.SpringForce;
57 
58 import com.android.launcher3.LauncherSettings;
59 import com.android.launcher3.R;
60 import com.android.launcher3.Utilities;
61 import com.android.launcher3.anim.Interpolators;
62 import com.android.launcher3.icons.FastBitmapDrawable;
63 import com.android.launcher3.icons.LauncherIcons;
64 import com.android.launcher3.model.data.ItemInfo;
65 import com.android.launcher3.util.RunnableList;
66 import com.android.launcher3.views.ActivityContext;
67 import com.android.launcher3.views.BaseDragLayer;
68 
69 /** A custom view for rendering an icon, folder, shortcut or widget during drag-n-drop. */
70 public abstract class DragView<T extends Context & ActivityContext> extends FrameLayout {
71 
72     public static final int VIEW_ZOOM_DURATION = 150;
73 
74     private final View mContent;
75     // The following are only used for rendering mContent directly during drag-n-drop.
76     @Nullable private ViewGroup.LayoutParams mContentViewLayoutParams;
77     @Nullable private ViewGroup mContentViewParent;
78     private int mContentViewInParentViewIndex = -1;
79     private final int mWidth;
80     private final int mHeight;
81 
82     private final int mBlurSizeOutline;
83     protected final int mRegistrationX;
84     protected final int mRegistrationY;
85     private final float mInitialScale;
86     protected final float mScaleOnDrop;
87     protected final int[] mTempLoc = new int[2];
88 
89     private final RunnableList mOnDragStartCallback = new RunnableList();
90 
91     private Point mDragVisualizeOffset = null;
92     private Rect mDragRegion = null;
93     protected final T mActivity;
94     private final BaseDragLayer<T> mDragLayer;
95     private boolean mHasDrawn = false;
96 
97     final ValueAnimator mAnim;
98     // Whether mAnim has started. Unlike mAnim.isStarted(), this is true even after mAnim ends.
99     private boolean mAnimStarted;
100 
101     private int mLastTouchX;
102     private int mLastTouchY;
103     private int mAnimatedShiftX;
104     private int mAnimatedShiftY;
105 
106     // Below variable only needed IF FeatureFlags.LAUNCHER3_SPRING_ICONS is {@code true}
107     private Drawable mBgSpringDrawable, mFgSpringDrawable;
108     private SpringFloatValue mTranslateX, mTranslateY;
109     private Path mScaledMaskPath;
110     private Drawable mBadge;
111 
DragView(T launcher, Drawable drawable, int registrationX, int registrationY, final float initialScale, final float scaleOnDrop, final float finalScaleDps)112     public DragView(T launcher, Drawable drawable, int registrationX,
113             int registrationY, final float initialScale, final float scaleOnDrop,
114             final float finalScaleDps) {
115         this(launcher, getViewFromDrawable(launcher, drawable),
116                 drawable.getIntrinsicWidth(), drawable.getIntrinsicHeight(),
117                 registrationX, registrationY, initialScale, scaleOnDrop, finalScaleDps);
118     }
119 
120     /**
121      * Construct the drag view.
122      * <p>
123      * The registration point is the point inside our view that the touch events should
124      * be centered upon.
125      * @param activity The Launcher instance/ActivityContext this DragView is in.
126      * @param content the view content that is attached to the drag view.
127      * @param width the width of the dragView
128      * @param height the height of the dragView
129      * @param initialScale The view that we're dragging around.  We scale it up when we draw it.
130      * @param registrationX The x coordinate of the registration point.
131      * @param registrationY The y coordinate of the registration point.
132      * @param scaleOnDrop the scale used in the drop animation.
133      * @param finalScaleDps the scale used in the zoom out animation when the drag view is shown.
134      */
DragView(T activity, View content, int width, int height, int registrationX, int registrationY, final float initialScale, final float scaleOnDrop, final float finalScaleDps)135     public DragView(T activity, View content, int width, int height, int registrationX,
136             int registrationY, final float initialScale, final float scaleOnDrop,
137             final float finalScaleDps) {
138         super(activity);
139         mActivity = activity;
140         mDragLayer = activity.getDragLayer();
141 
142         mContent = content;
143         mWidth = width;
144         mHeight = height;
145         mContentViewLayoutParams = mContent.getLayoutParams();
146         if (mContent.getParent() instanceof ViewGroup) {
147             mContentViewParent = (ViewGroup) mContent.getParent();
148             mContentViewInParentViewIndex = mContentViewParent.indexOfChild(mContent);
149             mContentViewParent.removeView(mContent);
150         }
151 
152         addView(content, new LayoutParams(width, height));
153 
154         // If there is already a scale set on the content, we don't want to clip the children.
155         if (content.getScaleX() != 1 || content.getScaleY() != 1) {
156             setClipChildren(false);
157             setClipToPadding(false);
158         }
159 
160         final float scale = (width + finalScaleDps) / width;
161 
162         // Set the initial scale to avoid any jumps
163         setScaleX(initialScale);
164         setScaleY(initialScale);
165 
166         // Animate the view into the correct position
167         mAnim = ValueAnimator.ofFloat(0f, 1f);
168         mAnim.setDuration(VIEW_ZOOM_DURATION);
169         mAnim.addUpdateListener(animation -> {
170             final float value = (Float) animation.getAnimatedValue();
171             setScaleX(initialScale + (value * (scale - initialScale)));
172             setScaleY(initialScale + (value * (scale - initialScale)));
173             if (!isAttachedToWindow()) {
174                 animation.cancel();
175             }
176         });
177         mAnim.addListener(new AnimatorListenerAdapter() {
178             @Override
179             public void onAnimationStart(Animator animation) {
180                 mAnimStarted = true;
181             }
182         });
183 
184         setDragRegion(new Rect(0, 0, width, height));
185 
186         // The point in our scaled bitmap that the touch events are located
187         mRegistrationX = registrationX;
188         mRegistrationY = registrationY;
189 
190         mInitialScale = initialScale;
191         mScaleOnDrop = scaleOnDrop;
192 
193         // Force a measure, because Workspace uses getMeasuredHeight() before the layout pass
194         measure(makeMeasureSpec(width, EXACTLY), makeMeasureSpec(height, EXACTLY));
195 
196         mBlurSizeOutline = getResources().getDimensionPixelSize(R.dimen.blur_size_medium_outline);
197         setElevation(getResources().getDimension(R.dimen.drag_elevation));
198         setWillNotDraw(false);
199     }
200 
201     /**
202      * Initialize {@code #mIconDrawable} if the item can be represented using
203      * an {@link AdaptiveIconDrawable} or {@link FolderAdaptiveIcon}.
204      */
205     @TargetApi(Build.VERSION_CODES.O)
setItemInfo(final ItemInfo info)206     public void setItemInfo(final ItemInfo info) {
207         if (info.itemType != LauncherSettings.Favorites.ITEM_TYPE_APPLICATION
208                 && info.itemType != LauncherSettings.Favorites.ITEM_TYPE_SEARCH_ACTION
209                 && info.itemType != LauncherSettings.Favorites.ITEM_TYPE_DEEP_SHORTCUT
210                 && info.itemType != LauncherSettings.Favorites.ITEM_TYPE_FOLDER) {
211             return;
212         }
213         // Load the adaptive icon on a background thread and add the view in ui thread.
214         MODEL_EXECUTOR.getHandler().postAtFrontOfQueue(() -> {
215             Object[] outObj = new Object[1];
216             int w = mWidth;
217             int h = mHeight;
218             Drawable dr = Utilities.getFullDrawable(mActivity, info, w, h, outObj);
219 
220             if (dr instanceof AdaptiveIconDrawable) {
221                 int blurMargin = (int) mActivity.getResources()
222                         .getDimension(R.dimen.blur_size_medium_outline) / 2;
223 
224                 Rect bounds = new Rect(0, 0, w, h);
225                 bounds.inset(blurMargin, blurMargin);
226                 // Badge is applied after icon normalization so the bounds for badge should not
227                 // be scaled down due to icon normalization.
228                 Rect badgeBounds = new Rect(bounds);
229                 mBadge = getBadge(mActivity, info, outObj[0]);
230                 mBadge.setBounds(badgeBounds);
231 
232                 // Do not draw the background in case of folder as its translucent
233                 final boolean shouldDrawBackground = !(dr instanceof FolderAdaptiveIcon);
234 
235                 try (LauncherIcons li = LauncherIcons.obtain(mActivity)) {
236                     Drawable nDr; // drawable to be normalized
237                     if (shouldDrawBackground) {
238                         nDr = dr;
239                     } else {
240                         // Since we just want the scale, avoid heavy drawing operations
241                         nDr = new AdaptiveIconDrawable(new ColorDrawable(Color.BLACK), null);
242                     }
243                     Utilities.scaleRectAboutCenter(bounds,
244                             li.getNormalizer().getScale(nDr, null, null, null));
245                 }
246                 AdaptiveIconDrawable adaptiveIcon = (AdaptiveIconDrawable) dr;
247 
248                 // Shrink very tiny bit so that the clip path is smaller than the original bitmap
249                 // that has anti aliased edges and shadows.
250                 Rect shrunkBounds = new Rect(bounds);
251                 Utilities.scaleRectAboutCenter(shrunkBounds, 0.98f);
252                 adaptiveIcon.setBounds(shrunkBounds);
253                 final Path mask = adaptiveIcon.getIconMask();
254 
255                 mTranslateX = new SpringFloatValue(DragView.this,
256                         w * AdaptiveIconDrawable.getExtraInsetFraction());
257                 mTranslateY = new SpringFloatValue(DragView.this,
258                         h * AdaptiveIconDrawable.getExtraInsetFraction());
259 
260                 bounds.inset(
261                         (int) (-bounds.width() * AdaptiveIconDrawable.getExtraInsetFraction()),
262                         (int) (-bounds.height() * AdaptiveIconDrawable.getExtraInsetFraction())
263                 );
264                 mBgSpringDrawable = adaptiveIcon.getBackground();
265                 if (mBgSpringDrawable == null) {
266                     mBgSpringDrawable = new ColorDrawable(Color.TRANSPARENT);
267                 }
268                 mBgSpringDrawable.setBounds(bounds);
269                 mFgSpringDrawable = adaptiveIcon.getForeground();
270                 if (mFgSpringDrawable == null) {
271                     mFgSpringDrawable = new ColorDrawable(Color.TRANSPARENT);
272                 }
273                 mFgSpringDrawable.setBounds(bounds);
274 
275                 new Handler(Looper.getMainLooper()).post(() -> mOnDragStartCallback.add(() -> {
276                     // TODO: Consider fade-in animation
277                     // Assign the variable on the UI thread to avoid race conditions.
278                     mScaledMaskPath = mask;
279                     // Avoid relayout as we do not care about children affecting layout
280                     removeAllViewsInLayout();
281 
282                     if (info.isDisabled()) {
283                         FastBitmapDrawable d = new FastBitmapDrawable((Bitmap) null);
284                         d.setIsDisabled(true);
285                         mBgSpringDrawable.setColorFilter(d.getColorFilter());
286                         mFgSpringDrawable.setColorFilter(d.getColorFilter());
287                         mBadge.setColorFilter(d.getColorFilter());
288                     }
289                     invalidate();
290                 }));
291             }
292         });
293     }
294 
295     /**
296      * Called when pre-drag finishes for an icon
297      */
onDragStart()298     public void onDragStart() {
299         mOnDragStartCallback.executeAllAndDestroy();
300     }
301 
302     @Override
onMeasure(int widthMeasureSpec, int heightMeasureSpec)303     protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
304         super.onMeasure(makeMeasureSpec(mWidth, EXACTLY), makeMeasureSpec(mHeight, EXACTLY));
305     }
306 
getDragRegionWidth()307     public int getDragRegionWidth() {
308         return mDragRegion.width();
309     }
310 
getDragRegionHeight()311     public int getDragRegionHeight() {
312         return mDragRegion.height();
313     }
314 
setDragVisualizeOffset(Point p)315     public void setDragVisualizeOffset(Point p) {
316         mDragVisualizeOffset = p;
317     }
318 
getDragVisualizeOffset()319     public Point getDragVisualizeOffset() {
320         return mDragVisualizeOffset;
321     }
322 
setDragRegion(Rect r)323     public void setDragRegion(Rect r) {
324         mDragRegion = r;
325     }
326 
getDragRegion()327     public Rect getDragRegion() {
328         return mDragRegion;
329     }
330 
331     @Override
draw(Canvas canvas)332     public void draw(Canvas canvas) {
333         super.draw(canvas);
334 
335         // Draw after the content
336         mHasDrawn = true;
337         if (mScaledMaskPath != null) {
338             int cnt = canvas.save();
339             canvas.clipPath(mScaledMaskPath);
340             mBgSpringDrawable.draw(canvas);
341             canvas.translate(mTranslateX.mValue, mTranslateY.mValue);
342             mFgSpringDrawable.draw(canvas);
343             canvas.restoreToCount(cnt);
344             mBadge.draw(canvas);
345         }
346     }
347 
crossFadeContent(Drawable crossFadeDrawable, int duration)348     public void crossFadeContent(Drawable crossFadeDrawable, int duration) {
349         if (mContent.getParent() == null) {
350             // If the content is already removed, ignore
351             return;
352         }
353         View newContent = getViewFromDrawable(getContext(), crossFadeDrawable);
354         newContent.measure(makeMeasureSpec(mWidth, EXACTLY), makeMeasureSpec(mHeight, EXACTLY));
355         newContent.layout(0, 0, mWidth, mHeight);
356         addViewInLayout(newContent, 0, new LayoutParams(mWidth, mHeight));
357 
358         AnimatorSet anim = new AnimatorSet();
359         anim.play(ObjectAnimator.ofFloat(newContent, VIEW_ALPHA, 0, 1));
360         anim.play(ObjectAnimator.ofFloat(mContent, VIEW_ALPHA, 0));
361         anim.setDuration(duration).setInterpolator(Interpolators.DEACCEL_1_5);
362         anim.start();
363     }
364 
hasDrawn()365     public boolean hasDrawn() {
366         return mHasDrawn;
367     }
368 
369     /**
370      * Create a window containing this view and show it.
371      *
372      * @param touchX the x coordinate the user touched in DragLayer coordinates
373      * @param touchY the y coordinate the user touched in DragLayer coordinates
374      */
show(int touchX, int touchY)375     public void show(int touchX, int touchY) {
376         mDragLayer.addView(this);
377 
378         // Start the pick-up animation
379         BaseDragLayer.LayoutParams lp = new BaseDragLayer.LayoutParams(mWidth, mHeight);
380         lp.customPosition = true;
381         setLayoutParams(lp);
382 
383         if (mContent != null) {
384             // At the drag start, the source view visibility is set to invisible.
385             mContent.setVisibility(VISIBLE);
386         }
387 
388         move(touchX, touchY);
389         // Post the animation to skip other expensive work happening on the first frame
390         post(mAnim::start);
391     }
392 
cancelAnimation()393     public void cancelAnimation() {
394         if (mAnim != null && mAnim.isRunning()) {
395             mAnim.cancel();
396         }
397     }
398 
isAnimationFinished()399     public boolean isAnimationFinished() {
400         return mAnimStarted && !mAnim.isRunning();
401     }
402 
403     /**
404      * Move the window containing this view.
405      *
406      * @param touchX the x coordinate the user touched in DragLayer coordinates
407      * @param touchY the y coordinate the user touched in DragLayer coordinates
408      */
move(int touchX, int touchY)409     public void move(int touchX, int touchY) {
410         if (touchX > 0 && touchY > 0 && mLastTouchX > 0 && mLastTouchY > 0
411                 && mScaledMaskPath != null) {
412             mTranslateX.animateToPos(mLastTouchX - touchX);
413             mTranslateY.animateToPos(mLastTouchY - touchY);
414         }
415         mLastTouchX = touchX;
416         mLastTouchY = touchY;
417         applyTranslation();
418     }
419 
420     /**
421      * Animate this DragView to the given DragLayer coordinates and then remove it.
422      */
animateTo(int toTouchX, int toTouchY, Runnable onCompleteRunnable, int duration)423     public abstract void animateTo(int toTouchX, int toTouchY, Runnable onCompleteRunnable,
424             int duration);
425 
animateShift(final int shiftX, final int shiftY)426     public void animateShift(final int shiftX, final int shiftY) {
427         if (mAnim.isStarted()) {
428             return;
429         }
430         mAnimatedShiftX = shiftX;
431         mAnimatedShiftY = shiftY;
432         applyTranslation();
433         mAnim.addUpdateListener(new AnimatorUpdateListener() {
434             @Override
435             public void onAnimationUpdate(ValueAnimator animation) {
436                 float fraction = 1 - animation.getAnimatedFraction();
437                 mAnimatedShiftX = (int) (fraction * shiftX);
438                 mAnimatedShiftY = (int) (fraction * shiftY);
439                 applyTranslation();
440             }
441         });
442     }
443 
applyTranslation()444     private void applyTranslation() {
445         setTranslationX(mLastTouchX - mRegistrationX + mAnimatedShiftX);
446         setTranslationY(mLastTouchY - mRegistrationY + mAnimatedShiftY);
447     }
448 
449     /**
450      * Detaches {@link #mContent}, if previously attached, from this view.
451      *
452      * <p>In the case of no change in the drop position, sets {@code reattachToPreviousParent} to
453      * {@code true} to attach the {@link #mContent} back to its previous parent.
454      */
detachContentView(boolean reattachToPreviousParent)455     public void detachContentView(boolean reattachToPreviousParent) {
456         if (mContent != null && mContentViewParent != null && mContentViewInParentViewIndex >= 0) {
457             Picture picture = new Picture();
458             mContent.draw(picture.beginRecording(mWidth, mHeight));
459             picture.endRecording();
460             View view = new View(mActivity);
461             view.setBackground(new PictureDrawable(picture));
462             view.measure(makeMeasureSpec(mWidth, EXACTLY), makeMeasureSpec(mHeight, EXACTLY));
463             view.layout(mContent.getLeft(), mContent.getTop(),
464                     mContent.getRight(), mContent.getBottom());
465             setClipToOutline(mContent.getClipToOutline());
466             setOutlineProvider(mContent.getOutlineProvider());
467             addViewInLayout(view, indexOfChild(mContent), mContent.getLayoutParams(), true);
468 
469             removeViewInLayout(mContent);
470             mContent.setVisibility(INVISIBLE);
471             mContent.setLayoutParams(mContentViewLayoutParams);
472             if (reattachToPreviousParent) {
473                 mContentViewParent.addView(mContent, mContentViewInParentViewIndex);
474             }
475             mContentViewParent = null;
476             mContentViewInParentViewIndex = -1;
477         }
478     }
479 
480     /**
481      * Removes this view from the {@link DragLayer}.
482      *
483      * <p>If the drag content is a {@link #mContent}, this call doesn't reattach the
484      * {@link #mContent} back to its previous parent. To reattach to previous parent, the caller
485      * should call {@link #detachContentView} with {@code reattachToPreviousParent} sets to true
486      * before this call.
487      */
remove()488     public void remove() {
489         if (getParent() != null) {
490             mDragLayer.removeView(DragView.this);
491         }
492     }
493 
getBlurSizeOutline()494     public int getBlurSizeOutline() {
495         return mBlurSizeOutline;
496     }
497 
getInitialScale()498     public float getInitialScale() {
499         return mInitialScale;
500     }
501 
502     @Override
hasOverlappingRendering()503     public boolean hasOverlappingRendering() {
504         return false;
505     }
506 
507     /** Returns the current content view that is rendered in the drag view. */
getContentView()508     public View getContentView() {
509         return mContent;
510     }
511 
512     /**
513      * Returns the previous {@link ViewGroup} parent of the {@link #mContent} before the drag
514      * content is attached to this view.
515      */
516     @Nullable
getContentViewParent()517     public ViewGroup getContentViewParent() {
518         return mContentViewParent;
519     }
520 
521     private static class SpringFloatValue {
522 
523         private static final FloatPropertyCompat<SpringFloatValue> VALUE =
524                 new FloatPropertyCompat<SpringFloatValue>("value") {
525                     @Override
526                     public float getValue(SpringFloatValue object) {
527                         return object.mValue;
528                     }
529 
530                     @Override
531                     public void setValue(SpringFloatValue object, float value) {
532                         object.mValue = value;
533                         object.mView.invalidate();
534                     }
535                 };
536 
537         // Following three values are fine tuned with motion ux designer
538         private static final int STIFFNESS = 4000;
539         private static final float DAMPENING_RATIO = 1f;
540         private static final int PARALLAX_MAX_IN_DP = 8;
541 
542         private final View mView;
543         private final SpringAnimation mSpring;
544         private final float mDelta;
545 
546         private float mValue;
547 
SpringFloatValue(View view, float range)548         public SpringFloatValue(View view, float range) {
549             mView = view;
550             mSpring = new SpringAnimation(this, VALUE, 0)
551                     .setMinValue(-range).setMaxValue(range)
552                     .setSpring(new SpringForce(0)
553                             .setDampingRatio(DAMPENING_RATIO)
554                             .setStiffness(STIFFNESS));
555             mDelta = view.getResources().getDisplayMetrics().density * PARALLAX_MAX_IN_DP;
556         }
557 
animateToPos(float value)558         public void animateToPos(float value) {
559             mSpring.animateToFinalPosition(Utilities.boundToRange(value, -mDelta, mDelta));
560         }
561     }
562 
getViewFromDrawable(Context context, Drawable drawable)563     private static View getViewFromDrawable(Context context, Drawable drawable) {
564         ImageView iv = new ImageView(context);
565         iv.setImageDrawable(drawable);
566         return iv;
567     }
568 
569     /**
570      * Removes any stray DragView from the DragLayer.
571      */
removeAllViews(ActivityContext activity)572     public static void removeAllViews(ActivityContext activity) {
573         BaseDragLayer dragLayer = activity.getDragLayer();
574         // Iterate in reverse order. DragView is added later to the dragLayer,
575         // and will be one of the last views.
576         for (int i = dragLayer.getChildCount() - 1; i >= 0; i--) {
577             View child = dragLayer.getChildAt(i);
578             if (child instanceof DragView) {
579                 dragLayer.removeView(child);
580             }
581         }
582     }
583 }
584