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