• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1 
2 /*
3  * Copyright (C) 2008 The Android Open Source Project
4  *
5  * Licensed under the Apache License, Version 2.0 (the "License");
6  * you may not use this file except in compliance with the License.
7  * You may obtain a copy of the License at
8  *
9  *      http://www.apache.org/licenses/LICENSE-2.0
10  *
11  * Unless required by applicable law or agreed to in writing, software
12  * distributed under the License is distributed on an "AS IS" BASIS,
13  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14  * See the License for the specific language governing permissions and
15  * limitations under the License.
16  */
17 
18 package com.android.launcher3.dragndrop;
19 
20 import static com.android.launcher3.anim.Interpolators.DEACCEL_1_5;
21 import static com.android.launcher3.compat.AccessibilityManagerCompat.sendCustomAccessibilityEvent;
22 
23 import android.animation.Animator;
24 import android.animation.AnimatorListenerAdapter;
25 import android.animation.TimeInterpolator;
26 import android.animation.ValueAnimator;
27 import android.animation.ValueAnimator.AnimatorUpdateListener;
28 import android.content.Context;
29 import android.content.res.Resources;
30 import android.graphics.Canvas;
31 import android.graphics.Rect;
32 import android.util.AttributeSet;
33 import android.view.KeyEvent;
34 import android.view.MotionEvent;
35 import android.view.View;
36 import android.view.accessibility.AccessibilityEvent;
37 import android.view.accessibility.AccessibilityManager;
38 import android.view.animation.Interpolator;
39 
40 import com.android.launcher3.AbstractFloatingView;
41 import com.android.launcher3.CellLayout;
42 import com.android.launcher3.DropTargetBar;
43 import com.android.launcher3.Launcher;
44 import com.android.launcher3.R;
45 import com.android.launcher3.ShortcutAndWidgetContainer;
46 import com.android.launcher3.Workspace;
47 import com.android.launcher3.folder.Folder;
48 import com.android.launcher3.graphics.Scrim;
49 import com.android.launcher3.keyboard.ViewGroupFocusHelper;
50 import com.android.launcher3.util.Thunk;
51 import com.android.launcher3.util.TouchController;
52 import com.android.launcher3.views.BaseDragLayer;
53 
54 import java.util.ArrayList;
55 
56 /**
57  * A ViewGroup that coordinates dragging across its descendants
58  */
59 public class DragLayer extends BaseDragLayer<Launcher> {
60 
61     public static final int ALPHA_INDEX_OVERLAY = 0;
62     public static final int ALPHA_INDEX_LAUNCHER_LOAD = 1;
63     public static final int ALPHA_INDEX_TRANSITIONS = 2;
64     private static final int ALPHA_CHANNEL_COUNT = 3;
65 
66     public static final int ANIMATION_END_DISAPPEAR = 0;
67     public static final int ANIMATION_END_REMAIN_VISIBLE = 2;
68 
69     private DragController mDragController;
70 
71     // Variables relating to animation of views after drop
72     private ValueAnimator mDropAnim = null;
73 
74     @Thunk DragView mDropView = null;
75     @Thunk int mAnchorViewInitialScrollX = 0;
76     @Thunk View mAnchorView = null;
77 
78     private boolean mHoverPointClosesFolder = false;
79 
80     private int mTopViewIndex;
81     private int mChildCountOnLastUpdate = -1;
82 
83     // Related to adjacent page hints
84     private final ViewGroupFocusHelper mFocusIndicatorHelper;
85     private Scrim mWorkspaceDragScrim;
86 
87     /**
88      * Used to create a new DragLayer from XML.
89      *
90      * @param context The application's context.
91      * @param attrs The attributes set containing the Workspace's customization values.
92      */
DragLayer(Context context, AttributeSet attrs)93     public DragLayer(Context context, AttributeSet attrs) {
94         super(context, attrs, ALPHA_CHANNEL_COUNT);
95 
96         // Disable multitouch across the workspace/all apps/customize tray
97         setMotionEventSplittingEnabled(false);
98         setChildrenDrawingOrderEnabled(true);
99 
100         mFocusIndicatorHelper = new ViewGroupFocusHelper(this);
101     }
102 
setup(DragController dragController, Workspace workspace)103     public void setup(DragController dragController, Workspace workspace) {
104         mDragController = dragController;
105         recreateControllers();
106         mWorkspaceDragScrim = new Scrim(this);
107     }
108 
109     @Override
recreateControllers()110     public void recreateControllers() {
111         mControllers = mActivity.createTouchControllers();
112     }
113 
getFocusIndicatorHelper()114     public ViewGroupFocusHelper getFocusIndicatorHelper() {
115         return mFocusIndicatorHelper;
116     }
117 
118     @Override
dispatchKeyEvent(KeyEvent event)119     public boolean dispatchKeyEvent(KeyEvent event) {
120         return mDragController.dispatchKeyEvent(event) || super.dispatchKeyEvent(event);
121     }
122 
isEventOverAccessibleDropTargetBar(MotionEvent ev)123     private boolean isEventOverAccessibleDropTargetBar(MotionEvent ev) {
124         return isInAccessibleDrag() && isEventOverView(mActivity.getDropTargetBar(), ev);
125     }
126 
127     @Override
onInterceptHoverEvent(MotionEvent ev)128     public boolean onInterceptHoverEvent(MotionEvent ev) {
129         if (mActivity == null || mActivity.getWorkspace() == null) {
130             return false;
131         }
132         AbstractFloatingView topView = AbstractFloatingView.getTopOpenView(mActivity);
133         if (!(topView instanceof Folder)) {
134             return false;
135         } else {
136             AccessibilityManager accessibilityManager = (AccessibilityManager)
137                     getContext().getSystemService(Context.ACCESSIBILITY_SERVICE);
138             if (accessibilityManager.isTouchExplorationEnabled()) {
139                 Folder currentFolder = (Folder) topView;
140                 final int action = ev.getAction();
141                 boolean isOverFolderOrSearchBar;
142                 switch (action) {
143                     case MotionEvent.ACTION_HOVER_ENTER:
144                         isOverFolderOrSearchBar = isEventOverView(topView, ev) ||
145                                 isEventOverAccessibleDropTargetBar(ev);
146                         if (!isOverFolderOrSearchBar) {
147                             sendTapOutsideFolderAccessibilityEvent(currentFolder.isEditingName());
148                             mHoverPointClosesFolder = true;
149                             return true;
150                         }
151                         mHoverPointClosesFolder = false;
152                         break;
153                     case MotionEvent.ACTION_HOVER_MOVE:
154                         isOverFolderOrSearchBar = isEventOverView(topView, ev) ||
155                                 isEventOverAccessibleDropTargetBar(ev);
156                         if (!isOverFolderOrSearchBar && !mHoverPointClosesFolder) {
157                             sendTapOutsideFolderAccessibilityEvent(currentFolder.isEditingName());
158                             mHoverPointClosesFolder = true;
159                             return true;
160                         } else if (!isOverFolderOrSearchBar) {
161                             return true;
162                         }
163                         mHoverPointClosesFolder = false;
164                 }
165             }
166         }
167         return false;
168     }
169 
sendTapOutsideFolderAccessibilityEvent(boolean isEditingName)170     private void sendTapOutsideFolderAccessibilityEvent(boolean isEditingName) {
171         int stringId = isEditingName ? R.string.folder_tap_to_rename : R.string.folder_tap_to_close;
172         sendCustomAccessibilityEvent(
173                 this, AccessibilityEvent.TYPE_VIEW_FOCUSED, getContext().getString(stringId));
174     }
175 
176     @Override
onHoverEvent(MotionEvent ev)177     public boolean onHoverEvent(MotionEvent ev) {
178         // If we've received this, we've already done the necessary handling
179         // in onInterceptHoverEvent. Return true to consume the event.
180         return false;
181     }
182 
183 
isInAccessibleDrag()184     private boolean isInAccessibleDrag() {
185         return mActivity.getAccessibilityDelegate().isInAccessibleDrag();
186     }
187 
188     @Override
onRequestSendAccessibilityEvent(View child, AccessibilityEvent event)189     public boolean onRequestSendAccessibilityEvent(View child, AccessibilityEvent event) {
190         if (isInAccessibleDrag() && child instanceof DropTargetBar) {
191             return true;
192         }
193         return super.onRequestSendAccessibilityEvent(child, event);
194     }
195 
196     @Override
addChildrenForAccessibility(ArrayList<View> childrenForAccessibility)197     public void addChildrenForAccessibility(ArrayList<View> childrenForAccessibility) {
198         View topView = AbstractFloatingView.getTopOpenViewWithType(mActivity,
199                 AbstractFloatingView.TYPE_ACCESSIBLE);
200         if (topView != null) {
201             addAccessibleChildToList(topView, childrenForAccessibility);
202             if (isInAccessibleDrag()) {
203                 addAccessibleChildToList(mActivity.getDropTargetBar(), childrenForAccessibility);
204             }
205         } else {
206             super.addChildrenForAccessibility(childrenForAccessibility);
207         }
208     }
209 
210     @Override
dispatchTouchEvent(MotionEvent ev)211     public boolean dispatchTouchEvent(MotionEvent ev) {
212         ev.offsetLocation(getTranslationX(), 0);
213         try {
214             return super.dispatchTouchEvent(ev);
215         } finally {
216             ev.offsetLocation(-getTranslationX(), 0);
217         }
218     }
219 
animateViewIntoPosition(DragView dragView, final int[] pos, float alpha, float scaleX, float scaleY, int animationEndStyle, Runnable onFinishRunnable, int duration)220     public void animateViewIntoPosition(DragView dragView, final int[] pos, float alpha,
221             float scaleX, float scaleY, int animationEndStyle, Runnable onFinishRunnable,
222             int duration) {
223         Rect r = new Rect();
224         getViewRectRelativeToSelf(dragView, r);
225         final int fromX = r.left;
226         final int fromY = r.top;
227 
228         animateViewIntoPosition(dragView, fromX, fromY, pos[0], pos[1], alpha, 1, 1, scaleX, scaleY,
229                 onFinishRunnable, animationEndStyle, duration, null);
230     }
231 
animateViewIntoPosition(DragView dragView, final View child, View anchorView)232     public void animateViewIntoPosition(DragView dragView, final View child, View anchorView) {
233         animateViewIntoPosition(dragView, child, -1, anchorView);
234     }
235 
animateViewIntoPosition(DragView dragView, final View child, int duration, View anchorView)236     public void animateViewIntoPosition(DragView dragView, final View child, int duration,
237             View anchorView) {
238 
239         ShortcutAndWidgetContainer parentChildren = (ShortcutAndWidgetContainer) child.getParent();
240         CellLayout.LayoutParams lp =  (CellLayout.LayoutParams) child.getLayoutParams();
241         parentChildren.measureChild(child);
242         parentChildren.layoutChild(child);
243 
244         Rect dragViewBounds = new Rect();
245         getViewRectRelativeToSelf(dragView, dragViewBounds);
246         final int fromX = dragViewBounds.left;
247         final int fromY = dragViewBounds.top;
248 
249         float coord[] = new float[2];
250         float childScale = child.getScaleX();
251 
252         coord[0] = lp.x + (child.getMeasuredWidth() * (1 - childScale) / 2);
253         coord[1] = lp.y + (child.getMeasuredHeight() * (1 - childScale) / 2);
254 
255         // Since the child hasn't necessarily been laid out, we force the lp to be updated with
256         // the correct coordinates (above) and use these to determine the final location
257         float scale = getDescendantCoordRelativeToSelf((View) child.getParent(), coord);
258 
259         // We need to account for the scale of the child itself, as the above only accounts for
260         // for the scale in parents.
261         scale *= childScale;
262         int toX = Math.round(coord[0]);
263         int toY = Math.round(coord[1]);
264 
265         float toScale = scale;
266 
267         if (child instanceof DraggableView) {
268             // This code is fairly subtle. Please verify drag and drop is pixel-perfect in a number
269             // of scenarios before modifying (from all apps, from workspace, different grid-sizes,
270             // shortcuts from in and out of Launcher etc).
271             DraggableView d = (DraggableView) child;
272             Rect destRect = new Rect();
273             d.getWorkspaceVisualDragBounds(destRect);
274 
275             // In most cases this additional scale factor should be a no-op (1). It mainly accounts
276             // for alternate grids where the source and destination icon sizes are different
277             toScale *= ((1f * destRect.width())
278                     / (dragView.getMeasuredWidth() - dragView.getBlurSizeOutline()));
279 
280             // This accounts for the offset of the DragView created by scaling it about its
281             // center as it animates into place.
282             float scaleShiftX = dragView.getMeasuredWidth() * (1 - toScale) / 2;
283             float scaleShiftY = dragView.getMeasuredHeight() * (1 - toScale) / 2;
284 
285             toX += scale * destRect.left - toScale * dragView.getBlurSizeOutline() / 2 - scaleShiftX;
286             toY += scale * destRect.top - toScale * dragView.getBlurSizeOutline() / 2 - scaleShiftY;
287         }
288 
289         child.setVisibility(INVISIBLE);
290         Runnable onCompleteRunnable = () -> child.setVisibility(VISIBLE);
291         animateViewIntoPosition(dragView, fromX, fromY, toX, toY, 1, 1, 1, toScale, toScale,
292                 onCompleteRunnable, ANIMATION_END_DISAPPEAR, duration, anchorView);
293     }
294 
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)295     public void animateViewIntoPosition(final DragView view, final int fromX, final int fromY,
296             final int toX, final int toY, float finalAlpha, float initScaleX, float initScaleY,
297             float finalScaleX, float finalScaleY, Runnable onCompleteRunnable,
298             int animationEndStyle, int duration, View anchorView) {
299         Rect from = new Rect(fromX, fromY, fromX +
300                 view.getMeasuredWidth(), fromY + view.getMeasuredHeight());
301         Rect to = new Rect(toX, toY, toX + view.getMeasuredWidth(), toY + view.getMeasuredHeight());
302         animateView(view, from, to, finalAlpha, initScaleX, initScaleY, finalScaleX, finalScaleY, duration,
303                 null, null, onCompleteRunnable, animationEndStyle, anchorView);
304     }
305 
306     /**
307      * This method animates a view at the end of a drag and drop animation.
308      *
309      * @param view The view to be animated. This view is drawn directly into DragLayer, and so
310      *        doesn't need to be a child of DragLayer.
311      * @param from The initial location of the view. Only the left and top parameters are used.
312      * @param to The final location of the view. Only the left and top parameters are used. This
313      *        location doesn't account for scaling, and so should be centered about the desired
314      *        final location (including scaling).
315      * @param finalAlpha The final alpha of the view, in case we want it to fade as it animates.
316      * @param finalScaleX The final scale of the view. The view is scaled about its center.
317      * @param finalScaleY The final scale of the view. The view is scaled about its center.
318      * @param duration The duration of the animation.
319      * @param motionInterpolator The interpolator to use for the location of the view.
320      * @param alphaInterpolator The interpolator to use for the alpha of the view.
321      * @param onCompleteRunnable Optional runnable to run on animation completion.
322      * @param animationEndStyle Whether or not to fade out the view once the animation completes.
323      *        {@link #ANIMATION_END_DISAPPEAR} or {@link #ANIMATION_END_REMAIN_VISIBLE}.
324      * @param anchorView If not null, this represents the view which the animated view stays
325      *        anchored to in case scrolling is currently taking place. Note: currently this is
326      *        only used for the X dimension for the case of the workspace.
327      */
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)328     public void animateView(final DragView view, final Rect from, final Rect to,
329             final float finalAlpha, final float initScaleX, final float initScaleY,
330             final float finalScaleX, final float finalScaleY, int duration,
331             final Interpolator motionInterpolator, final Interpolator alphaInterpolator,
332             final Runnable onCompleteRunnable, final int animationEndStyle, View anchorView) {
333 
334         // Calculate the duration of the animation based on the object's distance
335         final float dist = (float) Math.hypot(to.left - from.left, to.top - from.top);
336         final Resources res = getResources();
337         final float maxDist = (float) res.getInteger(R.integer.config_dropAnimMaxDist);
338 
339         // If duration < 0, this is a cue to compute the duration based on the distance
340         if (duration < 0) {
341             duration = res.getInteger(R.integer.config_dropAnimMaxDuration);
342             if (dist < maxDist) {
343                 duration *= DEACCEL_1_5.getInterpolation(dist / maxDist);
344             }
345             duration = Math.max(duration, res.getInteger(R.integer.config_dropAnimMinDuration));
346         }
347 
348         // Fall back to cubic ease out interpolator for the animation if none is specified
349         TimeInterpolator interpolator = null;
350         if (alphaInterpolator == null || motionInterpolator == null) {
351             interpolator = DEACCEL_1_5;
352         }
353 
354         // Animate the view
355         final float initAlpha = view.getAlpha();
356         final float dropViewScale = view.getScaleX();
357         AnimatorUpdateListener updateCb = new AnimatorUpdateListener() {
358             @Override
359             public void onAnimationUpdate(ValueAnimator animation) {
360                 final float percent = (Float) animation.getAnimatedValue();
361                 final int width = view.getMeasuredWidth();
362                 final int height = view.getMeasuredHeight();
363 
364                 float alphaPercent = alphaInterpolator == null ? percent :
365                         alphaInterpolator.getInterpolation(percent);
366                 float motionPercent = motionInterpolator == null ? percent :
367                         motionInterpolator.getInterpolation(percent);
368 
369                 float initialScaleX = initScaleX * dropViewScale;
370                 float initialScaleY = initScaleY * dropViewScale;
371                 float scaleX = finalScaleX * percent + initialScaleX * (1 - percent);
372                 float scaleY = finalScaleY * percent + initialScaleY * (1 - percent);
373                 float alpha = finalAlpha * alphaPercent + initAlpha * (1 - alphaPercent);
374 
375                 float fromLeft = from.left + (initialScaleX - 1f) * width / 2;
376                 float fromTop = from.top + (initialScaleY - 1f) * height / 2;
377 
378                 int x = (int) (fromLeft + Math.round(((to.left - fromLeft) * motionPercent)));
379                 int y = (int) (fromTop + Math.round(((to.top - fromTop) * motionPercent)));
380 
381                 int anchorAdjust = mAnchorView == null ? 0 : (int) (mAnchorView.getScaleX() *
382                     (mAnchorViewInitialScrollX - mAnchorView.getScrollX()));
383 
384                 int xPos = x - mDropView.getScrollX() + anchorAdjust;
385                 int yPos = y - mDropView.getScrollY();
386 
387                 mDropView.setTranslationX(xPos);
388                 mDropView.setTranslationY(yPos);
389                 mDropView.setScaleX(scaleX);
390                 mDropView.setScaleY(scaleY);
391                 mDropView.setAlpha(alpha);
392             }
393         };
394         animateView(view, updateCb, duration, interpolator, onCompleteRunnable, animationEndStyle,
395                 anchorView);
396     }
397 
animateView(final DragView view, AnimatorUpdateListener updateCb, int duration, TimeInterpolator interpolator, final Runnable onCompleteRunnable, final int animationEndStyle, View anchorView)398     public void animateView(final DragView view, AnimatorUpdateListener updateCb, int duration,
399             TimeInterpolator interpolator, final Runnable onCompleteRunnable,
400             final int animationEndStyle, View anchorView) {
401         // Clean up the previous animations
402         if (mDropAnim != null) mDropAnim.cancel();
403 
404         // Show the drop view if it was previously hidden
405         mDropView = view;
406         mDropView.cancelAnimation();
407         mDropView.requestLayout();
408 
409         // Set the anchor view if the page is scrolling
410         if (anchorView != null) {
411             mAnchorViewInitialScrollX = anchorView.getScrollX();
412         }
413         mAnchorView = anchorView;
414 
415         // Create and start the animation
416         mDropAnim = new ValueAnimator();
417         mDropAnim.setInterpolator(interpolator);
418         mDropAnim.setDuration(duration);
419         mDropAnim.setFloatValues(0f, 1f);
420         mDropAnim.addUpdateListener(updateCb);
421         mDropAnim.addListener(new AnimatorListenerAdapter() {
422             public void onAnimationEnd(Animator animation) {
423                 if (onCompleteRunnable != null) {
424                     onCompleteRunnable.run();
425                 }
426                 switch (animationEndStyle) {
427                 case ANIMATION_END_DISAPPEAR:
428                     clearAnimatedView();
429                     break;
430                 case ANIMATION_END_REMAIN_VISIBLE:
431                     break;
432                 }
433                 mDropAnim = null;
434             }
435         });
436         mDropAnim.start();
437     }
438 
clearAnimatedView()439     public void clearAnimatedView() {
440         if (mDropAnim != null) {
441             mDropAnim.cancel();
442         }
443         mDropAnim = null;
444         if (mDropView != null) {
445             mDragController.onDeferredEndDrag(mDropView);
446         }
447         mDropView = null;
448         invalidate();
449     }
450 
getAnimatedView()451     public View getAnimatedView() {
452         return mDropView;
453     }
454 
455     @Override
onViewAdded(View child)456     public void onViewAdded(View child) {
457         super.onViewAdded(child);
458         updateChildIndices();
459         mActivity.onDragLayerHierarchyChanged();
460     }
461 
462     @Override
onViewRemoved(View child)463     public void onViewRemoved(View child) {
464         super.onViewRemoved(child);
465         updateChildIndices();
466         mActivity.onDragLayerHierarchyChanged();
467     }
468 
469     @Override
bringChildToFront(View child)470     public void bringChildToFront(View child) {
471         super.bringChildToFront(child);
472         updateChildIndices();
473     }
474 
updateChildIndices()475     private void updateChildIndices() {
476         mTopViewIndex = -1;
477         int childCount = getChildCount();
478         for (int i = 0; i < childCount; i++) {
479             if (getChildAt(i) instanceof DragView) {
480                 mTopViewIndex = i;
481             }
482         }
483         mChildCountOnLastUpdate = childCount;
484     }
485 
486     @Override
getChildDrawingOrder(int childCount, int i)487     protected int getChildDrawingOrder(int childCount, int i) {
488         if (mChildCountOnLastUpdate != childCount) {
489             // between platform versions 17 and 18, behavior for onChildViewRemoved / Added changed.
490             // Pre-18, the child was not added / removed by the time of those callbacks. We need to
491             // force update our representation of things here to avoid crashing on pre-18 devices
492             // in certain instances.
493             updateChildIndices();
494         }
495 
496         // i represents the current draw iteration
497         if (mTopViewIndex == -1) {
498             // in general we do nothing
499             return i;
500         } else if (i == childCount - 1) {
501             // if we have a top index, we return it when drawing last item (highest z-order)
502             return mTopViewIndex;
503         } else if (i < mTopViewIndex) {
504             return i;
505         } else {
506             // for indexes greater than the top index, we fetch one item above to shift for the
507             // displacement of the top index
508             return i + 1;
509         }
510     }
511 
512     @Override
dispatchDraw(Canvas canvas)513     protected void dispatchDraw(Canvas canvas) {
514         // Draw the background below children.
515         mWorkspaceDragScrim.draw(canvas);
516         mFocusIndicatorHelper.draw(canvas);
517         super.dispatchDraw(canvas);
518     }
519 
getWorkspaceDragScrim()520     public Scrim getWorkspaceDragScrim() {
521         return mWorkspaceDragScrim;
522     }
523 
524     /**
525      * Called when one handed mode state changed.
526      * @param activated true if one handed mode activated, false otherwise.
527      */
onOneHandedModeStateChanged(boolean activated)528     public void onOneHandedModeStateChanged(boolean activated) {
529         for (TouchController controller : mControllers) {
530             controller.onOneHandedModeStateChanged(activated);
531         }
532     }
533 }
534