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