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 com.android.launcher3.Utilities.getBadge;
20 import static com.android.launcher3.util.Executors.MODEL_EXECUTOR;
21 
22 import android.animation.FloatArrayEvaluator;
23 import android.animation.ValueAnimator;
24 import android.animation.ValueAnimator.AnimatorUpdateListener;
25 import android.annotation.TargetApi;
26 import android.graphics.Bitmap;
27 import android.graphics.Canvas;
28 import android.graphics.Color;
29 import android.graphics.ColorMatrix;
30 import android.graphics.ColorMatrixColorFilter;
31 import android.graphics.Paint;
32 import android.graphics.Path;
33 import android.graphics.Point;
34 import android.graphics.Rect;
35 import android.graphics.drawable.AdaptiveIconDrawable;
36 import android.graphics.drawable.ColorDrawable;
37 import android.graphics.drawable.Drawable;
38 import android.os.Build;
39 import android.os.Handler;
40 import android.os.Looper;
41 import android.view.View;
42 
43 import androidx.dynamicanimation.animation.FloatPropertyCompat;
44 import androidx.dynamicanimation.animation.SpringAnimation;
45 import androidx.dynamicanimation.animation.SpringForce;
46 
47 import com.android.launcher3.FastBitmapDrawable;
48 import com.android.launcher3.FirstFrameAnimatorHelper;
49 import com.android.launcher3.Launcher;
50 import com.android.launcher3.LauncherSettings;
51 import com.android.launcher3.LauncherState;
52 import com.android.launcher3.R;
53 import com.android.launcher3.Utilities;
54 import com.android.launcher3.anim.Interpolators;
55 import com.android.launcher3.icons.LauncherIcons;
56 import com.android.launcher3.model.data.ItemInfo;
57 import com.android.launcher3.statemanager.StateManager.StateListener;
58 import com.android.launcher3.util.Themes;
59 import com.android.launcher3.util.Thunk;
60 
61 import java.util.Arrays;
62 
63 public class DragView extends View implements StateListener<LauncherState> {
64     private static final ColorMatrix sTempMatrix1 = new ColorMatrix();
65     private static final ColorMatrix sTempMatrix2 = new ColorMatrix();
66 
67     public static final int COLOR_CHANGE_DURATION = 120;
68     public static final int VIEW_ZOOM_DURATION = 150;
69 
70     private boolean mDrawBitmap = true;
71     private Bitmap mBitmap;
72     private Bitmap mCrossFadeBitmap;
73     @Thunk Paint mPaint;
74     private final int mBlurSizeOutline;
75     private final int mRegistrationX;
76     private final int mRegistrationY;
77     private final float mInitialScale;
78     private final float mScaleOnDrop;
79     private final int[] mTempLoc = new int[2];
80 
81     private Point mDragVisualizeOffset = null;
82     private Rect mDragRegion = null;
83     private final Launcher mLauncher;
84     private final DragLayer mDragLayer;
85     @Thunk final DragController mDragController;
86     final FirstFrameAnimatorHelper mFirstFrameAnimatorHelper;
87     private boolean mHasDrawn = false;
88     @Thunk float mCrossFadeProgress = 0f;
89     private boolean mAnimationCancelled = false;
90 
91     ValueAnimator mAnim;
92     // The intrinsic icon scale factor is the scale factor for a drag icon over the workspace
93     // size.  This is ignored for non-icons.
94     private float mIntrinsicIconScale = 1f;
95 
96     @Thunk float[] mCurrentFilter;
97     private ValueAnimator mFilterAnimator;
98 
99     private int mLastTouchX;
100     private int mLastTouchY;
101     private int mAnimatedShiftX;
102     private int mAnimatedShiftY;
103 
104     // Below variable only needed IF FeatureFlags.LAUNCHER3_SPRING_ICONS is {@code true}
105     private Drawable mBgSpringDrawable, mFgSpringDrawable;
106     private SpringFloatValue mTranslateX, mTranslateY;
107     private Path mScaledMaskPath;
108     private Drawable mBadge;
109     private ColorMatrixColorFilter mBaseFilter;
110 
111     /**
112      * Construct the drag view.
113      * <p>
114      * The registration point is the point inside our view that the touch events should
115      * be centered upon.
116      * @param launcher The Launcher instance
117      * @param bitmap The view that we're dragging around.  We scale it up when we draw it.
118      * @param registrationX The x coordinate of the registration point.
119      * @param registrationY The y coordinate of the registration point.
120      */
DragView(Launcher launcher, Bitmap bitmap, int registrationX, int registrationY, final float initialScale, final float scaleOnDrop, final float finalScaleDps)121     public DragView(Launcher launcher, Bitmap bitmap, int registrationX, int registrationY,
122                     final float initialScale, final float scaleOnDrop, final float finalScaleDps) {
123         super(launcher);
124         mLauncher = launcher;
125         mDragLayer = launcher.getDragLayer();
126         mDragController = launcher.getDragController();
127         mFirstFrameAnimatorHelper = new FirstFrameAnimatorHelper(this);
128 
129         final float scale = (bitmap.getWidth() + finalScaleDps) / bitmap.getWidth();
130 
131         // Set the initial scale to avoid any jumps
132         setScaleX(initialScale);
133         setScaleY(initialScale);
134 
135         // Animate the view into the correct position
136         mAnim = ValueAnimator.ofFloat(0f, 1f);
137         mAnim.setDuration(VIEW_ZOOM_DURATION);
138         mAnim.addUpdateListener(animation -> {
139             final float value = (Float) animation.getAnimatedValue();
140             setScaleX(initialScale + (value * (scale - initialScale)));
141             setScaleY(initialScale + (value * (scale - initialScale)));
142             if (!isAttachedToWindow()) {
143                 animation.cancel();
144             }
145         });
146 
147         mBitmap = bitmap;
148         setDragRegion(new Rect(0, 0, bitmap.getWidth(), bitmap.getHeight()));
149 
150         // The point in our scaled bitmap that the touch events are located
151         mRegistrationX = registrationX;
152         mRegistrationY = registrationY;
153 
154         mInitialScale = initialScale;
155         mScaleOnDrop = scaleOnDrop;
156 
157         // Force a measure, because Workspace uses getMeasuredHeight() before the layout pass
158         int ms = View.MeasureSpec.makeMeasureSpec(0, View.MeasureSpec.UNSPECIFIED);
159         measure(ms, ms);
160         mPaint = new Paint(Paint.FILTER_BITMAP_FLAG);
161 
162         mBlurSizeOutline = getResources().getDimensionPixelSize(R.dimen.blur_size_medium_outline);
163         setElevation(getResources().getDimension(R.dimen.drag_elevation));
164     }
165 
166     @Override
onAttachedToWindow()167     protected void onAttachedToWindow() {
168         super.onAttachedToWindow();
169         mLauncher.getStateManager().addStateListener(this);
170     }
171 
172     @Override
onDetachedFromWindow()173     protected void onDetachedFromWindow() {
174         super.onDetachedFromWindow();
175         mLauncher.getStateManager().removeStateListener(this);
176     }
177 
178     @Override
onStateTransitionComplete(LauncherState finalState)179     public void onStateTransitionComplete(LauncherState finalState) {
180         setVisibility((finalState == LauncherState.NORMAL
181                 || finalState == LauncherState.SPRING_LOADED) ? VISIBLE : INVISIBLE);
182     }
183 
184     /**
185      * Initialize {@code #mIconDrawable} if the item can be represented using
186      * an {@link AdaptiveIconDrawable} or {@link FolderAdaptiveIcon}.
187      */
188     @TargetApi(Build.VERSION_CODES.O)
setItemInfo(final ItemInfo info)189     public void setItemInfo(final ItemInfo info) {
190         if (!Utilities.ATLEAST_OREO) {
191             return;
192         }
193         if (info.itemType != LauncherSettings.Favorites.ITEM_TYPE_APPLICATION &&
194                 info.itemType != LauncherSettings.Favorites.ITEM_TYPE_DEEP_SHORTCUT &&
195                 info.itemType != LauncherSettings.Favorites.ITEM_TYPE_FOLDER) {
196             return;
197         }
198         // Load the adaptive icon on a background thread and add the view in ui thread.
199         MODEL_EXECUTOR.getHandler().postAtFrontOfQueue(new Runnable() {
200             @Override
201             public void run() {
202                 Object[] outObj = new Object[1];
203                 int w = mBitmap.getWidth();
204                 int h = mBitmap.getHeight();
205                 Drawable dr = Utilities.getFullDrawable(mLauncher, info, w, h, outObj);
206 
207                 if (dr instanceof AdaptiveIconDrawable) {
208                     int blurMargin = (int) mLauncher.getResources()
209                             .getDimension(R.dimen.blur_size_medium_outline) / 2;
210 
211                     Rect bounds = new Rect(0, 0, w, h);
212                     bounds.inset(blurMargin, blurMargin);
213                     // Badge is applied after icon normalization so the bounds for badge should not
214                     // be scaled down due to icon normalization.
215                     Rect badgeBounds = new Rect(bounds);
216                     mBadge = getBadge(mLauncher, info, outObj[0]);
217                     mBadge.setBounds(badgeBounds);
218 
219                     // Do not draw the background in case of folder as its translucent
220                     mDrawBitmap = !(dr instanceof FolderAdaptiveIcon);
221 
222                     try (LauncherIcons li = LauncherIcons.obtain(mLauncher)) {
223                         Drawable nDr; // drawable to be normalized
224                         if (mDrawBitmap) {
225                             nDr = dr;
226                         } else {
227                             // Since we just want the scale, avoid heavy drawing operations
228                             nDr = new AdaptiveIconDrawable(new ColorDrawable(Color.BLACK), null);
229                         }
230                         Utilities.scaleRectAboutCenter(bounds,
231                                 li.getNormalizer().getScale(nDr, null, null, null));
232                     }
233                     AdaptiveIconDrawable adaptiveIcon = (AdaptiveIconDrawable) dr;
234 
235                     // Shrink very tiny bit so that the clip path is smaller than the original bitmap
236                     // that has anti aliased edges and shadows.
237                     Rect shrunkBounds = new Rect(bounds);
238                     Utilities.scaleRectAboutCenter(shrunkBounds, 0.98f);
239                     adaptiveIcon.setBounds(shrunkBounds);
240                     final Path mask = adaptiveIcon.getIconMask();
241 
242                     mTranslateX = new SpringFloatValue(DragView.this,
243                             w * AdaptiveIconDrawable.getExtraInsetFraction());
244                     mTranslateY = new SpringFloatValue(DragView.this,
245                             h * AdaptiveIconDrawable.getExtraInsetFraction());
246 
247                     bounds.inset(
248                             (int) (-bounds.width() * AdaptiveIconDrawable.getExtraInsetFraction()),
249                             (int) (-bounds.height() * AdaptiveIconDrawable.getExtraInsetFraction())
250                     );
251                     mBgSpringDrawable = adaptiveIcon.getBackground();
252                     if (mBgSpringDrawable == null) {
253                         mBgSpringDrawable = new ColorDrawable(Color.TRANSPARENT);
254                     }
255                     mBgSpringDrawable.setBounds(bounds);
256                     mFgSpringDrawable = adaptiveIcon.getForeground();
257                     if (mFgSpringDrawable == null) {
258                         mFgSpringDrawable = new ColorDrawable(Color.TRANSPARENT);
259                     }
260                     mFgSpringDrawable.setBounds(bounds);
261 
262                     new Handler(Looper.getMainLooper()).post(new Runnable() {
263                         @Override
264                         public void run() {
265                             // Assign the variable on the UI thread to avoid race conditions.
266                             mScaledMaskPath = mask;
267 
268                             if (info.isDisabled()) {
269                                 FastBitmapDrawable d = new FastBitmapDrawable((Bitmap) null);
270                                 d.setIsDisabled(true);
271                                 mBaseFilter = (ColorMatrixColorFilter) d.getColorFilter();
272                             }
273                             updateColorFilter();
274                         }
275                     });
276                 }
277             }});
278     }
279 
280     @TargetApi(Build.VERSION_CODES.O)
updateColorFilter()281     private void updateColorFilter() {
282         if (mCurrentFilter == null) {
283             mPaint.setColorFilter(null);
284 
285             if (mScaledMaskPath != null) {
286                 mBgSpringDrawable.setColorFilter(mBaseFilter);
287                 mFgSpringDrawable.setColorFilter(mBaseFilter);
288                 mBadge.setColorFilter(mBaseFilter);
289             }
290         } else {
291             ColorMatrixColorFilter currentFilter = new ColorMatrixColorFilter(mCurrentFilter);
292             mPaint.setColorFilter(currentFilter);
293 
294             if (mScaledMaskPath != null) {
295                 if (mBaseFilter != null) {
296                     mBaseFilter.getColorMatrix(sTempMatrix1);
297                     sTempMatrix2.set(mCurrentFilter);
298                     sTempMatrix1.postConcat(sTempMatrix2);
299 
300                     currentFilter = new ColorMatrixColorFilter(sTempMatrix1);
301                 }
302 
303                 mBgSpringDrawable.setColorFilter(currentFilter);
304                 mFgSpringDrawable.setColorFilter(currentFilter);
305                 mBadge.setColorFilter(currentFilter);
306             }
307         }
308 
309         invalidate();
310     }
311 
312     @Override
onMeasure(int widthMeasureSpec, int heightMeasureSpec)313     protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
314         setMeasuredDimension(mBitmap.getWidth(), mBitmap.getHeight());
315     }
316 
317     /** Sets the scale of the view over the normal workspace icon size. */
setIntrinsicIconScaleFactor(float scale)318     public void setIntrinsicIconScaleFactor(float scale) {
319         mIntrinsicIconScale = scale;
320     }
321 
getIntrinsicIconScaleFactor()322     public float getIntrinsicIconScaleFactor() {
323         return mIntrinsicIconScale;
324     }
325 
getDragRegionLeft()326     public int getDragRegionLeft() {
327         return mDragRegion.left;
328     }
329 
getDragRegionTop()330     public int getDragRegionTop() {
331         return mDragRegion.top;
332     }
333 
getDragRegionWidth()334     public int getDragRegionWidth() {
335         return mDragRegion.width();
336     }
337 
getDragRegionHeight()338     public int getDragRegionHeight() {
339         return mDragRegion.height();
340     }
341 
setDragVisualizeOffset(Point p)342     public void setDragVisualizeOffset(Point p) {
343         mDragVisualizeOffset = p;
344     }
345 
getDragVisualizeOffset()346     public Point getDragVisualizeOffset() {
347         return mDragVisualizeOffset;
348     }
349 
setDragRegion(Rect r)350     public void setDragRegion(Rect r) {
351         mDragRegion = r;
352     }
353 
getDragRegion()354     public Rect getDragRegion() {
355         return mDragRegion;
356     }
357 
getPreviewBitmap()358     public Bitmap getPreviewBitmap() {
359         return mBitmap;
360     }
361 
362     @Override
onDraw(Canvas canvas)363     protected void onDraw(Canvas canvas) {
364         mHasDrawn = true;
365 
366         if (mDrawBitmap) {
367             // Always draw the bitmap to mask anti aliasing due to clipPath
368             boolean crossFade = mCrossFadeProgress > 0 && mCrossFadeBitmap != null;
369             if (crossFade) {
370                 int alpha = crossFade ? (int) (255 * (1 - mCrossFadeProgress)) : 255;
371                 mPaint.setAlpha(alpha);
372             }
373             canvas.drawBitmap(mBitmap, 0.0f, 0.0f, mPaint);
374             if (crossFade) {
375                 mPaint.setAlpha((int) (255 * mCrossFadeProgress));
376                 final int saveCount = canvas.save();
377                 float sX = (mBitmap.getWidth() * 1.0f) / mCrossFadeBitmap.getWidth();
378                 float sY = (mBitmap.getHeight() * 1.0f) / mCrossFadeBitmap.getHeight();
379                 canvas.scale(sX, sY);
380                 canvas.drawBitmap(mCrossFadeBitmap, 0.0f, 0.0f, mPaint);
381                 canvas.restoreToCount(saveCount);
382             }
383         }
384 
385         if (mScaledMaskPath != null) {
386             int cnt = canvas.save();
387             canvas.clipPath(mScaledMaskPath);
388             mBgSpringDrawable.draw(canvas);
389             canvas.translate(mTranslateX.mValue, mTranslateY.mValue);
390             mFgSpringDrawable.draw(canvas);
391             canvas.restoreToCount(cnt);
392             mBadge.draw(canvas);
393         }
394     }
395 
setCrossFadeBitmap(Bitmap crossFadeBitmap)396     public void setCrossFadeBitmap(Bitmap crossFadeBitmap) {
397         mCrossFadeBitmap = crossFadeBitmap;
398     }
399 
crossFade(int duration)400     public void crossFade(int duration) {
401         ValueAnimator va = ValueAnimator.ofFloat(0f, 1f);
402         va.setDuration(duration);
403         va.setInterpolator(Interpolators.DEACCEL_1_5);
404         va.addUpdateListener(a -> {
405             mCrossFadeProgress = a.getAnimatedFraction();
406             invalidate();
407         });
408         va.start();
409     }
410 
setColor(int color)411     public void setColor(int color) {
412         if (mPaint == null) {
413             mPaint = new Paint(Paint.FILTER_BITMAP_FLAG);
414         }
415         if (color != 0) {
416             ColorMatrix m1 = new ColorMatrix();
417             m1.setSaturation(0);
418 
419             ColorMatrix m2 = new ColorMatrix();
420             Themes.setColorScaleOnMatrix(color, m2);
421             m1.postConcat(m2);
422 
423             animateFilterTo(m1.getArray());
424         } else {
425             if (mCurrentFilter == null) {
426                 updateColorFilter();
427             } else {
428                 animateFilterTo(new ColorMatrix().getArray());
429             }
430         }
431     }
432 
animateFilterTo(float[] targetFilter)433     private void animateFilterTo(float[] targetFilter) {
434         float[] oldFilter = mCurrentFilter == null ? new ColorMatrix().getArray() : mCurrentFilter;
435         mCurrentFilter = Arrays.copyOf(oldFilter, oldFilter.length);
436 
437         if (mFilterAnimator != null) {
438             mFilterAnimator.cancel();
439         }
440         mFilterAnimator = ValueAnimator.ofObject(new FloatArrayEvaluator(mCurrentFilter),
441                 oldFilter, targetFilter);
442         mFilterAnimator.setDuration(COLOR_CHANGE_DURATION);
443         mFilterAnimator.addUpdateListener(new AnimatorUpdateListener() {
444 
445             @Override
446             public void onAnimationUpdate(ValueAnimator animation) {
447                 updateColorFilter();
448             }
449         });
450         mFilterAnimator.start();
451     }
452 
hasDrawn()453     public boolean hasDrawn() {
454         return mHasDrawn;
455     }
456 
457     @Override
setAlpha(float alpha)458     public void setAlpha(float alpha) {
459         super.setAlpha(alpha);
460         mPaint.setAlpha((int) (255 * alpha));
461         invalidate();
462     }
463 
464     /**
465      * Create a window containing this view and show it.
466      *
467      * @param touchX the x coordinate the user touched in DragLayer coordinates
468      * @param touchY the y coordinate the user touched in DragLayer coordinates
469      */
show(int touchX, int touchY)470     public void show(int touchX, int touchY) {
471         mDragLayer.addView(this);
472 
473         // Start the pick-up animation
474         DragLayer.LayoutParams lp = new DragLayer.LayoutParams(0, 0);
475         lp.width = mBitmap.getWidth();
476         lp.height = mBitmap.getHeight();
477         lp.customPosition = true;
478         setLayoutParams(lp);
479         move(touchX, touchY);
480         // Post the animation to skip other expensive work happening on the first frame
481         post(new Runnable() {
482             public void run() {
483                 mAnim.start();
484             }
485         });
486     }
487 
cancelAnimation()488     public void cancelAnimation() {
489         mAnimationCancelled = true;
490         if (mAnim != null && mAnim.isRunning()) {
491             mAnim.cancel();
492         }
493     }
494 
495     /**
496      * Move the window containing this view.
497      *
498      * @param touchX the x coordinate the user touched in DragLayer coordinates
499      * @param touchY the y coordinate the user touched in DragLayer coordinates
500      */
move(int touchX, int touchY)501     public void move(int touchX, int touchY) {
502         if (touchX > 0 && touchY > 0 && mLastTouchX > 0 && mLastTouchY > 0
503                 && mScaledMaskPath != null) {
504             mTranslateX.animateToPos(mLastTouchX - touchX);
505             mTranslateY.animateToPos(mLastTouchY - touchY);
506         }
507         mLastTouchX = touchX;
508         mLastTouchY = touchY;
509         applyTranslation();
510     }
511 
animateTo(int toTouchX, int toTouchY, Runnable onCompleteRunnable, int duration)512     public void animateTo(int toTouchX, int toTouchY, Runnable onCompleteRunnable, int duration) {
513         mTempLoc[0] = toTouchX - mRegistrationX;
514         mTempLoc[1] = toTouchY - mRegistrationY;
515         mDragLayer.animateViewIntoPosition(this, mTempLoc, 1f, mScaleOnDrop, mScaleOnDrop,
516                 DragLayer.ANIMATION_END_DISAPPEAR, onCompleteRunnable, duration);
517     }
518 
animateShift(final int shiftX, final int shiftY)519     public void animateShift(final int shiftX, final int shiftY) {
520         if (mAnim.isStarted()) {
521             return;
522         }
523         mAnimatedShiftX = shiftX;
524         mAnimatedShiftY = shiftY;
525         applyTranslation();
526         mAnim.addUpdateListener(new AnimatorUpdateListener() {
527             @Override
528             public void onAnimationUpdate(ValueAnimator animation) {
529                 float fraction = 1 - animation.getAnimatedFraction();
530                 mAnimatedShiftX = (int) (fraction * shiftX);
531                 mAnimatedShiftY = (int) (fraction * shiftY);
532                 applyTranslation();
533             }
534         });
535     }
536 
applyTranslation()537     private void applyTranslation() {
538         setTranslationX(mLastTouchX - mRegistrationX + mAnimatedShiftX);
539         setTranslationY(mLastTouchY - mRegistrationY + mAnimatedShiftY);
540     }
541 
remove()542     public void remove() {
543         if (getParent() != null) {
544             mDragLayer.removeView(DragView.this);
545         }
546     }
547 
getBlurSizeOutline()548     public int getBlurSizeOutline() {
549         return mBlurSizeOutline;
550     }
551 
getInitialScale()552     public float getInitialScale() {
553         return mInitialScale;
554     }
555 
556     private static class SpringFloatValue {
557 
558         private static final FloatPropertyCompat<SpringFloatValue> VALUE =
559                 new FloatPropertyCompat<SpringFloatValue>("value") {
560                     @Override
561                     public float getValue(SpringFloatValue object) {
562                         return object.mValue;
563                     }
564 
565                     @Override
566                     public void setValue(SpringFloatValue object, float value) {
567                         object.mValue = value;
568                         object.mView.invalidate();
569                     }
570                 };
571 
572         // Following three values are fine tuned with motion ux designer
573         private final static int STIFFNESS = 4000;
574         private final static float DAMPENING_RATIO = 1f;
575         private final static int PARALLAX_MAX_IN_DP = 8;
576 
577         private final View mView;
578         private final SpringAnimation mSpring;
579         private final float mDelta;
580 
581         private float mValue;
582 
SpringFloatValue(View view, float range)583         public SpringFloatValue(View view, float range) {
584             mView = view;
585             mSpring = new SpringAnimation(this, VALUE, 0)
586                     .setMinValue(-range).setMaxValue(range)
587                     .setSpring(new SpringForce(0)
588                             .setDampingRatio(DAMPENING_RATIO)
589                             .setStiffness(STIFFNESS));
590             mDelta = view.getResources().getDisplayMetrics().density * PARALLAX_MAX_IN_DP;
591         }
592 
animateToPos(float value)593         public void animateToPos(float value) {
594             mSpring.animateToFinalPosition(Utilities.boundToRange(value, -mDelta, mDelta));
595         }
596     }
597 }
598