DragLayer.java revision 0f97b7874fe29f36e7a40d2fe4b45d1e0e24fe1b
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.TimeInterpolator;
22import android.animation.ValueAnimator;
23import android.animation.ValueAnimator.AnimatorUpdateListener;
24import android.content.Context;
25import android.content.res.Resources;
26import android.graphics.Canvas;
27import android.graphics.Color;
28import android.graphics.Rect;
29import android.graphics.drawable.Drawable;
30import android.util.AttributeSet;
31import android.view.DragEvent;
32import android.view.KeyEvent;
33import android.view.MotionEvent;
34import android.view.View;
35import android.view.ViewGroup;
36import android.view.accessibility.AccessibilityEvent;
37import android.view.accessibility.AccessibilityManager;
38import android.view.animation.DecelerateInterpolator;
39import android.view.animation.Interpolator;
40import android.widget.FrameLayout;
41import android.widget.TextView;
42
43import com.android.launcher3.AppWidgetResizeFrame;
44import com.android.launcher3.CellLayout;
45import com.android.launcher3.Folder;
46import com.android.launcher3.FolderIcon;
47import com.android.launcher3.InsettableFrameLayout;
48import com.android.launcher3.ItemInfo;
49import com.android.launcher3.Launcher;
50import com.android.launcher3.LauncherAppState;
51import com.android.launcher3.LauncherAppWidgetHostView;
52import com.android.launcher3.SearchDropTargetBar;
53import com.android.launcher3.ShortcutAndWidgetContainer;
54import com.android.launcher3.Utilities;
55import com.android.launcher3.Workspace;
56import com.android.launcher3.accessibility.LauncherAccessibilityDelegate;
57import com.android.launcher3.util.Thunk;
58
59import com.android.launcher3.R;
60
61import java.util.ArrayList;
62
63/**
64 * A ViewGroup that coordinates dragging across its descendants
65 */
66public class DragLayer extends InsettableFrameLayout {
67
68    public static final int ANIMATION_END_DISAPPEAR = 0;
69    public static final int ANIMATION_END_REMAIN_VISIBLE = 2;
70
71    // Scrim color without any alpha component.
72    private static final int SCRIM_COLOR = Color.BLACK & 0x00FFFFFF;
73
74    private final int[] mTmpXY = new int[2];
75
76    @Thunk DragController mDragController;
77
78    private int mXDown, mYDown;
79    private Launcher mLauncher;
80
81    // Variables relating to resizing widgets
82    private final ArrayList<AppWidgetResizeFrame> mResizeFrames = new ArrayList<>();
83    private final boolean mIsRtl;
84    private AppWidgetResizeFrame mCurrentResizeFrame;
85
86    // Variables relating to animation of views after drop
87    private ValueAnimator mDropAnim = null;
88    private final TimeInterpolator mCubicEaseOutInterpolator = new DecelerateInterpolator(1.5f);
89    @Thunk DragView mDropView = null;
90    @Thunk int mAnchorViewInitialScrollX = 0;
91    @Thunk View mAnchorView = null;
92
93    private boolean mHoverPointClosesFolder = false;
94    private final Rect mHitRect = new Rect();
95
96    private TouchCompleteListener mTouchCompleteListener;
97
98    private View mOverlayView;
99    private int mTopViewIndex;
100    private int mChildCountOnLastUpdate = -1;
101
102    // Darkening scrim
103    private float mBackgroundAlpha = 0;
104
105    // Related to adjacent page hints
106    private final Rect mScrollChildPosition = new Rect();
107    private boolean mInScrollArea;
108    private boolean mShowPageHints;
109    private Drawable mLeftHoverDrawable;
110    private Drawable mRightHoverDrawable;
111    private Drawable mLeftHoverDrawableActive;
112    private Drawable mRightHoverDrawableActive;
113
114    private boolean mBlockTouches = false;
115
116    /**
117     * Used to create a new DragLayer from XML.
118     *
119     * @param context The application's context.
120     * @param attrs The attributes set containing the Workspace's customization values.
121     */
122    public DragLayer(Context context, AttributeSet attrs) {
123        super(context, attrs);
124
125        // Disable multitouch across the workspace/all apps/customize tray
126        setMotionEventSplittingEnabled(false);
127        setChildrenDrawingOrderEnabled(true);
128
129        final Resources res = getResources();
130        mLeftHoverDrawable = res.getDrawable(R.drawable.page_hover_left);
131        mRightHoverDrawable = res.getDrawable(R.drawable.page_hover_right);
132        mLeftHoverDrawableActive = res.getDrawable(R.drawable.page_hover_left_active);
133        mRightHoverDrawableActive = res.getDrawable(R.drawable.page_hover_right_active);
134        mIsRtl = Utilities.isRtl(res);
135    }
136
137    public void setup(Launcher launcher, DragController controller) {
138        mLauncher = launcher;
139        mDragController = controller;
140    }
141
142    @Override
143    public boolean dispatchKeyEvent(KeyEvent event) {
144        return mDragController.dispatchKeyEvent(event) || super.dispatchKeyEvent(event);
145    }
146
147    public void showOverlayView(View overlayView) {
148        LayoutParams lp = new LayoutParams(LayoutParams.MATCH_PARENT, LayoutParams.MATCH_PARENT);
149        mOverlayView = overlayView;
150        addView(overlayView, lp);
151
152        // ensure that the overlay view stays on top. we can't use drawing order for this
153        // because in API level 16 touch dispatch doesn't respect drawing order.
154        mOverlayView.bringToFront();
155    }
156
157    public void dismissOverlayView() {
158        removeView(mOverlayView);
159    }
160
161    private boolean isEventOverFolderTextRegion(Folder folder, MotionEvent ev) {
162        getDescendantRectRelativeToSelf(folder.getEditTextRegion(), mHitRect);
163        if (mHitRect.contains((int) ev.getX(), (int) ev.getY())) {
164            return true;
165        }
166        return false;
167    }
168
169    private boolean isEventOverFolder(Folder folder, MotionEvent ev) {
170        getDescendantRectRelativeToSelf(folder, mHitRect);
171        if (mHitRect.contains((int) ev.getX(), (int) ev.getY())) {
172            return true;
173        }
174        return false;
175    }
176
177    private boolean isEventOverDropTargetBar(MotionEvent ev) {
178        getDescendantRectRelativeToSelf(mLauncher.getSearchDropTargetBar(), mHitRect);
179        if (mHitRect.contains((int) ev.getX(), (int) ev.getY())) {
180            return true;
181        }
182
183        getDescendantRectRelativeToSelf(mLauncher.getAppInfoDropTargetBar(), mHitRect);
184        if (mHitRect.contains((int) ev.getX(), (int) ev.getY())) {
185            return true;
186        }
187        return false;
188    }
189
190    public void setBlockTouch(boolean block) {
191        mBlockTouches = block;
192    }
193
194    private boolean handleTouchDown(MotionEvent ev, boolean intercept) {
195        Rect hitRect = new Rect();
196        int x = (int) ev.getX();
197        int y = (int) ev.getY();
198
199        if (mBlockTouches) {
200            return true;
201        }
202
203        for (AppWidgetResizeFrame child: mResizeFrames) {
204            child.getHitRect(hitRect);
205            if (hitRect.contains(x, y)) {
206                if (child.beginResizeIfPointInRegion(x - child.getLeft(), y - child.getTop())) {
207                    mCurrentResizeFrame = child;
208                    mXDown = x;
209                    mYDown = y;
210                    requestDisallowInterceptTouchEvent(true);
211                    return true;
212                }
213            }
214        }
215
216        Folder currentFolder = mLauncher.getWorkspace().getOpenFolder();
217        if (currentFolder != null && intercept) {
218            if (currentFolder.isEditingName()) {
219                if (!isEventOverFolderTextRegion(currentFolder, ev)) {
220                    currentFolder.dismissEditingName();
221                    return true;
222                }
223            }
224
225            if (!isEventOverFolder(currentFolder, ev)) {
226                if (isInAccessibleDrag()) {
227                    // Do not close the folder if in drag and drop.
228                    if (!isEventOverDropTargetBar(ev)) {
229                        return true;
230                    }
231                } else {
232                    mLauncher.closeFolder();
233                    return true;
234                }
235            }
236        }
237        return false;
238    }
239
240    @Override
241    public boolean onInterceptTouchEvent(MotionEvent ev) {
242        int action = ev.getAction();
243
244        if (action == MotionEvent.ACTION_DOWN) {
245            if (handleTouchDown(ev, true)) {
246                return true;
247            }
248        } else if (action == MotionEvent.ACTION_UP || action == MotionEvent.ACTION_CANCEL) {
249            if (mTouchCompleteListener != null) {
250                mTouchCompleteListener.onTouchComplete();
251            }
252            mTouchCompleteListener = null;
253        }
254        clearAllResizeFrames();
255        return mDragController.onInterceptTouchEvent(ev);
256    }
257
258    @Override
259    public boolean onInterceptHoverEvent(MotionEvent ev) {
260        if (mLauncher == null || mLauncher.getWorkspace() == null) {
261            return false;
262        }
263        Folder currentFolder = mLauncher.getWorkspace().getOpenFolder();
264        if (currentFolder == null) {
265            return false;
266        } else {
267                AccessibilityManager accessibilityManager = (AccessibilityManager)
268                        getContext().getSystemService(Context.ACCESSIBILITY_SERVICE);
269            if (accessibilityManager.isTouchExplorationEnabled()) {
270                final int action = ev.getAction();
271                boolean isOverFolderOrSearchBar;
272                switch (action) {
273                    case MotionEvent.ACTION_HOVER_ENTER:
274                        isOverFolderOrSearchBar = isEventOverFolder(currentFolder, ev) ||
275                            (isInAccessibleDrag() && isEventOverDropTargetBar(ev));
276                        if (!isOverFolderOrSearchBar) {
277                            sendTapOutsideFolderAccessibilityEvent(currentFolder.isEditingName());
278                            mHoverPointClosesFolder = true;
279                            return true;
280                        }
281                        mHoverPointClosesFolder = false;
282                        break;
283                    case MotionEvent.ACTION_HOVER_MOVE:
284                        isOverFolderOrSearchBar = isEventOverFolder(currentFolder, ev) ||
285                            (isInAccessibleDrag() && isEventOverDropTargetBar(ev));
286                        if (!isOverFolderOrSearchBar && !mHoverPointClosesFolder) {
287                            sendTapOutsideFolderAccessibilityEvent(currentFolder.isEditingName());
288                            mHoverPointClosesFolder = true;
289                            return true;
290                        } else if (!isOverFolderOrSearchBar) {
291                            return true;
292                        }
293                        mHoverPointClosesFolder = false;
294                }
295            }
296        }
297        return false;
298    }
299
300    private void sendTapOutsideFolderAccessibilityEvent(boolean isEditingName) {
301        AccessibilityManager accessibilityManager = (AccessibilityManager)
302                getContext().getSystemService(Context.ACCESSIBILITY_SERVICE);
303        if (accessibilityManager.isEnabled()) {
304            int stringId = isEditingName ? R.string.folder_tap_to_rename : R.string.folder_tap_to_close;
305            AccessibilityEvent event = AccessibilityEvent.obtain(
306                    AccessibilityEvent.TYPE_VIEW_FOCUSED);
307            onInitializeAccessibilityEvent(event);
308            event.getText().add(getContext().getString(stringId));
309            accessibilityManager.sendAccessibilityEvent(event);
310        }
311    }
312
313    private boolean isInAccessibleDrag() {
314        LauncherAccessibilityDelegate delegate = LauncherAppState
315                .getInstance().getAccessibilityDelegate();
316        return delegate != null && delegate.isInAccessibleDrag();
317    }
318
319    @Override
320    public boolean onRequestSendAccessibilityEvent(View child, AccessibilityEvent event) {
321        Folder currentFolder = mLauncher.getWorkspace().getOpenFolder();
322        if (currentFolder != null) {
323            if (child == currentFolder) {
324                return super.onRequestSendAccessibilityEvent(child, event);
325            }
326
327            if (isInAccessibleDrag() && child instanceof SearchDropTargetBar) {
328                return super.onRequestSendAccessibilityEvent(child, event);
329            }
330            // Skip propagating onRequestSendAccessibilityEvent all for other children
331            // when a folder is open
332            return false;
333        }
334        return super.onRequestSendAccessibilityEvent(child, event);
335    }
336
337    @Override
338    public void addChildrenForAccessibility(ArrayList<View> childrenForAccessibility) {
339        Folder currentFolder = mLauncher.getWorkspace().getOpenFolder();
340        if (currentFolder != null) {
341            // Only add the folder as a child for accessibility when it is open
342            childrenForAccessibility.add(currentFolder);
343
344            if (isInAccessibleDrag()) {
345                childrenForAccessibility.add(mLauncher.getSearchDropTargetBar());
346                childrenForAccessibility.add(mLauncher.getAppInfoDropTargetBar());
347            }
348        } else {
349            super.addChildrenForAccessibility(childrenForAccessibility);
350        }
351    }
352
353    @Override
354    public boolean onHoverEvent(MotionEvent ev) {
355        // If we've received this, we've already done the necessary handling
356        // in onInterceptHoverEvent. Return true to consume the event.
357        return false;
358    }
359
360    @Override
361    public boolean onTouchEvent(MotionEvent ev) {
362        boolean handled = false;
363        int action = ev.getAction();
364
365        int x = (int) ev.getX();
366        int y = (int) ev.getY();
367
368        if (mBlockTouches) {
369            return true;
370        }
371
372        if (action == MotionEvent.ACTION_DOWN) {
373            if (handleTouchDown(ev, false)) {
374                return true;
375            }
376        } else if (action == MotionEvent.ACTION_UP || action == MotionEvent.ACTION_CANCEL) {
377            if (mTouchCompleteListener != null) {
378                mTouchCompleteListener.onTouchComplete();
379            }
380            mTouchCompleteListener = null;
381        }
382
383        if (mCurrentResizeFrame != null) {
384            handled = true;
385            switch (action) {
386                case MotionEvent.ACTION_MOVE:
387                    mCurrentResizeFrame.visualizeResizeForDelta(x - mXDown, y - mYDown);
388                    break;
389                case MotionEvent.ACTION_CANCEL:
390                case MotionEvent.ACTION_UP:
391                    mCurrentResizeFrame.visualizeResizeForDelta(x - mXDown, y - mYDown);
392                    mCurrentResizeFrame.onTouchUp();
393                    mCurrentResizeFrame = null;
394            }
395        }
396        if (handled) return true;
397        return mDragController.onTouchEvent(ev);
398    }
399
400    @Override
401    public boolean onDragEvent (DragEvent event) {
402        return mDragController.onDragEvent(event);
403    }
404
405    /**
406     * Determine the rect of the descendant in this DragLayer's coordinates
407     *
408     * @param descendant The descendant whose coordinates we want to find.
409     * @param r The rect into which to place the results.
410     * @return The factor by which this descendant is scaled relative to this DragLayer.
411     */
412    public float getDescendantRectRelativeToSelf(View descendant, Rect r) {
413        mTmpXY[0] = 0;
414        mTmpXY[1] = 0;
415        float scale = getDescendantCoordRelativeToSelf(descendant, mTmpXY);
416
417        r.set(mTmpXY[0], mTmpXY[1],
418                (int) (mTmpXY[0] + scale * descendant.getMeasuredWidth()),
419                (int) (mTmpXY[1] + scale * descendant.getMeasuredHeight()));
420        return scale;
421    }
422
423    public float getLocationInDragLayer(View child, int[] loc) {
424        loc[0] = 0;
425        loc[1] = 0;
426        return getDescendantCoordRelativeToSelf(child, loc);
427    }
428
429    public float getDescendantCoordRelativeToSelf(View descendant, int[] coord) {
430        return getDescendantCoordRelativeToSelf(descendant, coord, false);
431    }
432
433    /**
434     * Given a coordinate relative to the descendant, find the coordinate in this DragLayer's
435     * coordinates.
436     *
437     * @param descendant The descendant to which the passed coordinate is relative.
438     * @param coord The coordinate that we want mapped.
439     * @param includeRootScroll Whether or not to account for the scroll of the root descendant:
440     *          sometimes this is relevant as in a child's coordinates within the root descendant.
441     * @return The factor by which this descendant is scaled relative to this DragLayer. Caution
442     *         this scale factor is assumed to be equal in X and Y, and so if at any point this
443     *         assumption fails, we will need to return a pair of scale factors.
444     */
445    public float getDescendantCoordRelativeToSelf(View descendant, int[] coord,
446            boolean includeRootScroll) {
447        return Utilities.getDescendantCoordRelativeToParent(descendant, this,
448                coord, includeRootScroll);
449    }
450
451    /**
452     * Inverse of {@link #getDescendantCoordRelativeToSelf(View, int[])}.
453     */
454    public float mapCoordInSelfToDescendent(View descendant, int[] coord) {
455        return Utilities.mapCoordInSelfToDescendent(descendant, this, coord);
456    }
457
458    public void getViewRectRelativeToSelf(View v, Rect r) {
459        int[] loc = new int[2];
460        getLocationInWindow(loc);
461        int x = loc[0];
462        int y = loc[1];
463
464        v.getLocationInWindow(loc);
465        int vX = loc[0];
466        int vY = loc[1];
467
468        int left = vX - x;
469        int top = vY - y;
470        r.set(left, top, left + v.getMeasuredWidth(), top + v.getMeasuredHeight());
471    }
472
473    @Override
474    public boolean dispatchUnhandledMove(View focused, int direction) {
475        return mDragController.dispatchUnhandledMove(focused, direction);
476    }
477
478    @Override
479    public LayoutParams generateLayoutParams(AttributeSet attrs) {
480        return new LayoutParams(getContext(), attrs);
481    }
482
483    @Override
484    protected LayoutParams generateDefaultLayoutParams() {
485        return new LayoutParams(LayoutParams.WRAP_CONTENT, LayoutParams.WRAP_CONTENT);
486    }
487
488    // Override to allow type-checking of LayoutParams.
489    @Override
490    protected boolean checkLayoutParams(ViewGroup.LayoutParams p) {
491        return p instanceof LayoutParams;
492    }
493
494    @Override
495    protected LayoutParams generateLayoutParams(ViewGroup.LayoutParams p) {
496        return new LayoutParams(p);
497    }
498
499    public static class LayoutParams extends InsettableFrameLayout.LayoutParams {
500        public int x, y;
501        public boolean customPosition = false;
502
503        public LayoutParams(Context c, AttributeSet attrs) {
504            super(c, attrs);
505        }
506
507        public LayoutParams(int width, int height) {
508            super(width, height);
509        }
510
511        public LayoutParams(ViewGroup.LayoutParams lp) {
512            super(lp);
513        }
514
515        public void setWidth(int width) {
516            this.width = width;
517        }
518
519        public int getWidth() {
520            return width;
521        }
522
523        public void setHeight(int height) {
524            this.height = height;
525        }
526
527        public int getHeight() {
528            return height;
529        }
530
531        public void setX(int x) {
532            this.x = x;
533        }
534
535        public int getX() {
536            return x;
537        }
538
539        public void setY(int y) {
540            this.y = y;
541        }
542
543        public int getY() {
544            return y;
545        }
546    }
547
548    protected void onLayout(boolean changed, int l, int t, int r, int b) {
549        super.onLayout(changed, l, t, r, b);
550        int count = getChildCount();
551        for (int i = 0; i < count; i++) {
552            View child = getChildAt(i);
553            final FrameLayout.LayoutParams flp = (FrameLayout.LayoutParams) child.getLayoutParams();
554            if (flp instanceof LayoutParams) {
555                final LayoutParams lp = (LayoutParams) flp;
556                if (lp.customPosition) {
557                    child.layout(lp.x, lp.y, lp.x + lp.width, lp.y + lp.height);
558                }
559            }
560        }
561    }
562
563    public void clearAllResizeFrames() {
564        if (mResizeFrames.size() > 0) {
565            for (AppWidgetResizeFrame frame: mResizeFrames) {
566                frame.commitResize();
567                removeView(frame);
568            }
569            mResizeFrames.clear();
570        }
571    }
572
573    public boolean hasResizeFrames() {
574        return mResizeFrames.size() > 0;
575    }
576
577    public boolean isWidgetBeingResized() {
578        return mCurrentResizeFrame != null;
579    }
580
581    public void addResizeFrame(ItemInfo itemInfo, LauncherAppWidgetHostView widget,
582            CellLayout cellLayout) {
583        AppWidgetResizeFrame resizeFrame = new AppWidgetResizeFrame(getContext(),
584                widget, cellLayout, this);
585
586        LayoutParams lp = new LayoutParams(-1, -1);
587        lp.customPosition = true;
588
589        addView(resizeFrame, lp);
590        mResizeFrames.add(resizeFrame);
591
592        resizeFrame.snapToWidget(false);
593    }
594
595    public void animateViewIntoPosition(DragView dragView, final int[] pos, float alpha,
596            float scaleX, float scaleY, int animationEndStyle, Runnable onFinishRunnable,
597            int duration) {
598        Rect r = new Rect();
599        getViewRectRelativeToSelf(dragView, r);
600        final int fromX = r.left;
601        final int fromY = r.top;
602
603        animateViewIntoPosition(dragView, fromX, fromY, pos[0], pos[1], alpha, 1, 1, scaleX, scaleY,
604                onFinishRunnable, animationEndStyle, duration, null);
605    }
606
607    public void animateViewIntoPosition(DragView dragView, final View child,
608            final Runnable onFinishAnimationRunnable, View anchorView) {
609        animateViewIntoPosition(dragView, child, -1, onFinishAnimationRunnable, anchorView);
610    }
611
612    public void animateViewIntoPosition(DragView dragView, final View child, int duration,
613            final Runnable onFinishAnimationRunnable, View anchorView) {
614        ShortcutAndWidgetContainer parentChildren = (ShortcutAndWidgetContainer) child.getParent();
615        CellLayout.LayoutParams lp =  (CellLayout.LayoutParams) child.getLayoutParams();
616        parentChildren.measureChild(child);
617
618        Rect r = new Rect();
619        getViewRectRelativeToSelf(dragView, r);
620
621        int coord[] = new int[2];
622        float childScale = child.getScaleX();
623        coord[0] = lp.x + (int) (child.getMeasuredWidth() * (1 - childScale) / 2);
624        coord[1] = lp.y + (int) (child.getMeasuredHeight() * (1 - childScale) / 2);
625
626        // Since the child hasn't necessarily been laid out, we force the lp to be updated with
627        // the correct coordinates (above) and use these to determine the final location
628        float scale = getDescendantCoordRelativeToSelf((View) child.getParent(), coord);
629        // We need to account for the scale of the child itself, as the above only accounts for
630        // for the scale in parents.
631        scale *= childScale;
632        int toX = coord[0];
633        int toY = coord[1];
634        float toScale = scale;
635        if (child instanceof TextView) {
636            TextView tv = (TextView) child;
637            // Account for the source scale of the icon (ie. from AllApps to Workspace, in which
638            // the workspace may have smaller icon bounds).
639            toScale = scale / dragView.getIntrinsicIconScaleFactor();
640
641            // The child may be scaled (always about the center of the view) so to account for it,
642            // we have to offset the position by the scaled size.  Once we do that, we can center
643            // the drag view about the scaled child view.
644            toY += Math.round(toScale * tv.getPaddingTop());
645            toY -= dragView.getMeasuredHeight() * (1 - toScale) / 2;
646            if (dragView.getDragVisualizeOffset() != null) {
647                toY -=  Math.round(toScale * dragView.getDragVisualizeOffset().y);
648            }
649
650            toX -= (dragView.getMeasuredWidth() - Math.round(scale * child.getMeasuredWidth())) / 2;
651        } else if (child instanceof FolderIcon) {
652            // Account for holographic blur padding on the drag view
653            toY += Math.round(scale * (child.getPaddingTop() - dragView.getDragRegionTop()));
654            toY -= scale * Workspace.DRAG_BITMAP_PADDING / 2;
655            toY -= (1 - scale) * dragView.getMeasuredHeight() / 2;
656            // Center in the x coordinate about the target's drawable
657            toX -= (dragView.getMeasuredWidth() - Math.round(scale * child.getMeasuredWidth())) / 2;
658        } else {
659            toY -= (Math.round(scale * (dragView.getHeight() - child.getMeasuredHeight()))) / 2;
660            toX -= (Math.round(scale * (dragView.getMeasuredWidth()
661                    - child.getMeasuredWidth()))) / 2;
662        }
663
664        final int fromX = r.left;
665        final int fromY = r.top;
666        child.setVisibility(INVISIBLE);
667        Runnable onCompleteRunnable = new Runnable() {
668            public void run() {
669                child.setVisibility(VISIBLE);
670                if (onFinishAnimationRunnable != null) {
671                    onFinishAnimationRunnable.run();
672                }
673            }
674        };
675        animateViewIntoPosition(dragView, fromX, fromY, toX, toY, 1, 1, 1, toScale, toScale,
676                onCompleteRunnable, ANIMATION_END_DISAPPEAR, duration, anchorView);
677    }
678
679    public void animateViewIntoPosition(final DragView view, final int fromX, final int fromY,
680            final int toX, final int toY, float finalAlpha, float initScaleX, float initScaleY,
681            float finalScaleX, float finalScaleY, Runnable onCompleteRunnable,
682            int animationEndStyle, int duration, View anchorView) {
683        Rect from = new Rect(fromX, fromY, fromX +
684                view.getMeasuredWidth(), fromY + view.getMeasuredHeight());
685        Rect to = new Rect(toX, toY, toX + view.getMeasuredWidth(), toY + view.getMeasuredHeight());
686        animateView(view, from, to, finalAlpha, initScaleX, initScaleY, finalScaleX, finalScaleY, duration,
687                null, null, onCompleteRunnable, animationEndStyle, anchorView);
688    }
689
690    /**
691     * This method animates a view at the end of a drag and drop animation.
692     *
693     * @param view The view to be animated. This view is drawn directly into DragLayer, and so
694     *        doesn't need to be a child of DragLayer.
695     * @param from The initial location of the view. Only the left and top parameters are used.
696     * @param to The final location of the view. Only the left and top parameters are used. This
697     *        location doesn't account for scaling, and so should be centered about the desired
698     *        final location (including scaling).
699     * @param finalAlpha The final alpha of the view, in case we want it to fade as it animates.
700     * @param finalScale The final scale of the view. The view is scaled about its center.
701     * @param duration The duration of the animation.
702     * @param motionInterpolator The interpolator to use for the location of the view.
703     * @param alphaInterpolator The interpolator to use for the alpha of the view.
704     * @param onCompleteRunnable Optional runnable to run on animation completion.
705     * @param fadeOut Whether or not to fade out the view once the animation completes. If true,
706     *        the runnable will execute after the view is faded out.
707     * @param anchorView If not null, this represents the view which the animated view stays
708     *        anchored to in case scrolling is currently taking place. Note: currently this is
709     *        only used for the X dimension for the case of the workspace.
710     */
711    public void animateView(final DragView view, final Rect from, final Rect to,
712            final float finalAlpha, final float initScaleX, final float initScaleY,
713            final float finalScaleX, final float finalScaleY, int duration,
714            final Interpolator motionInterpolator, final Interpolator alphaInterpolator,
715            final Runnable onCompleteRunnable, final int animationEndStyle, View anchorView) {
716
717        // Calculate the duration of the animation based on the object's distance
718        final float dist = (float) Math.hypot(to.left - from.left, to.top - from.top);
719        final Resources res = getResources();
720        final float maxDist = (float) res.getInteger(R.integer.config_dropAnimMaxDist);
721
722        // If duration < 0, this is a cue to compute the duration based on the distance
723        if (duration < 0) {
724            duration = res.getInteger(R.integer.config_dropAnimMaxDuration);
725            if (dist < maxDist) {
726                duration *= mCubicEaseOutInterpolator.getInterpolation(dist / maxDist);
727            }
728            duration = Math.max(duration, res.getInteger(R.integer.config_dropAnimMinDuration));
729        }
730
731        // Fall back to cubic ease out interpolator for the animation if none is specified
732        TimeInterpolator interpolator = null;
733        if (alphaInterpolator == null || motionInterpolator == null) {
734            interpolator = mCubicEaseOutInterpolator;
735        }
736
737        // Animate the view
738        final float initAlpha = view.getAlpha();
739        final float dropViewScale = view.getScaleX();
740        AnimatorUpdateListener updateCb = new AnimatorUpdateListener() {
741            @Override
742            public void onAnimationUpdate(ValueAnimator animation) {
743                final float percent = (Float) animation.getAnimatedValue();
744                final int width = view.getMeasuredWidth();
745                final int height = view.getMeasuredHeight();
746
747                float alphaPercent = alphaInterpolator == null ? percent :
748                        alphaInterpolator.getInterpolation(percent);
749                float motionPercent = motionInterpolator == null ? percent :
750                        motionInterpolator.getInterpolation(percent);
751
752                float initialScaleX = initScaleX * dropViewScale;
753                float initialScaleY = initScaleY * dropViewScale;
754                float scaleX = finalScaleX * percent + initialScaleX * (1 - percent);
755                float scaleY = finalScaleY * percent + initialScaleY * (1 - percent);
756                float alpha = finalAlpha * alphaPercent + initAlpha * (1 - alphaPercent);
757
758                float fromLeft = from.left + (initialScaleX - 1f) * width / 2;
759                float fromTop = from.top + (initialScaleY - 1f) * height / 2;
760
761                int x = (int) (fromLeft + Math.round(((to.left - fromLeft) * motionPercent)));
762                int y = (int) (fromTop + Math.round(((to.top - fromTop) * motionPercent)));
763
764                int anchorAdjust = mAnchorView == null ? 0 : (int) (mAnchorView.getScaleX() *
765                    (mAnchorViewInitialScrollX - mAnchorView.getScrollX()));
766
767                int xPos = x - mDropView.getScrollX() + anchorAdjust;
768                int yPos = y - mDropView.getScrollY();
769
770                mDropView.setTranslationX(xPos);
771                mDropView.setTranslationY(yPos);
772                mDropView.setScaleX(scaleX);
773                mDropView.setScaleY(scaleY);
774                mDropView.setAlpha(alpha);
775            }
776        };
777        animateView(view, updateCb, duration, interpolator, onCompleteRunnable, animationEndStyle,
778                anchorView);
779    }
780
781    public void animateView(final DragView view, AnimatorUpdateListener updateCb, int duration,
782            TimeInterpolator interpolator, final Runnable onCompleteRunnable,
783            final int animationEndStyle, View anchorView) {
784        // Clean up the previous animations
785        if (mDropAnim != null) mDropAnim.cancel();
786
787        // Show the drop view if it was previously hidden
788        mDropView = view;
789        mDropView.cancelAnimation();
790        mDropView.requestLayout();
791
792        // Set the anchor view if the page is scrolling
793        if (anchorView != null) {
794            mAnchorViewInitialScrollX = anchorView.getScrollX();
795        }
796        mAnchorView = anchorView;
797
798        // Create and start the animation
799        mDropAnim = new ValueAnimator();
800        mDropAnim.setInterpolator(interpolator);
801        mDropAnim.setDuration(duration);
802        mDropAnim.setFloatValues(0f, 1f);
803        mDropAnim.addUpdateListener(updateCb);
804        mDropAnim.addListener(new AnimatorListenerAdapter() {
805            public void onAnimationEnd(Animator animation) {
806                if (onCompleteRunnable != null) {
807                    onCompleteRunnable.run();
808                }
809                switch (animationEndStyle) {
810                case ANIMATION_END_DISAPPEAR:
811                    clearAnimatedView();
812                    break;
813                case ANIMATION_END_REMAIN_VISIBLE:
814                    break;
815                }
816            }
817        });
818        mDropAnim.start();
819    }
820
821    public void clearAnimatedView() {
822        if (mDropAnim != null) {
823            mDropAnim.cancel();
824        }
825        if (mDropView != null) {
826            mDragController.onDeferredEndDrag(mDropView);
827        }
828        mDropView = null;
829        invalidate();
830    }
831
832    public View getAnimatedView() {
833        return mDropView;
834    }
835
836    @Override
837    public void onChildViewAdded(View parent, View child) {
838        super.onChildViewAdded(parent, child);
839        if (mOverlayView != null) {
840            // ensure that the overlay view stays on top. we can't use drawing order for this
841            // because in API level 16 touch dispatch doesn't respect drawing order.
842            mOverlayView.bringToFront();
843        }
844        updateChildIndices();
845    }
846
847    @Override
848    public void onChildViewRemoved(View parent, View child) {
849        updateChildIndices();
850    }
851
852    @Override
853    public void bringChildToFront(View child) {
854        super.bringChildToFront(child);
855        if (child != mOverlayView && mOverlayView != null) {
856            // ensure that the overlay view stays on top. we can't use drawing order for this
857            // because in API level 16 touch dispatch doesn't respect drawing order.
858            mOverlayView.bringToFront();
859        }
860        updateChildIndices();
861    }
862
863    private void updateChildIndices() {
864        mTopViewIndex = -1;
865        int childCount = getChildCount();
866        for (int i = 0; i < childCount; i++) {
867            if (getChildAt(i) instanceof DragView) {
868                mTopViewIndex = i;
869            }
870        }
871        mChildCountOnLastUpdate = childCount;
872    }
873
874    @Override
875    protected int getChildDrawingOrder(int childCount, int i) {
876        if (mChildCountOnLastUpdate != childCount) {
877            // between platform versions 17 and 18, behavior for onChildViewRemoved / Added changed.
878            // Pre-18, the child was not added / removed by the time of those callbacks. We need to
879            // force update our representation of things here to avoid crashing on pre-18 devices
880            // in certain instances.
881            updateChildIndices();
882        }
883
884        // i represents the current draw iteration
885        if (mTopViewIndex == -1) {
886            // in general we do nothing
887            return i;
888        } else if (i == childCount - 1) {
889            // if we have a top index, we return it when drawing last item (highest z-order)
890            return mTopViewIndex;
891        } else if (i < mTopViewIndex) {
892            return i;
893        } else {
894            // for indexes greater than the top index, we fetch one item above to shift for the
895            // displacement of the top index
896            return i + 1;
897        }
898    }
899
900    void onEnterScrollArea(int direction) {
901        mInScrollArea = true;
902        invalidate();
903    }
904
905    void onExitScrollArea() {
906        mInScrollArea = false;
907        invalidate();
908    }
909
910    public void showPageHints() {
911        mShowPageHints = true;
912        Workspace workspace = mLauncher.getWorkspace();
913        getDescendantRectRelativeToSelf(workspace.getChildAt(workspace.numCustomPages()),
914                mScrollChildPosition);
915        invalidate();
916    }
917
918    public void hidePageHints() {
919        mShowPageHints = false;
920        invalidate();
921    }
922
923    @Override
924    protected void dispatchDraw(Canvas canvas) {
925        // Draw the background below children.
926        if (mBackgroundAlpha > 0.0f) {
927            int alpha = (int) (mBackgroundAlpha * 255);
928            canvas.drawColor((alpha << 24) | SCRIM_COLOR);
929        }
930
931        super.dispatchDraw(canvas);
932    }
933
934    private void drawPageHints(Canvas canvas) {
935        if (mShowPageHints) {
936            Workspace workspace = mLauncher.getWorkspace();
937            int width = getMeasuredWidth();
938            int page = workspace.getNextPage();
939            CellLayout leftPage = (CellLayout) workspace.getChildAt(mIsRtl ? page + 1 : page - 1);
940            CellLayout rightPage = (CellLayout) workspace.getChildAt(mIsRtl ? page - 1 : page + 1);
941
942            if (leftPage != null && leftPage.isDragTarget()) {
943                Drawable left = mInScrollArea && leftPage.getIsDragOverlapping() ?
944                        mLeftHoverDrawableActive : mLeftHoverDrawable;
945                left.setBounds(0, mScrollChildPosition.top,
946                        left.getIntrinsicWidth(), mScrollChildPosition.bottom);
947                left.draw(canvas);
948            }
949            if (rightPage != null && rightPage.isDragTarget()) {
950                Drawable right = mInScrollArea && rightPage.getIsDragOverlapping() ?
951                        mRightHoverDrawableActive : mRightHoverDrawable;
952                right.setBounds(width - right.getIntrinsicWidth(),
953                        mScrollChildPosition.top, width, mScrollChildPosition.bottom);
954                right.draw(canvas);
955            }
956        }
957    }
958
959    protected boolean drawChild(Canvas canvas, View child, long drawingTime) {
960        boolean ret = super.drawChild(canvas, child, drawingTime);
961
962        // We want to draw the page hints above the workspace, but below the drag view.
963        if (child instanceof Workspace) {
964            drawPageHints(canvas);
965        }
966        return ret;
967    }
968
969    public void setBackgroundAlpha(float alpha) {
970        if (alpha != mBackgroundAlpha) {
971            mBackgroundAlpha = alpha;
972            invalidate();
973        }
974    }
975
976    public float getBackgroundAlpha() {
977        return mBackgroundAlpha;
978    }
979
980    public void setTouchCompleteListener(TouchCompleteListener listener) {
981        mTouchCompleteListener = listener;
982    }
983
984    public interface TouchCompleteListener {
985        public void onTouchComplete();
986    }
987}
988