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