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