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