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