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