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