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