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