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