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