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