DragLayer.java revision 3371da0159cc54ff8ae1b1b26effb96445f208d5
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.launcher2;
18
19import android.animation.Animator;
20import android.animation.AnimatorListenerAdapter;
21import android.animation.ObjectAnimator;
22import android.animation.TimeInterpolator;
23import android.animation.ValueAnimator;
24import android.animation.ValueAnimator.AnimatorUpdateListener;
25import android.content.Context;
26import android.content.res.Resources;
27import android.graphics.Canvas;
28import android.graphics.Rect;
29import android.graphics.drawable.Drawable;
30import android.util.AttributeSet;
31import android.view.KeyEvent;
32import android.view.MotionEvent;
33import android.view.View;
34import android.view.ViewParent;
35import android.view.accessibility.AccessibilityEvent;
36import android.view.accessibility.AccessibilityManager;
37import android.view.animation.DecelerateInterpolator;
38import android.view.animation.Interpolator;
39import android.widget.FrameLayout;
40import android.widget.TextView;
41
42import com.android.launcher.R;
43
44import java.util.ArrayList;
45
46/**
47 * A ViewGroup that coordinates dragging across its descendants
48 */
49public class DragLayer extends FrameLayout {
50    private DragController mDragController;
51    private int[] mTmpXY = new int[2];
52
53    private int mXDown, mYDown;
54    private Launcher mLauncher;
55
56    // Variables relating to resizing widgets
57    private final ArrayList<AppWidgetResizeFrame> mResizeFrames =
58            new ArrayList<AppWidgetResizeFrame>();
59    private AppWidgetResizeFrame mCurrentResizeFrame;
60
61    // Variables relating to animation of views after drop
62    private ValueAnimator mDropAnim = null;
63    private ValueAnimator mFadeOutAnim = null;
64    private TimeInterpolator mCubicEaseOutInterpolator = new DecelerateInterpolator(1.5f);
65    private View mDropView = null;
66
67    private int[] mDropViewPos = new int[2];
68    private float mDropViewScale;
69    private float mDropViewAlpha;
70    private boolean mHoverPointClosesFolder = false;
71    private Rect mHitRect = new Rect();
72
73    /**
74     * Used to create a new DragLayer from XML.
75     *
76     * @param context The application's context.
77     * @param attrs The attributes set containing the Workspace's customization values.
78     */
79    public DragLayer(Context context, AttributeSet attrs) {
80        super(context, attrs);
81
82        // Disable multitouch across the workspace/all apps/customize tray
83        setMotionEventSplittingEnabled(false);
84    }
85
86    public void setup(Launcher launcher, DragController controller) {
87        mLauncher = launcher;
88        mDragController = controller;
89    }
90
91    @Override
92    public boolean dispatchKeyEvent(KeyEvent event) {
93        return mDragController.dispatchKeyEvent(event) || super.dispatchKeyEvent(event);
94    }
95
96    private boolean isEventOverFolderTextRegion(Folder folder, MotionEvent ev) {
97        getDescendantRectRelativeToSelf(folder.getEditTextRegion(), mHitRect);
98        if (mHitRect.contains((int) ev.getX(), (int) ev.getY())) {
99            return true;
100        }
101        return false;
102    }
103
104    private boolean isEventOverFolder(Folder folder, MotionEvent ev) {
105        getDescendantRectRelativeToSelf(folder, mHitRect);
106        if (mHitRect.contains((int) ev.getX(), (int) ev.getY())) {
107            return true;
108        }
109        return false;
110    }
111
112    private boolean handleTouchDown(MotionEvent ev, boolean intercept) {
113        Rect hitRect = new Rect();
114        int x = (int) ev.getX();
115        int y = (int) ev.getY();
116
117        for (AppWidgetResizeFrame child: mResizeFrames) {
118            child.getHitRect(hitRect);
119            if (hitRect.contains(x, y)) {
120                if (child.beginResizeIfPointInRegion(x - child.getLeft(), y - child.getTop())) {
121                    mCurrentResizeFrame = child;
122                    mXDown = x;
123                    mYDown = y;
124                    requestDisallowInterceptTouchEvent(true);
125                    return true;
126                }
127            }
128        }
129
130        Folder currentFolder = mLauncher.getWorkspace().getOpenFolder();
131        if (currentFolder != null && !mLauncher.isFolderClingVisible() && intercept) {
132            if (currentFolder.isEditingName()) {
133                if (!isEventOverFolderTextRegion(currentFolder, ev)) {
134                    currentFolder.dismissEditingName();
135                    return true;
136                }
137            }
138
139            getDescendantRectRelativeToSelf(currentFolder, hitRect);
140            if (!isEventOverFolder(currentFolder, ev)) {
141                mLauncher.closeFolder();
142                return true;
143            }
144        }
145        return false;
146    }
147
148    @Override
149    public boolean onInterceptTouchEvent(MotionEvent ev) {
150        if (ev.getAction() == MotionEvent.ACTION_DOWN) {
151            if (handleTouchDown(ev, true)) {
152                return true;
153            }
154        }
155        clearAllResizeFrames();
156        return mDragController.onInterceptTouchEvent(ev);
157    }
158
159    @Override
160    public boolean onInterceptHoverEvent(MotionEvent ev) {
161        Folder currentFolder = mLauncher.getWorkspace().getOpenFolder();
162        if (currentFolder == null) {
163            return false;
164        } else {
165            if (AccessibilityManager.getInstance(mContext).isTouchExplorationEnabled()) {
166                final int action = ev.getAction();
167                boolean isOverFolder;
168                switch (action) {
169                    case MotionEvent.ACTION_HOVER_ENTER:
170                        isOverFolder = isEventOverFolder(currentFolder, ev);
171                        if (!isOverFolder) {
172                            sendTapOutsideFolderAccessibilityEvent(currentFolder.isEditingName());
173                            mHoverPointClosesFolder = true;
174                            return true;
175                        } else if (isOverFolder) {
176                            mHoverPointClosesFolder = false;
177                        } else {
178                            return true;
179                        }
180                    case MotionEvent.ACTION_HOVER_MOVE:
181                        isOverFolder = isEventOverFolder(currentFolder, ev);
182                        if (!isOverFolder && !mHoverPointClosesFolder) {
183                            sendTapOutsideFolderAccessibilityEvent(currentFolder.isEditingName());
184                            mHoverPointClosesFolder = true;
185                            return true;
186                        } else if (isOverFolder) {
187                            mHoverPointClosesFolder = false;
188                        } else {
189                            return true;
190                        }
191                }
192            }
193        }
194        return false;
195    }
196
197    private void sendTapOutsideFolderAccessibilityEvent(boolean isEditingName) {
198        if (AccessibilityManager.getInstance(mContext).isEnabled()) {
199            Folder currentFolder = mLauncher.getWorkspace().getOpenFolder();
200            int stringId = isEditingName ? R.string.folder_tap_to_rename : R.string.folder_tap_to_close;
201            AccessibilityEvent event = AccessibilityEvent.obtain(
202                    AccessibilityEvent.TYPE_VIEW_FOCUSED);
203            onInitializeAccessibilityEvent(event);
204            event.getText().add(mContext.getString(stringId));
205            AccessibilityManager.getInstance(mContext).sendAccessibilityEvent(event);
206        }
207    }
208
209    @Override
210    public boolean onHoverEvent(MotionEvent ev) {
211        // If we've received this, we've already done the necessary handling
212        // in onInterceptHoverEvent. Return true to consume the event.
213        return false;
214    }
215
216    @Override
217    public boolean onTouchEvent(MotionEvent ev) {
218        boolean handled = false;
219        int action = ev.getAction();
220
221        int x = (int) ev.getX();
222        int y = (int) ev.getY();
223
224        if (ev.getAction() == MotionEvent.ACTION_DOWN) {
225            if (ev.getAction() == MotionEvent.ACTION_DOWN) {
226                if (handleTouchDown(ev, false)) {
227                    return true;
228                }
229            }
230        }
231
232        if (mCurrentResizeFrame != null) {
233            handled = true;
234            switch (action) {
235                case MotionEvent.ACTION_MOVE:
236                    mCurrentResizeFrame.visualizeResizeForDelta(x - mXDown, y - mYDown);
237                    break;
238                case MotionEvent.ACTION_CANCEL:
239                case MotionEvent.ACTION_UP:
240                    mCurrentResizeFrame.commitResizeForDelta(x - mXDown, y - mYDown);
241                    mCurrentResizeFrame = null;
242            }
243        }
244        if (handled) return true;
245        return mDragController.onTouchEvent(ev);
246    }
247
248    /**
249     * Determine the rect of the descendant in this DragLayer's coordinates
250     *
251     * @param descendant The descendant whose coordinates we want to find.
252     * @param r The rect into which to place the results.
253     * @return The factor by which this descendant is scaled relative to this DragLayer.
254     */
255    public float getDescendantRectRelativeToSelf(View descendant, Rect r) {
256        mTmpXY[0] = 0;
257        mTmpXY[1] = 0;
258        float scale = getDescendantCoordRelativeToSelf(descendant, mTmpXY);
259        r.set(mTmpXY[0], mTmpXY[1],
260                mTmpXY[0] + descendant.getWidth(), mTmpXY[1] + descendant.getHeight());
261        return scale;
262    }
263
264    public void getLocationInDragLayer(View child, int[] loc) {
265        loc[0] = 0;
266        loc[1] = 0;
267        getDescendantCoordRelativeToSelf(child, loc);
268    }
269
270    /**
271     * Given a coordinate relative to the descendant, find the coordinate in this DragLayer's
272     * coordinates.
273     *
274     * @param descendant The descendant to which the passed coordinate is relative.
275     * @param coord The coordinate that we want mapped.
276     * @return The factor by which this descendant is scaled relative to this DragLayer.
277     */
278    public float getDescendantCoordRelativeToSelf(View descendant, int[] coord) {
279        float scale = 1.0f;
280        float[] pt = {coord[0], coord[1]};
281        descendant.getMatrix().mapPoints(pt);
282        scale *= descendant.getScaleX();
283        pt[0] += descendant.getLeft();
284        pt[1] += descendant.getTop();
285        ViewParent viewParent = descendant.getParent();
286        while (viewParent instanceof View && viewParent != this) {
287            final View view = (View)viewParent;
288            view.getMatrix().mapPoints(pt);
289            scale *= view.getScaleX();
290            pt[0] += view.getLeft() - view.getScrollX();
291            pt[1] += view.getTop() - view.getScrollY();
292            viewParent = view.getParent();
293        }
294        coord[0] = (int) Math.round(pt[0]);
295        coord[1] = (int) Math.round(pt[1]);
296        return scale;
297    }
298
299    public void getViewRectRelativeToSelf(View v, Rect r) {
300        int[] loc = new int[2];
301        getLocationInWindow(loc);
302        int x = loc[0];
303        int y = loc[1];
304
305        v.getLocationInWindow(loc);
306        int vX = loc[0];
307        int vY = loc[1];
308
309        int left = vX - x;
310        int top = vY - y;
311        r.set(left, top, left + v.getMeasuredWidth(), top + v.getMeasuredHeight());
312    }
313
314    @Override
315    public boolean dispatchUnhandledMove(View focused, int direction) {
316        return mDragController.dispatchUnhandledMove(focused, direction);
317    }
318
319    public static class LayoutParams extends FrameLayout.LayoutParams {
320        public int x, y;
321        public boolean customPosition = false;
322
323        /**
324         * {@inheritDoc}
325         */
326        public LayoutParams(int width, int height) {
327            super(width, height);
328        }
329
330        public void setWidth(int width) {
331            this.width = width;
332        }
333
334        public int getWidth() {
335            return width;
336        }
337
338        public void setHeight(int height) {
339            this.height = height;
340        }
341
342        public int getHeight() {
343            return height;
344        }
345
346        public void setX(int x) {
347            this.x = x;
348        }
349
350        public int getX() {
351            return x;
352        }
353
354        public void setY(int y) {
355            this.y = y;
356        }
357
358        public int getY() {
359            return y;
360        }
361    }
362
363    protected void onLayout(boolean changed, int l, int t, int r, int b) {
364        super.onLayout(changed, l, t, r, b);
365        int count = getChildCount();
366        for (int i = 0; i < count; i++) {
367            View child = getChildAt(i);
368            final FrameLayout.LayoutParams flp = (FrameLayout.LayoutParams) child.getLayoutParams();
369            if (flp instanceof LayoutParams) {
370                final LayoutParams lp = (LayoutParams) flp;
371                if (lp.customPosition) {
372                    child.layout(lp.x, lp.y, lp.x + lp.width, lp.y + lp.height);
373                }
374            }
375        }
376    }
377
378    public void clearAllResizeFrames() {
379        if (mResizeFrames.size() > 0) {
380            for (AppWidgetResizeFrame frame: mResizeFrames) {
381                removeView(frame);
382            }
383            mResizeFrames.clear();
384        }
385    }
386
387    public boolean hasResizeFrames() {
388        return mResizeFrames.size() > 0;
389    }
390
391    public boolean isWidgetBeingResized() {
392        return mCurrentResizeFrame != null;
393    }
394
395    public void addResizeFrame(ItemInfo itemInfo, LauncherAppWidgetHostView widget,
396            CellLayout cellLayout) {
397        AppWidgetResizeFrame resizeFrame = new AppWidgetResizeFrame(getContext(),
398                itemInfo, widget, cellLayout, this);
399
400        LayoutParams lp = new LayoutParams(-1, -1);
401        lp.customPosition = true;
402
403        addView(resizeFrame, lp);
404        mResizeFrames.add(resizeFrame);
405
406        resizeFrame.snapToWidget(false);
407    }
408
409    public void animateViewIntoPosition(DragView dragView, final View child) {
410        animateViewIntoPosition(dragView, child, null);
411    }
412
413    public void animateViewIntoPosition(DragView dragView, final int[] pos, float scale,
414            Runnable onFinishRunnable) {
415        Rect r = new Rect();
416        getViewRectRelativeToSelf(dragView, r);
417        final int fromX = r.left;
418        final int fromY = r.top;
419
420        animateViewIntoPosition(dragView, fromX, fromY, pos[0], pos[1], scale,
421                onFinishRunnable, true, -1);
422    }
423
424    public void animateViewIntoPosition(DragView dragView, final View child,
425            final Runnable onFinishAnimationRunnable) {
426        animateViewIntoPosition(dragView, child, -1, onFinishAnimationRunnable);
427    }
428
429    public void animateViewIntoPosition(DragView dragView, final View child, int duration,
430            final Runnable onFinishAnimationRunnable) {
431        ((CellLayoutChildren) child.getParent()).measureChild(child);
432        CellLayout.LayoutParams lp =  (CellLayout.LayoutParams) child.getLayoutParams();
433
434        Rect r = new Rect();
435        getViewRectRelativeToSelf(dragView, r);
436
437        int coord[] = new int[2];
438        coord[0] = lp.x;
439        coord[1] = lp.y;
440        // Since the child hasn't necessarily been laid out, we force the lp to be updated with
441        // the correct coordinates (above) and use these to determine the final location
442        float scale = getDescendantCoordRelativeToSelf((View) child.getParent(), coord);
443        int toX = coord[0];
444        int toY = coord[1];
445        if (child instanceof TextView) {
446            TextView tv = (TextView) child;
447            Drawable d = tv.getCompoundDrawables()[1];
448
449            // Center in the y coordinate about the target's drawable
450            toY += Math.round(scale * tv.getPaddingTop());
451            toY -= (dragView.getHeight() - (int) Math.round(scale * d.getIntrinsicHeight())) / 2;
452            // Center in the x coordinate about the target's drawable
453            toX -= (dragView.getMeasuredWidth() - Math.round(scale * child.getMeasuredWidth())) / 2;
454        } else if (child instanceof FolderIcon) {
455            // Account for holographic blur padding on the drag view
456            toY -= HolographicOutlineHelper.MAX_OUTER_BLUR_RADIUS / 2;
457            // Center in the x coordinate about the target's drawable
458            toX -= (dragView.getMeasuredWidth() - Math.round(scale * child.getMeasuredWidth())) / 2;
459        } else {
460            toY -= (Math.round(scale * (dragView.getHeight() - child.getMeasuredHeight()))) / 2;
461            toX -= (Math.round(scale * (dragView.getMeasuredWidth()
462                    - child.getMeasuredWidth()))) / 2;
463        }
464
465        final int fromX = r.left;
466        final int fromY = r.top;
467        child.setVisibility(INVISIBLE);
468        child.setAlpha(0);
469        Runnable onCompleteRunnable = new Runnable() {
470            public void run() {
471                child.setVisibility(VISIBLE);
472                ObjectAnimator oa = ObjectAnimator.ofFloat(child, "alpha", 0f, 1f);
473                oa.setDuration(60);
474                oa.addListener(new AnimatorListenerAdapter() {
475                    @Override
476                    public void onAnimationEnd(android.animation.Animator animation) {
477                        if (onFinishAnimationRunnable != null) {
478                            onFinishAnimationRunnable.run();
479                        }
480                    }
481                });
482                oa.start();
483            }
484        };
485        animateViewIntoPosition(dragView, fromX, fromY, toX, toY, scale,
486                onCompleteRunnable, true, duration);
487    }
488
489    private void animateViewIntoPosition(final View view, final int fromX, final int fromY,
490            final int toX, final int toY, float finalScale, Runnable onCompleteRunnable,
491            boolean fadeOut, int duration) {
492        Rect from = new Rect(fromX, fromY, fromX +
493                view.getMeasuredWidth(), fromY + view.getMeasuredHeight());
494        Rect to = new Rect(toX, toY, toX + view.getMeasuredWidth(), toY + view.getMeasuredHeight());
495        animateView(view, from, to, 1f, finalScale, duration, null, null, onCompleteRunnable, true);
496    }
497
498    /**
499     * This method animates a view at the end of a drag and drop animation.
500     *
501     * @param view The view to be animated. This view is drawn directly into DragLayer, and so
502     *        doesn't need to be a child of DragLayer.
503     * @param from The initial location of the view. Only the left and top parameters are used.
504     * @param to The final location of the view. Only the left and top parameters are used. This
505     *        location doesn't account for scaling, and so should be centered about the desired
506     *        final location (including scaling).
507     * @param finalAlpha The final alpha of the view, in case we want it to fade as it animates.
508     * @param finalScale The final scale of the view. The view is scaled about its center.
509     * @param duration The duration of the animation.
510     * @param motionInterpolator The interpolator to use for the location of the view.
511     * @param alphaInterpolator The interpolator to use for the alpha of the view.
512     * @param onCompleteRunnable Optional runnable to run on animation completion.
513     * @param fadeOut Whether or not to fade out the view once the animation completes. If true,
514     *        the runnable will execute after the view is faded out.
515     */
516    public void animateView(final View view, final Rect from, final Rect to, final float finalAlpha,
517            final float finalScale, int duration, final Interpolator motionInterpolator,
518            final Interpolator alphaInterpolator, final Runnable onCompleteRunnable,
519            final boolean fadeOut) {
520        // Calculate the duration of the animation based on the object's distance
521        final float dist = (float) Math.sqrt(Math.pow(to.left - from.left, 2) +
522                Math.pow(to.top - from.top, 2));
523        final Resources res = getResources();
524        final float maxDist = (float) res.getInteger(R.integer.config_dropAnimMaxDist);
525
526        // If duration < 0, this is a cue to compute the duration based on the distance
527        if (duration < 0) {
528            duration = res.getInteger(R.integer.config_dropAnimMaxDuration);
529            if (dist < maxDist) {
530                duration *= mCubicEaseOutInterpolator.getInterpolation(dist / maxDist);
531            }
532        }
533
534        if (mDropAnim != null) {
535            mDropAnim.cancel();
536        }
537
538        if (mFadeOutAnim != null) {
539            mFadeOutAnim.cancel();
540        }
541
542        mDropView = view;
543        final float initialAlpha = view.getAlpha();
544        mDropAnim = new ValueAnimator();
545        if (alphaInterpolator == null || motionInterpolator == null) {
546            mDropAnim.setInterpolator(mCubicEaseOutInterpolator);
547        }
548
549        mDropAnim.setDuration(duration);
550        mDropAnim.setFloatValues(0.0f, 1.0f);
551        mDropAnim.removeAllUpdateListeners();
552        mDropAnim.addUpdateListener(new AnimatorUpdateListener() {
553            public void onAnimationUpdate(ValueAnimator animation) {
554                final float percent = (Float) animation.getAnimatedValue();
555                // Invalidate the old position
556                int width = view.getMeasuredWidth();
557                int height = view.getMeasuredHeight();
558                invalidate(mDropViewPos[0], mDropViewPos[1],
559                        mDropViewPos[0] + width, mDropViewPos[1] + height);
560
561                float alphaPercent = alphaInterpolator == null ? percent :
562                        alphaInterpolator.getInterpolation(percent);
563                float motionPercent = motionInterpolator == null ? percent :
564                        motionInterpolator.getInterpolation(percent);
565
566                mDropViewPos[0] = from.left + (int) Math.round(((to.left - from.left) * motionPercent));
567                mDropViewPos[1] = from.top + (int) Math.round(((to.top - from.top) * motionPercent));
568                mDropViewScale = percent * finalScale + (1 - percent);
569                mDropViewAlpha = alphaPercent * finalAlpha + (1 - alphaPercent) * initialAlpha;
570                invalidate(mDropViewPos[0], mDropViewPos[1],
571                        mDropViewPos[0] + width, mDropViewPos[1] + height);
572            }
573        });
574        mDropAnim.addListener(new AnimatorListenerAdapter() {
575            public void onAnimationEnd(Animator animation) {
576                if (onCompleteRunnable != null) {
577                    onCompleteRunnable.run();
578                }
579                if (fadeOut) {
580                    fadeOutDragView();
581                } else {
582                    mDropView = null;
583                }
584            }
585        });
586        mDropAnim.start();
587    }
588
589    private void fadeOutDragView() {
590        mFadeOutAnim = new ValueAnimator();
591        mFadeOutAnim.setDuration(150);
592        mFadeOutAnim.setFloatValues(0f, 1f);
593        mFadeOutAnim.removeAllUpdateListeners();
594        mFadeOutAnim.addUpdateListener(new AnimatorUpdateListener() {
595            public void onAnimationUpdate(ValueAnimator animation) {
596                final float percent = (Float) animation.getAnimatedValue();
597                mDropViewAlpha = 1 - percent;
598                int width = mDropView.getMeasuredWidth();
599                int height = mDropView.getMeasuredHeight();
600                invalidate(mDropViewPos[0], mDropViewPos[1],
601                        mDropViewPos[0] + width, mDropViewPos[1] + height);
602            }
603        });
604        mFadeOutAnim.addListener(new AnimatorListenerAdapter() {
605            public void onAnimationEnd(Animator animation) {
606                mDropView = null;
607            }
608        });
609        mFadeOutAnim.start();
610    }
611
612    @Override
613    protected void dispatchDraw(Canvas canvas) {
614        super.dispatchDraw(canvas);
615        if (mDropView != null) {
616            // We are animating an item that was just dropped on the home screen.
617            // Render its View in the current animation position.
618            canvas.save(Canvas.MATRIX_SAVE_FLAG);
619            final int xPos = mDropViewPos[0] - mDropView.getScrollX();
620            final int yPos = mDropViewPos[1] - mDropView.getScrollY();
621            int width = mDropView.getMeasuredWidth();
622            int height = mDropView.getMeasuredHeight();
623            canvas.translate(xPos, yPos);
624            canvas.translate((1 - mDropViewScale) * width / 2, (1 - mDropViewScale) * height / 2);
625            canvas.scale(mDropViewScale, mDropViewScale);
626            mDropView.setAlpha(mDropViewAlpha);
627            mDropView.draw(canvas);
628            canvas.restore();
629        }
630    }
631}
632