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