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