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