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