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