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