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