• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1 /*
2  * Copyright (C) 2008 The Android Open Source Project
3  *
4  * Licensed under the Apache License, Version 2.0 (the "License");
5  * you may not use this file except in compliance with the License.
6  * You may obtain a copy of the License at
7  *
8  *      http://www.apache.org/licenses/LICENSE-2.0
9  *
10  * Unless required by applicable law or agreed to in writing, software
11  * distributed under the License is distributed on an "AS IS" BASIS,
12  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13  * See the License for the specific language governing permissions and
14  * limitations under the License.
15  */
16 
17 package com.android.launcher3.dragndrop;
18 
19 import static com.android.launcher3.Flags.removeAppsRefreshOnRightClick;
20 import static com.android.launcher3.model.data.ItemInfoWithIcon.FLAG_NOT_PINNABLE;
21 
22 import android.graphics.Point;
23 import android.graphics.Rect;
24 import android.graphics.drawable.Drawable;
25 import android.view.DragEvent;
26 import android.view.KeyEvent;
27 import android.view.MotionEvent;
28 import android.view.View;
29 
30 import androidx.annotation.Nullable;
31 import androidx.annotation.VisibleForTesting;
32 
33 import com.android.app.animation.Interpolators;
34 import com.android.launcher3.DragSource;
35 import com.android.launcher3.DropTarget;
36 import com.android.launcher3.Flags;
37 import com.android.launcher3.logging.InstanceId;
38 import com.android.launcher3.model.data.AppPairInfo;
39 import com.android.launcher3.model.data.ItemInfo;
40 import com.android.launcher3.model.data.ItemInfoWithIcon;
41 import com.android.launcher3.model.data.WorkspaceItemInfo;
42 import com.android.launcher3.util.TouchController;
43 import com.android.launcher3.views.ActivityContext;
44 
45 import java.util.ArrayList;
46 import java.util.Optional;
47 import java.util.function.Predicate;
48 
49 /**
50  * Class for initiating a drag within a view or across multiple views.
51  * @param <T>
52  */
53 public abstract class DragController<T extends ActivityContext>
54         implements DragDriver.EventListener, TouchController {
55 
56     /**
57      * When a drag is started from a deep press, you need to drag this much farther than normal to
58      * end a pre-drag. See {@link DragOptions.PreDragCondition#shouldStartDrag(double)}.
59      */
60     private static final int DEEP_PRESS_DISTANCE_FACTOR = 3;
61 
62     protected final T mActivity;
63 
64     // temporaries to avoid gc thrash
65     private final Rect mRectTemp = new Rect();
66     private final int[] mCoordinatesTemp = new int[2];
67 
68     /**
69      * Drag driver for the current drag/drop operation, or null if there is no active DND operation.
70      * It's null during accessible drag operations.
71      */
72     protected DragDriver mDragDriver = null;
73 
74     @VisibleForTesting
75     /** Options controlling the drag behavior. */
76     public DragOptions mOptions;
77 
78     /** Coordinate for motion down event */
79     protected final Point mMotionDown = new Point();
80     /** Coordinate for last touch event **/
81     protected final Point mLastTouch = new Point();
82 
83     protected final Point mTmpPoint = new Point();
84 
85     @VisibleForTesting
86     public DropTarget.DragObject mDragObject;
87 
88     /** Who can receive drop events */
89     private final ArrayList<DropTarget> mDropTargets = new ArrayList<>();
90     private final ArrayList<DragListener> mListeners = new ArrayList<>();
91 
92     protected DropTarget mLastDropTarget;
93 
94     private int mLastTouchClassification;
95     protected int mDistanceSinceScroll = 0;
96 
97     /**
98      * This variable is to differentiate between a long press and a drag, if it's true that means
99      * it's a long press and when it's false means that we are no longer in a long press.
100      */
101     protected boolean mIsInPreDrag;
102 
103     private final int DRAG_VIEW_SCALE_DURATION_MS = 500;
104 
105     /**
106      * Interface to receive notifications when a drag starts or stops
107      */
108     public interface DragListener {
109         /**
110          * A drag has begun
111          *
112          * @param dragObject The object being dragged
113          * @param options Options used to start the drag
114          */
onDragStart(DropTarget.DragObject dragObject, DragOptions options)115         void onDragStart(DropTarget.DragObject dragObject, DragOptions options);
116 
117         /**
118          * The drag has ended
119          */
onDragEnd()120         void onDragEnd();
121     }
122 
123     /**
124      * Used to create a new DragLayer from XML.
125      */
DragController(T activity)126     public DragController(T activity) {
127         mActivity = activity;
128     }
129 
130     /**
131      * Starts a drag.
132      *
133      * <p>When the drag is started, the UI automatically goes into spring loaded mode. On a
134      * successful drop, it is the responsibility of the {@link DropTarget} to exit out of the spring
135      * loaded mode. If the drop was cancelled for some reason, the UI will automatically exit out of
136      * this mode.
137      *
138      * @param drawable The drawable to be displayed in the drag view.  It will be re-scaled to the
139      *                 enlarged size.
140      * @param originalView The source view (ie. icon, widget etc.) that is being dragged and which
141      *                     the DragView represents
142      * @param dragLayerX The x position in the DragLayer of the left-top of the bitmap.
143      * @param dragLayerY The y position in the DragLayer of the left-top of the bitmap.
144      * @param source An object representing where the drag originated
145      * @param dragInfo The data associated with the object that is being dragged
146      * @param dragRegion Coordinates within the bitmap b for the position of item being dragged.
147      *                   Makes dragging feel more precise, e.g. you can clip out a transparent
148      *                   border
149      */
startDrag( Drawable drawable, DraggableView originalView, int dragLayerX, int dragLayerY, DragSource source, ItemInfo dragInfo, Rect dragRegion, float initialDragViewScale, float dragViewScaleOnDrop, DragOptions options)150     public DragView startDrag(
151             Drawable drawable,
152             DraggableView originalView,
153             int dragLayerX,
154             int dragLayerY,
155             DragSource source,
156             ItemInfo dragInfo,
157             Rect dragRegion,
158             float initialDragViewScale,
159             float dragViewScaleOnDrop,
160             DragOptions options) {
161         return startDrag(drawable, /* view= */ null, originalView, dragLayerX, dragLayerY, source,
162                 dragInfo, dragRegion, initialDragViewScale, dragViewScaleOnDrop, options);
163     }
164 
165     /**
166      * Starts a drag.
167      *
168      * <p>When the drag is started, the UI automatically goes into spring loaded mode. On a
169      * successful drop, it is the responsibility of the {@link DropTarget} to exit out of the spring
170      * loaded mode. If the drop was cancelled for some reason, the UI will automatically exit out of
171      * this mode.
172      *
173      * @param view The view to be displayed in the drag view.  It will be re-scaled to the
174      *             enlarged size.
175      * @param originalView The source view (ie. icon, widget etc.) that is being dragged and which
176      *                     the DragView represents
177      * @param dragLayerX The x position in the DragLayer of the left-top of the bitmap.
178      * @param dragLayerY The y position in the DragLayer of the left-top of the bitmap.
179      * @param source An object representing where the drag originated
180      * @param dragInfo The data associated with the object that is being dragged
181      * @param dragRegion Coordinates within the bitmap b for the position of item being dragged.
182      *                   Makes dragging feel more precise, e.g. you can clip out a transparent
183      *                   border
184      */
startDrag( View view, DraggableView originalView, int dragLayerX, int dragLayerY, DragSource source, ItemInfo dragInfo, Rect dragRegion, float initialDragViewScale, float dragViewScaleOnDrop, DragOptions options)185     public DragView startDrag(
186             View view,
187             DraggableView originalView,
188             int dragLayerX,
189             int dragLayerY,
190             DragSource source,
191             ItemInfo dragInfo,
192             Rect dragRegion,
193             float initialDragViewScale,
194             float dragViewScaleOnDrop,
195             DragOptions options) {
196         return startDrag(/* drawable= */ null, view, originalView, dragLayerX, dragLayerY, source,
197                 dragInfo, dragRegion, initialDragViewScale, dragViewScaleOnDrop, options);
198     }
199 
startDrag( @ullable Drawable drawable, @Nullable View view, DraggableView originalView, int dragLayerX, int dragLayerY, DragSource source, ItemInfo dragInfo, Rect dragRegion, float initialDragViewScale, float dragViewScaleOnDrop, DragOptions options)200     protected abstract DragView startDrag(
201             @Nullable Drawable drawable,
202             @Nullable View view,
203             DraggableView originalView,
204             int dragLayerX,
205             int dragLayerY,
206             DragSource source,
207             ItemInfo dragInfo,
208             Rect dragRegion,
209             float initialDragViewScale,
210             float dragViewScaleOnDrop,
211             DragOptions options);
212 
callOnDragStart()213     protected void callOnDragStart() {
214         if (mOptions.preDragCondition != null) {
215             mOptions.preDragCondition.onPreDragEnd(mDragObject, true /* dragStarted*/);
216         }
217         mIsInPreDrag = false;
218         if (mOptions.preDragEndScale != 0) {
219             mDragObject.dragView
220                     .animate()
221                     .scaleX(mOptions.preDragEndScale)
222                     .scaleY(mOptions.preDragEndScale)
223                     .setInterpolator(Interpolators.EMPHASIZED)
224                     .setDuration(DRAG_VIEW_SCALE_DURATION_MS)
225                     .start();
226         }
227         mDragObject.dragView.onDragStart();
228         for (DragListener listener : new ArrayList<>(mListeners)) {
229             listener.onDragStart(mDragObject, mOptions);
230         }
231     }
232 
isItemPinnable()233     protected boolean isItemPinnable() {
234         return !Flags.privateSpaceRestrictItemDrag()
235                 || !(mDragObject.dragInfo instanceof ItemInfoWithIcon itemInfoWithIcon)
236                 || (itemInfoWithIcon.runtimeStatusFlags & FLAG_NOT_PINNABLE) == 0;
237     }
238 
getLogInstanceId()239     public Optional<InstanceId> getLogInstanceId() {
240         return Optional.ofNullable(mDragObject)
241                 .map(dragObject -> dragObject.logInstanceId);
242     }
243 
244     /**
245      * Call this from a drag source view like this:
246      *
247      * <pre>
248      *  @Override
249      *  public boolean dispatchKeyEvent(KeyEvent event) {
250      *      return mDragController.dispatchKeyEvent(this, event)
251      *              || super.dispatchKeyEvent(event);
252      * </pre>
253      */
dispatchKeyEvent(KeyEvent event)254     public boolean dispatchKeyEvent(KeyEvent event) {
255         return mDragDriver != null;
256     }
257 
isDragging()258     public boolean isDragging() {
259         return mDragDriver != null || (mOptions != null && mOptions.isAccessibleDrag);
260     }
261 
262     /**
263      * Stop dragging without dropping.
264      */
cancelDrag()265     public void cancelDrag() {
266         if (isDragging()) {
267             if (mLastDropTarget != null) {
268                 mLastDropTarget.onDragExit(mDragObject);
269             }
270             mDragObject.deferDragViewCleanupPostAnimation = false;
271             mDragObject.cancelled = true;
272             mDragObject.dragComplete = true;
273             if (!mIsInPreDrag) {
274                 dispatchDropComplete(null, false);
275             }
276         }
277         endDrag();
278     }
279 
dispatchDropComplete(View dropTarget, boolean accepted)280     private void dispatchDropComplete(View dropTarget, boolean accepted) {
281         if (!accepted) {
282             // If it was not accepted, cleanup the state. If it was accepted, it is the
283             // responsibility of the drop target to cleanup the state.
284             exitDrag();
285             mDragObject.deferDragViewCleanupPostAnimation = false;
286         }
287 
288         mDragObject.dragSource.onDropCompleted(dropTarget, mDragObject, accepted);
289     }
290 
exitDrag()291     protected abstract void exitDrag();
292 
onAppsRemoved(Predicate<ItemInfo> matcher)293     public void onAppsRemoved(Predicate<ItemInfo> matcher) {
294         // Cancel the current drag if we are removing an app that we are dragging
295         if (mDragObject != null) {
296             ItemInfo dragInfo = mDragObject.dragInfo;
297             if ((dragInfo instanceof WorkspaceItemInfo && matcher.test(dragInfo))
298                     || (dragInfo instanceof AppPairInfo api && api.anyMatch(matcher))) {
299                 cancelDrag();
300             }
301         }
302     }
303 
endDrag()304     protected void endDrag() {
305         if (isDragging()) {
306             mDragDriver = null;
307             boolean isDeferred = false;
308             if (mDragObject.dragView != null) {
309                 isDeferred = mDragObject.deferDragViewCleanupPostAnimation;
310                 if (!isDeferred) {
311                     mDragObject.dragView.remove();
312                 } else if (mIsInPreDrag) {
313                     animateDragViewToOriginalPosition(null, null, -1);
314                 }
315                 mDragObject.dragView.clearAnimation();
316                 mDragObject.dragView = null;
317             }
318             // Only end the drag if we are not deferred
319             if (!isDeferred) {
320                 callOnDragEnd();
321             }
322         }
323     }
324 
animateDragViewToOriginalPosition(final Runnable onComplete, final View originalIcon, int duration)325     public void animateDragViewToOriginalPosition(final Runnable onComplete,
326             final View originalIcon, int duration) {
327         Runnable onCompleteRunnable = new Runnable() {
328             @Override
329             public void run() {
330                 if (originalIcon != null) {
331                     originalIcon.setVisibility(View.VISIBLE);
332                 }
333                 if (onComplete != null) {
334                     onComplete.run();
335                 }
336             }
337         };
338         mDragObject.dragView.animateTo(mMotionDown.x, mMotionDown.y, onCompleteRunnable, duration);
339     }
340 
callOnDragEnd()341     protected void callOnDragEnd() {
342         if (mIsInPreDrag && mOptions.preDragCondition != null) {
343             mOptions.preDragCondition.onPreDragEnd(mDragObject, false /* dragStarted*/);
344         }
345         mIsInPreDrag = false;
346         mOptions = null;
347         for (DragListener listener : new ArrayList<>(mListeners)) {
348             listener.onDragEnd();
349         }
350     }
351 
352     /**
353      * This only gets called as a result of drag view cleanup being deferred in endDrag();
354      */
onDeferredEndDrag(DragView dragView)355     void onDeferredEndDrag(DragView dragView) {
356         dragView.remove();
357 
358         if (mDragObject.deferDragViewCleanupPostAnimation) {
359             // If we skipped calling onDragEnd() before, do it now
360             callOnDragEnd();
361         }
362     }
363 
364     /**
365      * Clamps the position to the drag layer bounds.
366      */
getClampedDragLayerPos(float x, float y)367     protected Point getClampedDragLayerPos(float x, float y) {
368         mActivity.getDragLayer().getLocalVisibleRect(mRectTemp);
369         mTmpPoint.x = (int) Math.max(mRectTemp.left, Math.min(x, mRectTemp.right - 1));
370         mTmpPoint.y = (int) Math.max(mRectTemp.top, Math.min(y, mRectTemp.bottom - 1));
371         return mTmpPoint;
372     }
373 
374     @Override
onDriverDragMove(float x, float y)375     public void onDriverDragMove(float x, float y) {
376         Point dragLayerPos = getClampedDragLayerPos(x, y);
377         handleMoveEvent(dragLayerPos.x, dragLayerPos.y);
378     }
379 
380     @Override
onDriverDragExitWindow()381     public void onDriverDragExitWindow() {
382         if (mLastDropTarget != null) {
383             mLastDropTarget.onDragExit(mDragObject);
384             mLastDropTarget = null;
385         }
386     }
387 
388     @Override
onDriverDragEnd(float x, float y)389     public void onDriverDragEnd(float x, float y) {
390         if (!endWithFlingAnimation()) {
391             drop(findDropTarget((int) x, (int) y), null);
392         }
393         endDrag();
394     }
395 
endWithFlingAnimation()396     protected boolean endWithFlingAnimation() {
397         return false;
398     }
399 
400     @Override
onDriverDragCancel()401     public void onDriverDragCancel() {
402         cancelDrag();
403     }
404 
405     /**
406      * Call this from a drag source view.
407      */
408     @Override
onControllerInterceptTouchEvent(MotionEvent ev)409     public boolean onControllerInterceptTouchEvent(MotionEvent ev) {
410         if (mOptions != null && mOptions.isAccessibleDrag) {
411             return false;
412         }
413 
414         Point dragLayerPos = getClampedDragLayerPos(getX(ev), getY(ev));
415         mLastTouch.set(dragLayerPos.x,  dragLayerPos.y);
416         if (ev.getAction() == MotionEvent.ACTION_DOWN) {
417             // Remember location of down touch
418             mMotionDown.set(dragLayerPos.x,  dragLayerPos.y);
419         }
420 
421         mLastTouchClassification = ev.getClassification();
422         return mDragDriver != null && mDragDriver.onInterceptTouchEvent(ev);
423     }
424 
getX(MotionEvent ev)425     protected float getX(MotionEvent ev) {
426         return ev.getX();
427     }
428 
getY(MotionEvent ev)429     protected float getY(MotionEvent ev) {
430         return ev.getY();
431     }
432 
433     /**
434      * Call this from a drag source view.
435      */
436     @Override
onControllerTouchEvent(MotionEvent ev)437     public boolean onControllerTouchEvent(MotionEvent ev) {
438         return mDragDriver != null && mDragDriver.onTouchEvent(ev);
439     }
440 
441     /**
442      * Call this from a drag source view.
443      */
onDragEvent(DragEvent event)444     public boolean onDragEvent(DragEvent event) {
445         return mDragDriver != null && mDragDriver.onDragEvent(event);
446     }
447 
handleMoveEvent(int x, int y)448     protected void handleMoveEvent(int x, int y) {
449         mDragObject.dragView.move(x, y);
450 
451         // Check if we are hovering over the scroll areas
452         mDistanceSinceScroll += Math.hypot(mLastTouch.x - x, mLastTouch.y - y);
453         mLastTouch.set(x, y);
454 
455         int distanceDragged = mDistanceSinceScroll;
456         if (mLastTouchClassification == MotionEvent.CLASSIFICATION_DEEP_PRESS) {
457             distanceDragged /= DEEP_PRESS_DISTANCE_FACTOR;
458         }
459         if (mIsInPreDrag && mOptions.preDragCondition != null
460                 && mOptions.preDragCondition.shouldStartDrag(distanceDragged)) {
461             callOnDragStart();
462         }
463 
464         // Drop on someone?
465         checkTouchMove(x, y);
466     }
467 
getDistanceDragged()468     public float getDistanceDragged() {
469         return mDistanceSinceScroll;
470     }
471 
forceTouchMove()472     public void forceTouchMove() {
473         checkTouchMove(mLastTouch.x, mLastTouch.y);
474     }
475 
checkTouchMove(final int x, final int y)476     private DropTarget checkTouchMove(final int x, final int y) {
477         // If we are in predrag, don't trigger any other event until we get out of it
478         if (mIsInPreDrag) {
479             return mLastDropTarget;
480         }
481         DropTarget dropTarget = findDropTarget(x, y);
482         if (dropTarget != null) {
483             if (mLastDropTarget != dropTarget) {
484                 if (mLastDropTarget != null) {
485                     mLastDropTarget.onDragExit(mDragObject);
486                 }
487                 dropTarget.onDragEnter(mDragObject);
488             }
489             dropTarget.onDragOver(mDragObject);
490         } else if (mLastDropTarget != null) {
491             mLastDropTarget.onDragExit(mDragObject);
492         }
493         mLastDropTarget = dropTarget;
494         return mLastDropTarget;
495     }
496 
497     /**
498      * As above, since accessible drag and drop won't cause the same sequence of touch events,
499      * we manually ensure appropriate drag and drop events get emulated for accessible drag.
500      */
completeAccessibleDrag(int[] location)501     public void completeAccessibleDrag(int[] location) {
502         // We make sure that we prime the target for drop.
503         DropTarget dropTarget = checkTouchMove(location[0], location[1]);
504 
505         dropTarget.prepareAccessibilityDrop();
506         // Perform the drop
507         drop(dropTarget, null);
508         endDrag();
509     }
510 
drop(DropTarget dropTarget, Runnable flingAnimation)511     protected void drop(DropTarget dropTarget, Runnable flingAnimation) {
512         // Move dragging to the final target.
513         if (dropTarget != mLastDropTarget) {
514             if (mLastDropTarget != null) {
515                 mLastDropTarget.onDragExit(mDragObject);
516             }
517             mLastDropTarget = dropTarget;
518             if (dropTarget != null) {
519                 dropTarget.onDragEnter(mDragObject);
520             }
521         }
522 
523         mDragObject.dragComplete = true;
524         if (mIsInPreDrag) {
525             if (removeAppsRefreshOnRightClick()) {
526                 mDragObject.cancelled = true;
527             } else {
528                 if (dropTarget != null) {
529                     dropTarget.onDragExit(mDragObject);
530                 }
531                 return;
532             }
533         }
534 
535         // Drop onto the target.
536         boolean accepted = false;
537         if (dropTarget != null) {
538             dropTarget.onDragExit(mDragObject);
539             if (!mIsInPreDrag && dropTarget.acceptDrop(mDragObject)) {
540                 if (flingAnimation != null) {
541                     flingAnimation.run();
542                 } else {
543                     dropTarget.onDrop(mDragObject, mOptions);
544                 }
545                 accepted = true;
546             }
547 
548             final View dropTargetAsView = dropTarget.getDropView();
549             dispatchDropComplete(dropTargetAsView, accepted);
550         }
551     }
552 
findDropTarget(final int x, final int y)553     private DropTarget findDropTarget(final int x, final int y) {
554         mCoordinatesTemp[0] = x;
555         mCoordinatesTemp[1] = y;
556 
557         final Rect r = mRectTemp;
558         final ArrayList<DropTarget> dropTargets = mDropTargets;
559         final int count = dropTargets.size();
560         for (int i = count - 1; i >= 0; i--) {
561             DropTarget target = dropTargets.get(i);
562             if (!target.isDropEnabled())
563                 continue;
564 
565             target.getHitRectRelativeToDragLayer(r);
566             if (r.contains(x, y)) {
567                 mActivity.getDragLayer().mapCoordInSelfToDescendant(target.getDropView(),
568                         mCoordinatesTemp);
569                 mDragObject.x = mCoordinatesTemp[0];
570                 mDragObject.y = mCoordinatesTemp[1];
571                 return target;
572             }
573         }
574         DropTarget dropTarget = getDefaultDropTarget(mCoordinatesTemp);
575         mDragObject.x = mCoordinatesTemp[0];
576         mDragObject.y = mCoordinatesTemp[1];
577         return dropTarget;
578     }
579 
getDefaultDropTarget(int[] dropCoordinates)580     protected abstract DropTarget getDefaultDropTarget(int[] dropCoordinates);
581 
582     /**
583      * Sets the drag listener which will be notified when a drag starts or ends.
584      */
addDragListener(DragListener l)585     public void addDragListener(DragListener l) {
586         mListeners.add(l);
587     }
588 
589     /**
590      * Remove a previously installed drag listener.
591      */
removeDragListener(DragListener l)592     public void removeDragListener(DragListener l) {
593         mListeners.remove(l);
594     }
595 
596     /**
597      * Add a DropTarget to the list of potential places to receive drop events.
598      */
addDropTarget(DropTarget target)599     public void addDropTarget(DropTarget target) {
600         mDropTargets.add(target);
601     }
602 
603     /**
604      * Don't send drop events to <em>target</em> any more.
605      */
removeDropTarget(DropTarget target)606     public void removeDropTarget(DropTarget target) {
607         mDropTargets.remove(target);
608     }
609 }
610