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