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