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