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