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