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