DragLayer.java revision ff4f201fadb131e7c367f64296e3a08090946ada
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 (FeatureFlags.LAUNCHER3_ALL_APPS_PULL_UP && 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            mCurrentResizeFrame.commitResize();
548            removeView(mCurrentResizeFrame);
549            mCurrentResizeFrame = null;
550        }
551    }
552
553    public void addResizeFrame(LauncherAppWidgetHostView widget, CellLayout cellLayout) {
554        clearResizeFrame();
555
556        mCurrentResizeFrame = (AppWidgetResizeFrame) LayoutInflater.from(mLauncher)
557                .inflate(R.layout.app_widget_resize_frame, this, false);
558        mCurrentResizeFrame.setupForWidget(widget, cellLayout, this);
559        ((LayoutParams) mCurrentResizeFrame.getLayoutParams()).customPosition = true;
560
561        addView(mCurrentResizeFrame);
562        mCurrentResizeFrame.snapToWidget(false);
563    }
564
565    public void animateViewIntoPosition(DragView dragView, final int[] pos, float alpha,
566            float scaleX, float scaleY, int animationEndStyle, Runnable onFinishRunnable,
567            int duration) {
568        Rect r = new Rect();
569        getViewRectRelativeToSelf(dragView, r);
570        final int fromX = r.left;
571        final int fromY = r.top;
572
573        animateViewIntoPosition(dragView, fromX, fromY, pos[0], pos[1], alpha, 1, 1, scaleX, scaleY,
574                onFinishRunnable, animationEndStyle, duration, null);
575    }
576
577    public void animateViewIntoPosition(DragView dragView, final View child,
578            final Runnable onFinishAnimationRunnable, View anchorView) {
579        animateViewIntoPosition(dragView, child, -1, onFinishAnimationRunnable, anchorView);
580    }
581
582    public void animateViewIntoPosition(DragView dragView, final View child, int duration,
583            final Runnable onFinishAnimationRunnable, View anchorView) {
584        ShortcutAndWidgetContainer parentChildren = (ShortcutAndWidgetContainer) child.getParent();
585        CellLayout.LayoutParams lp =  (CellLayout.LayoutParams) child.getLayoutParams();
586        parentChildren.measureChild(child);
587
588        Rect r = new Rect();
589        getViewRectRelativeToSelf(dragView, r);
590
591        int coord[] = new int[2];
592        float childScale = child.getScaleX();
593        coord[0] = lp.x + (int) (child.getMeasuredWidth() * (1 - childScale) / 2);
594        coord[1] = lp.y + (int) (child.getMeasuredHeight() * (1 - childScale) / 2);
595
596        // Since the child hasn't necessarily been laid out, we force the lp to be updated with
597        // the correct coordinates (above) and use these to determine the final location
598        float scale = getDescendantCoordRelativeToSelf((View) child.getParent(), coord);
599        // We need to account for the scale of the child itself, as the above only accounts for
600        // for the scale in parents.
601        scale *= childScale;
602        int toX = coord[0];
603        int toY = coord[1];
604        float toScale = scale;
605        if (child instanceof TextView) {
606            TextView tv = (TextView) child;
607            // Account for the source scale of the icon (ie. from AllApps to Workspace, in which
608            // the workspace may have smaller icon bounds).
609            toScale = scale / dragView.getIntrinsicIconScaleFactor();
610
611            // The child may be scaled (always about the center of the view) so to account for it,
612            // we have to offset the position by the scaled size.  Once we do that, we can center
613            // the drag view about the scaled child view.
614            toY += Math.round(toScale * tv.getPaddingTop());
615            toY -= dragView.getMeasuredHeight() * (1 - toScale) / 2;
616            if (dragView.getDragVisualizeOffset() != null) {
617                toY -=  Math.round(toScale * dragView.getDragVisualizeOffset().y);
618            }
619
620            toX -= (dragView.getMeasuredWidth() - Math.round(scale * child.getMeasuredWidth())) / 2;
621        } else if (child instanceof FolderIcon) {
622            // Account for holographic blur padding on the drag view
623            toY += Math.round(scale * (child.getPaddingTop() - dragView.getDragRegionTop()));
624            toY -= scale * dragView.getBlurSizeOutline() / 2;
625            toY -= (1 - scale) * dragView.getMeasuredHeight() / 2;
626            // Center in the x coordinate about the target's drawable
627            toX -= (dragView.getMeasuredWidth() - Math.round(scale * child.getMeasuredWidth())) / 2;
628        } else {
629            toY -= (Math.round(scale * (dragView.getHeight() - child.getMeasuredHeight()))) / 2;
630            toX -= (Math.round(scale * (dragView.getMeasuredWidth()
631                    - child.getMeasuredWidth()))) / 2;
632        }
633
634        final int fromX = r.left;
635        final int fromY = r.top;
636        child.setVisibility(INVISIBLE);
637        Runnable onCompleteRunnable = new Runnable() {
638            public void run() {
639                child.setVisibility(VISIBLE);
640                if (onFinishAnimationRunnable != null) {
641                    onFinishAnimationRunnable.run();
642                }
643            }
644        };
645        animateViewIntoPosition(dragView, fromX, fromY, toX, toY, 1, 1, 1, toScale, toScale,
646                onCompleteRunnable, ANIMATION_END_DISAPPEAR, duration, anchorView);
647    }
648
649    public void animateViewIntoPosition(final DragView view, final int fromX, final int fromY,
650            final int toX, final int toY, float finalAlpha, float initScaleX, float initScaleY,
651            float finalScaleX, float finalScaleY, Runnable onCompleteRunnable,
652            int animationEndStyle, int duration, View anchorView) {
653        Rect from = new Rect(fromX, fromY, fromX +
654                view.getMeasuredWidth(), fromY + view.getMeasuredHeight());
655        Rect to = new Rect(toX, toY, toX + view.getMeasuredWidth(), toY + view.getMeasuredHeight());
656        animateView(view, from, to, finalAlpha, initScaleX, initScaleY, finalScaleX, finalScaleY, duration,
657                null, null, onCompleteRunnable, animationEndStyle, anchorView);
658    }
659
660    /**
661     * This method animates a view at the end of a drag and drop animation.
662     *
663     * @param view The view to be animated. This view is drawn directly into DragLayer, and so
664     *        doesn't need to be a child of DragLayer.
665     * @param from The initial location of the view. Only the left and top parameters are used.
666     * @param to The final location of the view. Only the left and top parameters are used. This
667     *        location doesn't account for scaling, and so should be centered about the desired
668     *        final location (including scaling).
669     * @param finalAlpha The final alpha of the view, in case we want it to fade as it animates.
670     * @param finalScaleX The final scale of the view. The view is scaled about its center.
671     * @param finalScaleY The final scale of the view. The view is scaled about its center.
672     * @param duration The duration of the animation.
673     * @param motionInterpolator The interpolator to use for the location of the view.
674     * @param alphaInterpolator The interpolator to use for the alpha of the view.
675     * @param onCompleteRunnable Optional runnable to run on animation completion.
676     * @param animationEndStyle Whether or not to fade out the view once the animation completes.
677     *        {@link #ANIMATION_END_DISAPPEAR} or {@link #ANIMATION_END_REMAIN_VISIBLE}.
678     * @param anchorView If not null, this represents the view which the animated view stays
679     *        anchored to in case scrolling is currently taking place. Note: currently this is
680     *        only used for the X dimension for the case of the workspace.
681     */
682    public void animateView(final DragView view, final Rect from, final Rect to,
683            final float finalAlpha, final float initScaleX, final float initScaleY,
684            final float finalScaleX, final float finalScaleY, int duration,
685            final Interpolator motionInterpolator, final Interpolator alphaInterpolator,
686            final Runnable onCompleteRunnable, final int animationEndStyle, View anchorView) {
687
688        // Calculate the duration of the animation based on the object's distance
689        final float dist = (float) Math.hypot(to.left - from.left, to.top - from.top);
690        final Resources res = getResources();
691        final float maxDist = (float) res.getInteger(R.integer.config_dropAnimMaxDist);
692
693        // If duration < 0, this is a cue to compute the duration based on the distance
694        if (duration < 0) {
695            duration = res.getInteger(R.integer.config_dropAnimMaxDuration);
696            if (dist < maxDist) {
697                duration *= mCubicEaseOutInterpolator.getInterpolation(dist / maxDist);
698            }
699            duration = Math.max(duration, res.getInteger(R.integer.config_dropAnimMinDuration));
700        }
701
702        // Fall back to cubic ease out interpolator for the animation if none is specified
703        TimeInterpolator interpolator = null;
704        if (alphaInterpolator == null || motionInterpolator == null) {
705            interpolator = mCubicEaseOutInterpolator;
706        }
707
708        // Animate the view
709        final float initAlpha = view.getAlpha();
710        final float dropViewScale = view.getScaleX();
711        AnimatorUpdateListener updateCb = new AnimatorUpdateListener() {
712            @Override
713            public void onAnimationUpdate(ValueAnimator animation) {
714                final float percent = (Float) animation.getAnimatedValue();
715                final int width = view.getMeasuredWidth();
716                final int height = view.getMeasuredHeight();
717
718                float alphaPercent = alphaInterpolator == null ? percent :
719                        alphaInterpolator.getInterpolation(percent);
720                float motionPercent = motionInterpolator == null ? percent :
721                        motionInterpolator.getInterpolation(percent);
722
723                float initialScaleX = initScaleX * dropViewScale;
724                float initialScaleY = initScaleY * dropViewScale;
725                float scaleX = finalScaleX * percent + initialScaleX * (1 - percent);
726                float scaleY = finalScaleY * percent + initialScaleY * (1 - percent);
727                float alpha = finalAlpha * alphaPercent + initAlpha * (1 - alphaPercent);
728
729                float fromLeft = from.left + (initialScaleX - 1f) * width / 2;
730                float fromTop = from.top + (initialScaleY - 1f) * height / 2;
731
732                int x = (int) (fromLeft + Math.round(((to.left - fromLeft) * motionPercent)));
733                int y = (int) (fromTop + Math.round(((to.top - fromTop) * motionPercent)));
734
735                int anchorAdjust = mAnchorView == null ? 0 : (int) (mAnchorView.getScaleX() *
736                    (mAnchorViewInitialScrollX - mAnchorView.getScrollX()));
737
738                int xPos = x - mDropView.getScrollX() + anchorAdjust;
739                int yPos = y - mDropView.getScrollY();
740
741                mDropView.setTranslationX(xPos);
742                mDropView.setTranslationY(yPos);
743                mDropView.setScaleX(scaleX);
744                mDropView.setScaleY(scaleY);
745                mDropView.setAlpha(alpha);
746            }
747        };
748        animateView(view, updateCb, duration, interpolator, onCompleteRunnable, animationEndStyle,
749                anchorView);
750    }
751
752    public void animateView(final DragView view, AnimatorUpdateListener updateCb, int duration,
753            TimeInterpolator interpolator, final Runnable onCompleteRunnable,
754            final int animationEndStyle, View anchorView) {
755        // Clean up the previous animations
756        if (mDropAnim != null) mDropAnim.cancel();
757
758        // Show the drop view if it was previously hidden
759        mDropView = view;
760        mDropView.cancelAnimation();
761        mDropView.requestLayout();
762
763        // Set the anchor view if the page is scrolling
764        if (anchorView != null) {
765            mAnchorViewInitialScrollX = anchorView.getScrollX();
766        }
767        mAnchorView = anchorView;
768
769        // Create and start the animation
770        mDropAnim = new ValueAnimator();
771        mDropAnim.setInterpolator(interpolator);
772        mDropAnim.setDuration(duration);
773        mDropAnim.setFloatValues(0f, 1f);
774        mDropAnim.addUpdateListener(updateCb);
775        mDropAnim.addListener(new AnimatorListenerAdapter() {
776            public void onAnimationEnd(Animator animation) {
777                if (onCompleteRunnable != null) {
778                    onCompleteRunnable.run();
779                }
780                switch (animationEndStyle) {
781                case ANIMATION_END_DISAPPEAR:
782                    clearAnimatedView();
783                    break;
784                case ANIMATION_END_REMAIN_VISIBLE:
785                    break;
786                }
787            }
788        });
789        mDropAnim.start();
790    }
791
792    public void clearAnimatedView() {
793        if (mDropAnim != null) {
794            mDropAnim.cancel();
795        }
796        if (mDropView != null) {
797            mDragController.onDeferredEndDrag(mDropView);
798        }
799        mDropView = null;
800        invalidate();
801    }
802
803    public View getAnimatedView() {
804        return mDropView;
805    }
806
807    @Override
808    public void onChildViewAdded(View parent, View child) {
809        super.onChildViewAdded(parent, child);
810        updateChildIndices();
811    }
812
813    @Override
814    public void onChildViewRemoved(View parent, View child) {
815        updateChildIndices();
816    }
817
818    @Override
819    public void bringChildToFront(View child) {
820        super.bringChildToFront(child);
821        updateChildIndices();
822    }
823
824    private void updateChildIndices() {
825        mTopViewIndex = -1;
826        int childCount = getChildCount();
827        for (int i = 0; i < childCount; i++) {
828            if (getChildAt(i) instanceof DragView) {
829                mTopViewIndex = i;
830            }
831        }
832        mChildCountOnLastUpdate = childCount;
833    }
834
835    @Override
836    protected int getChildDrawingOrder(int childCount, int i) {
837        if (mChildCountOnLastUpdate != childCount) {
838            // between platform versions 17 and 18, behavior for onChildViewRemoved / Added changed.
839            // Pre-18, the child was not added / removed by the time of those callbacks. We need to
840            // force update our representation of things here to avoid crashing on pre-18 devices
841            // in certain instances.
842            updateChildIndices();
843        }
844
845        // i represents the current draw iteration
846        if (mTopViewIndex == -1) {
847            // in general we do nothing
848            return i;
849        } else if (i == childCount - 1) {
850            // if we have a top index, we return it when drawing last item (highest z-order)
851            return mTopViewIndex;
852        } else if (i < mTopViewIndex) {
853            return i;
854        } else {
855            // for indexes greater than the top index, we fetch one item above to shift for the
856            // displacement of the top index
857            return i + 1;
858        }
859    }
860
861    public void invalidateScrim() {
862        if (mBackgroundAlpha > 0.0f) {
863            invalidate();
864        }
865    }
866
867    @Override
868    protected void dispatchDraw(Canvas canvas) {
869        // Draw the background below children.
870        if (mBackgroundAlpha > 0.0f) {
871            // Update the scroll position first to ensure scrim cutout is in the right place.
872            mLauncher.getWorkspace().computeScrollWithoutInvalidation();
873
874            int alpha = (int) (mBackgroundAlpha * 255);
875            CellLayout currCellLayout = mLauncher.getWorkspace().getCurrentDragOverlappingLayout();
876            canvas.save();
877            if (currCellLayout != null && currCellLayout != mLauncher.getHotseat().getLayout()) {
878                // Cut a hole in the darkening scrim on the page that should be highlighted, if any.
879                getDescendantRectRelativeToSelf(currCellLayout, mHighlightRect);
880                canvas.clipRect(mHighlightRect, Region.Op.DIFFERENCE);
881            }
882            // for super light wallpaper it needs to be darken for contrast to workspace
883            // for dark wallpapers the text is white so darkening works as well
884            int color = ColorUtils.compositeColors(0x66000000, mWallpaperColorInfo.getMainColor());
885            canvas.drawColor(ColorUtils.setAlphaComponent(color, alpha));
886            canvas.restore();
887        }
888
889        mFocusIndicatorHelper.draw(canvas);
890        super.dispatchDraw(canvas);
891    }
892
893    public void setBackgroundAlpha(float alpha) {
894        if (alpha != mBackgroundAlpha) {
895            mBackgroundAlpha = alpha;
896            invalidate();
897        }
898    }
899
900    public float getBackgroundAlpha() {
901        return mBackgroundAlpha;
902    }
903
904    @Override
905    protected boolean onRequestFocusInDescendants(int direction, Rect previouslyFocusedRect) {
906        View topView = AbstractFloatingView.getTopOpenView(mLauncher);
907        if (topView != null) {
908            return topView.requestFocus(direction, previouslyFocusedRect);
909        } else {
910            return super.onRequestFocusInDescendants(direction, previouslyFocusedRect);
911        }
912    }
913
914    @Override
915    public void addFocusables(ArrayList<View> views, int direction, int focusableMode) {
916        View topView = AbstractFloatingView.getTopOpenView(mLauncher);
917        if (topView != null) {
918            topView.addFocusables(views, direction);
919        } else {
920            super.addFocusables(views, direction, focusableMode);
921        }
922    }
923
924    public void setTouchCompleteListener(TouchCompleteListener listener) {
925        mTouchCompleteListener = listener;
926    }
927
928    public interface TouchCompleteListener {
929        public void onTouchComplete();
930    }
931}
932