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;
18 
19 import android.animation.FloatArrayEvaluator;
20 import android.animation.ValueAnimator;
21 import android.animation.ValueAnimator.AnimatorUpdateListener;
22 import android.annotation.TargetApi;
23 import android.content.res.Resources;
24 import android.graphics.Bitmap;
25 import android.graphics.Canvas;
26 import android.graphics.Color;
27 import android.graphics.ColorMatrix;
28 import android.graphics.ColorMatrixColorFilter;
29 import android.graphics.Paint;
30 import android.graphics.Point;
31 import android.graphics.Rect;
32 import android.os.Build;
33 import android.view.View;
34 import android.view.animation.DecelerateInterpolator;
35 
36 import com.android.launcher3.util.Thunk;
37 
38 import java.util.Arrays;
39 
40 public class DragView extends View {
41     public static int COLOR_CHANGE_DURATION = 120;
42 
43     @Thunk static float sDragAlpha = 1f;
44 
45     private Bitmap mBitmap;
46     private Bitmap mCrossFadeBitmap;
47     @Thunk Paint mPaint;
48     private int mRegistrationX;
49     private int mRegistrationY;
50 
51     private Point mDragVisualizeOffset = null;
52     private Rect mDragRegion = null;
53     private DragLayer mDragLayer = null;
54     private boolean mHasDrawn = false;
55     @Thunk float mCrossFadeProgress = 0f;
56 
57     ValueAnimator mAnim;
58     @Thunk float mOffsetX = 0.0f;
59     @Thunk float mOffsetY = 0.0f;
60     private float mInitialScale = 1f;
61     // The intrinsic icon scale factor is the scale factor for a drag icon over the workspace
62     // size.  This is ignored for non-icons.
63     private float mIntrinsicIconScale = 1f;
64 
65     @Thunk float[] mCurrentFilter;
66     private ValueAnimator mFilterAnimator;
67 
68     /**
69      * Construct the drag view.
70      * <p>
71      * The registration point is the point inside our view that the touch events should
72      * be centered upon.
73      *
74      * @param launcher The Launcher instance
75      * @param bitmap The view that we're dragging around.  We scale it up when we draw it.
76      * @param registrationX The x coordinate of the registration point.
77      * @param registrationY The y coordinate of the registration point.
78      */
79     @TargetApi(Build.VERSION_CODES.LOLLIPOP)
DragView(Launcher launcher, Bitmap bitmap, int registrationX, int registrationY, int left, int top, int width, int height, final float initialScale)80     public DragView(Launcher launcher, Bitmap bitmap, int registrationX, int registrationY,
81             int left, int top, int width, int height, final float initialScale) {
82         super(launcher);
83         mDragLayer = launcher.getDragLayer();
84         mInitialScale = initialScale;
85 
86         final Resources res = getResources();
87         final float scaleDps = res.getDimensionPixelSize(R.dimen.dragViewScale);
88         final float scale = (width + scaleDps) / width;
89 
90         // Set the initial scale to avoid any jumps
91         setScaleX(initialScale);
92         setScaleY(initialScale);
93 
94         // Animate the view into the correct position
95         mAnim = LauncherAnimUtils.ofFloat(this, 0f, 1f);
96         mAnim.setDuration(150);
97         mAnim.addUpdateListener(new AnimatorUpdateListener() {
98             @Override
99             public void onAnimationUpdate(ValueAnimator animation) {
100                 final float value = (Float) animation.getAnimatedValue();
101 
102                 final int deltaX = (int) (-mOffsetX);
103                 final int deltaY = (int) (-mOffsetY);
104 
105                 mOffsetX += deltaX;
106                 mOffsetY += deltaY;
107                 setScaleX(initialScale + (value * (scale - initialScale)));
108                 setScaleY(initialScale + (value * (scale - initialScale)));
109                 if (sDragAlpha != 1f) {
110                     setAlpha(sDragAlpha * value + (1f - value));
111                 }
112 
113                 if (getParent() == null) {
114                     animation.cancel();
115                 } else {
116                     setTranslationX(getTranslationX() + deltaX);
117                     setTranslationY(getTranslationY() + deltaY);
118                 }
119             }
120         });
121 
122         mBitmap = Bitmap.createBitmap(bitmap, left, top, width, height);
123         setDragRegion(new Rect(0, 0, width, height));
124 
125         // The point in our scaled bitmap that the touch events are located
126         mRegistrationX = registrationX;
127         mRegistrationY = registrationY;
128 
129         // Force a measure, because Workspace uses getMeasuredHeight() before the layout pass
130         int ms = View.MeasureSpec.makeMeasureSpec(0, View.MeasureSpec.UNSPECIFIED);
131         measure(ms, ms);
132         mPaint = new Paint(Paint.FILTER_BITMAP_FLAG);
133 
134         if (Utilities.ATLEAST_LOLLIPOP) {
135             setElevation(getResources().getDimension(R.dimen.drag_elevation));
136         }
137     }
138 
139     /** Sets the scale of the view over the normal workspace icon size. */
setIntrinsicIconScaleFactor(float scale)140     public void setIntrinsicIconScaleFactor(float scale) {
141         mIntrinsicIconScale = scale;
142     }
143 
getIntrinsicIconScaleFactor()144     public float getIntrinsicIconScaleFactor() {
145         return mIntrinsicIconScale;
146     }
147 
getOffsetY()148     public float getOffsetY() {
149         return mOffsetY;
150     }
151 
getDragRegionLeft()152     public int getDragRegionLeft() {
153         return mDragRegion.left;
154     }
155 
getDragRegionTop()156     public int getDragRegionTop() {
157         return mDragRegion.top;
158     }
159 
getDragRegionWidth()160     public int getDragRegionWidth() {
161         return mDragRegion.width();
162     }
163 
getDragRegionHeight()164     public int getDragRegionHeight() {
165         return mDragRegion.height();
166     }
167 
setDragVisualizeOffset(Point p)168     public void setDragVisualizeOffset(Point p) {
169         mDragVisualizeOffset = p;
170     }
171 
getDragVisualizeOffset()172     public Point getDragVisualizeOffset() {
173         return mDragVisualizeOffset;
174     }
175 
setDragRegion(Rect r)176     public void setDragRegion(Rect r) {
177         mDragRegion = r;
178     }
179 
getDragRegion()180     public Rect getDragRegion() {
181         return mDragRegion;
182     }
183 
getInitialScale()184     public float getInitialScale() {
185         return mInitialScale;
186     }
187 
updateInitialScaleToCurrentScale()188     public void updateInitialScaleToCurrentScale() {
189         mInitialScale = getScaleX();
190     }
191 
192     @Override
onMeasure(int widthMeasureSpec, int heightMeasureSpec)193     protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
194         setMeasuredDimension(mBitmap.getWidth(), mBitmap.getHeight());
195     }
196 
197     @Override
onDraw(Canvas canvas)198     protected void onDraw(Canvas canvas) {
199         @SuppressWarnings("all") // suppress dead code warning
200         final boolean debug = false;
201         if (debug) {
202             Paint p = new Paint();
203             p.setStyle(Paint.Style.FILL);
204             p.setColor(0x66ffffff);
205             canvas.drawRect(0, 0, getWidth(), getHeight(), p);
206         }
207 
208         mHasDrawn = true;
209         boolean crossFade = mCrossFadeProgress > 0 && mCrossFadeBitmap != null;
210         if (crossFade) {
211             int alpha = crossFade ? (int) (255 * (1 - mCrossFadeProgress)) : 255;
212             mPaint.setAlpha(alpha);
213         }
214         canvas.drawBitmap(mBitmap, 0.0f, 0.0f, mPaint);
215         if (crossFade) {
216             mPaint.setAlpha((int) (255 * mCrossFadeProgress));
217             canvas.save();
218             float sX = (mBitmap.getWidth() * 1.0f) / mCrossFadeBitmap.getWidth();
219             float sY = (mBitmap.getHeight() * 1.0f) / mCrossFadeBitmap.getHeight();
220             canvas.scale(sX, sY);
221             canvas.drawBitmap(mCrossFadeBitmap, 0.0f, 0.0f, mPaint);
222             canvas.restore();
223         }
224     }
225 
setCrossFadeBitmap(Bitmap crossFadeBitmap)226     public void setCrossFadeBitmap(Bitmap crossFadeBitmap) {
227         mCrossFadeBitmap = crossFadeBitmap;
228     }
229 
crossFade(int duration)230     public void crossFade(int duration) {
231         ValueAnimator va = LauncherAnimUtils.ofFloat(this, 0f, 1f);
232         va.setDuration(duration);
233         va.setInterpolator(new DecelerateInterpolator(1.5f));
234         va.addUpdateListener(new AnimatorUpdateListener() {
235             @Override
236             public void onAnimationUpdate(ValueAnimator animation) {
237                 mCrossFadeProgress = animation.getAnimatedFraction();
238             }
239         });
240         va.start();
241     }
242 
setColor(int color)243     public void setColor(int color) {
244         if (mPaint == null) {
245             mPaint = new Paint(Paint.FILTER_BITMAP_FLAG);
246         }
247         if (color != 0) {
248             ColorMatrix m1 = new ColorMatrix();
249             m1.setSaturation(0);
250 
251             ColorMatrix m2 = new ColorMatrix();
252             setColorScale(color, m2);
253             m1.postConcat(m2);
254 
255             if (Utilities.ATLEAST_LOLLIPOP) {
256                 animateFilterTo(m1.getArray());
257             } else {
258                 mPaint.setColorFilter(new ColorMatrixColorFilter(m1));
259                 invalidate();
260             }
261         } else {
262             if (!Utilities.ATLEAST_LOLLIPOP || mCurrentFilter == null) {
263                 mPaint.setColorFilter(null);
264                 invalidate();
265             } else {
266                 animateFilterTo(new ColorMatrix().getArray());
267             }
268         }
269     }
270 
271     @TargetApi(Build.VERSION_CODES.LOLLIPOP)
animateFilterTo(float[] targetFilter)272     private void animateFilterTo(float[] targetFilter) {
273         float[] oldFilter = mCurrentFilter == null ? new ColorMatrix().getArray() : mCurrentFilter;
274         mCurrentFilter = Arrays.copyOf(oldFilter, oldFilter.length);
275 
276         if (mFilterAnimator != null) {
277             mFilterAnimator.cancel();
278         }
279         mFilterAnimator = ValueAnimator.ofObject(new FloatArrayEvaluator(mCurrentFilter),
280                 oldFilter, targetFilter);
281         mFilterAnimator.setDuration(COLOR_CHANGE_DURATION);
282         mFilterAnimator.addUpdateListener(new AnimatorUpdateListener() {
283 
284             @Override
285             public void onAnimationUpdate(ValueAnimator animation) {
286                 mPaint.setColorFilter(new ColorMatrixColorFilter(mCurrentFilter));
287                 invalidate();
288             }
289         });
290         mFilterAnimator.start();
291     }
292 
hasDrawn()293     public boolean hasDrawn() {
294         return mHasDrawn;
295     }
296 
297     @Override
setAlpha(float alpha)298     public void setAlpha(float alpha) {
299         super.setAlpha(alpha);
300         mPaint.setAlpha((int) (255 * alpha));
301         invalidate();
302     }
303 
304     /**
305      * Create a window containing this view and show it.
306      *
307      * @param windowToken obtained from v.getWindowToken() from one of your views
308      * @param touchX the x coordinate the user touched in DragLayer coordinates
309      * @param touchY the y coordinate the user touched in DragLayer coordinates
310      */
show(int touchX, int touchY)311     public void show(int touchX, int touchY) {
312         mDragLayer.addView(this);
313 
314         // Start the pick-up animation
315         DragLayer.LayoutParams lp = new DragLayer.LayoutParams(0, 0);
316         lp.width = mBitmap.getWidth();
317         lp.height = mBitmap.getHeight();
318         lp.customPosition = true;
319         setLayoutParams(lp);
320         setTranslationX(touchX - mRegistrationX);
321         setTranslationY(touchY - mRegistrationY);
322         // Post the animation to skip other expensive work happening on the first frame
323         post(new Runnable() {
324             public void run() {
325                 mAnim.start();
326             }
327         });
328     }
329 
cancelAnimation()330     public void cancelAnimation() {
331         if (mAnim != null && mAnim.isRunning()) {
332             mAnim.cancel();
333         }
334     }
335 
resetLayoutParams()336     public void resetLayoutParams() {
337         mOffsetX = mOffsetY = 0;
338         requestLayout();
339     }
340 
341     /**
342      * Move the window containing this view.
343      *
344      * @param touchX the x coordinate the user touched in DragLayer coordinates
345      * @param touchY the y coordinate the user touched in DragLayer coordinates
346      */
move(int touchX, int touchY)347     void move(int touchX, int touchY) {
348         setTranslationX(touchX - mRegistrationX + (int) mOffsetX);
349         setTranslationY(touchY - mRegistrationY + (int) mOffsetY);
350     }
351 
remove()352     void remove() {
353         if (getParent() != null) {
354             mDragLayer.removeView(DragView.this);
355         }
356     }
357 
setColorScale(int color, ColorMatrix target)358     public static void setColorScale(int color, ColorMatrix target) {
359         target.setScale(Color.red(color) / 255f, Color.green(color) / 255f,
360                 Color.blue(color) / 255f, Color.alpha(color) / 255f);
361     }
362 }
363