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