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