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