/* * Copyright (C) 2008 The Android Open Source Project * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package com.android.launcher3.dragndrop; import android.content.ComponentName; import android.content.res.Resources; import android.graphics.Bitmap; import android.graphics.Point; import android.graphics.Rect; import android.os.IBinder; import android.view.DragEvent; import android.view.HapticFeedbackConstants; import android.view.KeyEvent; import android.view.MotionEvent; import android.view.View; import android.view.inputmethod.InputMethodManager; import com.android.launcher3.DragSource; import com.android.launcher3.DropTarget; import com.android.launcher3.ItemInfo; import com.android.launcher3.Launcher; import com.android.launcher3.R; import com.android.launcher3.ShortcutInfo; import com.android.launcher3.accessibility.DragViewStateAnnouncer; import com.android.launcher3.util.ItemInfoMatcher; import com.android.launcher3.util.Thunk; import com.android.launcher3.util.TouchController; import java.util.ArrayList; /** * Class for initiating a drag within a view or across multiple views. */ public class DragController implements DragDriver.EventListener, TouchController { private static final boolean PROFILE_DRAWING_DURING_DRAG = false; @Thunk Launcher mLauncher; private FlingToDeleteHelper mFlingToDeleteHelper; // temporaries to avoid gc thrash private Rect mRectTemp = new Rect(); private final int[] mCoordinatesTemp = new int[2]; /** * Drag driver for the current drag/drop operation, or null if there is no active DND operation. * It's null during accessible drag operations. */ private DragDriver mDragDriver = null; /** Options controlling the drag behavior. */ private DragOptions mOptions; /** X coordinate of the down event. */ private int mMotionDownX; /** Y coordinate of the down event. */ private int mMotionDownY; private DropTarget.DragObject mDragObject; /** Who can receive drop events */ private ArrayList mDropTargets = new ArrayList<>(); private ArrayList mListeners = new ArrayList<>(); /** The window token used as the parent for the DragView. */ private IBinder mWindowToken; private View mMoveTarget; private DropTarget mLastDropTarget; @Thunk int mLastTouch[] = new int[2]; @Thunk long mLastTouchUpTime = -1; @Thunk int mDistanceSinceScroll = 0; private int mTmpPoint[] = new int[2]; private Rect mDragLayerRect = new Rect(); private boolean mIsInPreDrag; /** * Interface to receive notifications when a drag starts or stops */ public interface DragListener { /** * A drag has begun * * @param dragObject The object being dragged * @param options Options used to start the drag */ void onDragStart(DropTarget.DragObject dragObject, DragOptions options); /** * The drag has ended */ void onDragEnd(); } /** * Used to create a new DragLayer from XML. */ public DragController(Launcher launcher) { mLauncher = launcher; mFlingToDeleteHelper = new FlingToDeleteHelper(launcher); } /** * Starts a drag. * * @param b The bitmap to display as the drag image. It will be re-scaled to the * enlarged size. * @param dragLayerX The x position in the DragLayer of the left-top of the bitmap. * @param dragLayerY The y position in the DragLayer of the left-top of the bitmap. * @param source An object representing where the drag originated * @param dragInfo The data associated with the object that is being dragged * @param dragRegion Coordinates within the bitmap b for the position of item being dragged. * Makes dragging feel more precise, e.g. you can clip out a transparent border */ public DragView startDrag(Bitmap b, int dragLayerX, int dragLayerY, DragSource source, ItemInfo dragInfo, Point dragOffset, Rect dragRegion, float initialDragViewScale, DragOptions options) { if (PROFILE_DRAWING_DURING_DRAG) { android.os.Debug.startMethodTracing("Launcher"); } // Hide soft keyboard, if visible mLauncher.getSystemService(InputMethodManager.class) .hideSoftInputFromWindow(mWindowToken, 0); mOptions = options; if (mOptions.systemDndStartPoint != null) { mMotionDownX = mOptions.systemDndStartPoint.x; mMotionDownY = mOptions.systemDndStartPoint.y; } final int registrationX = mMotionDownX - dragLayerX; final int registrationY = mMotionDownY - dragLayerY; final int dragRegionLeft = dragRegion == null ? 0 : dragRegion.left; final int dragRegionTop = dragRegion == null ? 0 : dragRegion.top; mLastDropTarget = null; mDragObject = new DropTarget.DragObject(); mIsInPreDrag = mOptions.preDragCondition != null && !mOptions.preDragCondition.shouldStartDrag(0); final Resources res = mLauncher.getResources(); final float scaleDps = mIsInPreDrag ? res.getDimensionPixelSize(R.dimen.pre_drag_view_scale) : 0f; final DragView dragView = mDragObject.dragView = new DragView(mLauncher, b, registrationX, registrationY, initialDragViewScale, scaleDps); mDragObject.dragComplete = false; if (mOptions.isAccessibleDrag) { // For an accessible drag, we assume the view is being dragged from the center. mDragObject.xOffset = b.getWidth() / 2; mDragObject.yOffset = b.getHeight() / 2; mDragObject.accessibleDrag = true; } else { mDragObject.xOffset = mMotionDownX - (dragLayerX + dragRegionLeft); mDragObject.yOffset = mMotionDownY - (dragLayerY + dragRegionTop); mDragObject.stateAnnouncer = DragViewStateAnnouncer.createFor(dragView); mDragDriver = DragDriver.create(mLauncher, this, mDragObject, mOptions); } mDragObject.dragSource = source; mDragObject.dragInfo = dragInfo; mDragObject.originalDragInfo = new ItemInfo(); mDragObject.originalDragInfo.copyFrom(dragInfo); if (dragOffset != null) { dragView.setDragVisualizeOffset(new Point(dragOffset)); } if (dragRegion != null) { dragView.setDragRegion(new Rect(dragRegion)); } mLauncher.getDragLayer().performHapticFeedback(HapticFeedbackConstants.LONG_PRESS); dragView.show(mMotionDownX, mMotionDownY); mDistanceSinceScroll = 0; if (!mIsInPreDrag) { callOnDragStart(); } else if (mOptions.preDragCondition != null) { mOptions.preDragCondition.onPreDragStart(mDragObject); } mLastTouch[0] = mMotionDownX; mLastTouch[1] = mMotionDownY; handleMoveEvent(mMotionDownX, mMotionDownY); mLauncher.getUserEventDispatcher().resetActionDurationMillis(); return dragView; } private void callOnDragStart() { for (DragListener listener : new ArrayList<>(mListeners)) { listener.onDragStart(mDragObject, mOptions); } if (mOptions.preDragCondition != null) { mOptions.preDragCondition.onPreDragEnd(mDragObject, true /* dragStarted*/); } mIsInPreDrag = false; } /** * Call this from a drag source view like this: * *
     *  @Override
     *  public boolean dispatchKeyEvent(KeyEvent event) {
     *      return mDragController.dispatchKeyEvent(this, event)
     *              || super.dispatchKeyEvent(event);
     * 
*/ public boolean dispatchKeyEvent(KeyEvent event) { return mDragDriver != null; } public boolean isDragging() { return mDragDriver != null || (mOptions != null && mOptions.isAccessibleDrag); } /** * Stop dragging without dropping. */ public void cancelDrag() { if (isDragging()) { if (mLastDropTarget != null) { mLastDropTarget.onDragExit(mDragObject); } mDragObject.deferDragViewCleanupPostAnimation = false; mDragObject.cancelled = true; mDragObject.dragComplete = true; if (!mIsInPreDrag) { mDragObject.dragSource.onDropCompleted(null, mDragObject, false, false); } } endDrag(); } public void onAppsRemoved(ItemInfoMatcher matcher) { // Cancel the current drag if we are removing an app that we are dragging if (mDragObject != null) { ItemInfo dragInfo = mDragObject.dragInfo; if (dragInfo instanceof ShortcutInfo) { ComponentName cn = dragInfo.getTargetComponent(); if (cn != null && matcher.matches(dragInfo, cn)) { cancelDrag(); } } } } private void endDrag() { if (isDragging()) { mDragDriver = null; boolean isDeferred = false; if (mDragObject.dragView != null) { isDeferred = mDragObject.deferDragViewCleanupPostAnimation; if (!isDeferred) { mDragObject.dragView.remove(); } else if (mIsInPreDrag) { animateDragViewToOriginalPosition(null, null, -1); } mDragObject.dragView = null; } // Only end the drag if we are not deferred if (!isDeferred) { callOnDragEnd(); } } mFlingToDeleteHelper.releaseVelocityTracker(); } public void animateDragViewToOriginalPosition(final Runnable onComplete, final View originalIcon, int duration) { Runnable onCompleteRunnable = new Runnable() { @Override public void run() { if (originalIcon != null) { originalIcon.setVisibility(View.VISIBLE); } if (onComplete != null) { onComplete.run(); } } }; mDragObject.dragView.animateTo(mMotionDownX, mMotionDownY, onCompleteRunnable, duration); } private void callOnDragEnd() { if (mIsInPreDrag && mOptions.preDragCondition != null) { mOptions.preDragCondition.onPreDragEnd(mDragObject, false /* dragStarted*/); } mIsInPreDrag = false; mOptions = null; for (DragListener listener : new ArrayList<>(mListeners)) { listener.onDragEnd(); } } /** * This only gets called as a result of drag view cleanup being deferred in endDrag(); */ void onDeferredEndDrag(DragView dragView) { dragView.remove(); if (mDragObject.deferDragViewCleanupPostAnimation) { // If we skipped calling onDragEnd() before, do it now callOnDragEnd(); } } /** * Clamps the position to the drag layer bounds. */ private int[] getClampedDragLayerPos(float x, float y) { mLauncher.getDragLayer().getLocalVisibleRect(mDragLayerRect); mTmpPoint[0] = (int) Math.max(mDragLayerRect.left, Math.min(x, mDragLayerRect.right - 1)); mTmpPoint[1] = (int) Math.max(mDragLayerRect.top, Math.min(y, mDragLayerRect.bottom - 1)); return mTmpPoint; } public long getLastGestureUpTime() { if (mDragDriver != null) { return System.currentTimeMillis(); } else { return mLastTouchUpTime; } } public void resetLastGestureUpTime() { mLastTouchUpTime = -1; } @Override public void onDriverDragMove(float x, float y) { final int[] dragLayerPos = getClampedDragLayerPos(x, y); handleMoveEvent(dragLayerPos[0], dragLayerPos[1]); } @Override public void onDriverDragExitWindow() { if (mLastDropTarget != null) { mLastDropTarget.onDragExit(mDragObject); mLastDropTarget = null; } } @Override public void onDriverDragEnd(float x, float y) { DropTarget dropTarget; Runnable flingAnimation = mFlingToDeleteHelper.getFlingAnimation(mDragObject); if (flingAnimation != null) { dropTarget = mFlingToDeleteHelper.getDropTarget(); } else { dropTarget = findDropTarget((int) x, (int) y, mCoordinatesTemp); } drop(dropTarget, flingAnimation); endDrag(); } @Override public void onDriverDragCancel() { cancelDrag(); } /** * Call this from a drag source view. */ public boolean onControllerInterceptTouchEvent(MotionEvent ev) { if (mOptions != null && mOptions.isAccessibleDrag) { return false; } // Update the velocity tracker mFlingToDeleteHelper.recordMotionEvent(ev); final int action = ev.getAction(); final int[] dragLayerPos = getClampedDragLayerPos(ev.getX(), ev.getY()); final int dragLayerX = dragLayerPos[0]; final int dragLayerY = dragLayerPos[1]; switch (action) { case MotionEvent.ACTION_DOWN: // Remember location of down touch mMotionDownX = dragLayerX; mMotionDownY = dragLayerY; break; case MotionEvent.ACTION_UP: mLastTouchUpTime = System.currentTimeMillis(); break; } return mDragDriver != null && mDragDriver.onInterceptTouchEvent(ev); } /** * Call this from a drag source view. */ public boolean onDragEvent(long dragStartTime, DragEvent event) { mFlingToDeleteHelper.recordDragEvent(dragStartTime, event); return mDragDriver != null && mDragDriver.onDragEvent(event); } /** * Call this from a drag view. */ public void onDragViewAnimationEnd() { if (mDragDriver != null) { mDragDriver.onDragViewAnimationEnd(); } } /** * Sets the view that should handle move events. */ public void setMoveTarget(View view) { mMoveTarget = view; } public boolean dispatchUnhandledMove(View focused, int direction) { return mMoveTarget != null && mMoveTarget.dispatchUnhandledMove(focused, direction); } private void handleMoveEvent(int x, int y) { mDragObject.dragView.move(x, y); // Drop on someone? final int[] coordinates = mCoordinatesTemp; DropTarget dropTarget = findDropTarget(x, y, coordinates); mDragObject.x = coordinates[0]; mDragObject.y = coordinates[1]; checkTouchMove(dropTarget); // Check if we are hovering over the scroll areas mDistanceSinceScroll += Math.hypot(mLastTouch[0] - x, mLastTouch[1] - y); mLastTouch[0] = x; mLastTouch[1] = y; if (mIsInPreDrag && mOptions.preDragCondition != null && mOptions.preDragCondition.shouldStartDrag(mDistanceSinceScroll)) { callOnDragStart(); } } public float getDistanceDragged() { return mDistanceSinceScroll; } public void forceTouchMove() { int[] dummyCoordinates = mCoordinatesTemp; DropTarget dropTarget = findDropTarget(mLastTouch[0], mLastTouch[1], dummyCoordinates); mDragObject.x = dummyCoordinates[0]; mDragObject.y = dummyCoordinates[1]; checkTouchMove(dropTarget); } private void checkTouchMove(DropTarget dropTarget) { if (dropTarget != null) { if (mLastDropTarget != dropTarget) { if (mLastDropTarget != null) { mLastDropTarget.onDragExit(mDragObject); } dropTarget.onDragEnter(mDragObject); } dropTarget.onDragOver(mDragObject); } else { if (mLastDropTarget != null) { mLastDropTarget.onDragExit(mDragObject); } } mLastDropTarget = dropTarget; } /** * Call this from a drag source view. */ public boolean onControllerTouchEvent(MotionEvent ev) { if (mDragDriver == null || mOptions == null || mOptions.isAccessibleDrag) { return false; } // Update the velocity tracker mFlingToDeleteHelper.recordMotionEvent(ev); final int action = ev.getAction(); final int[] dragLayerPos = getClampedDragLayerPos(ev.getX(), ev.getY()); final int dragLayerX = dragLayerPos[0]; final int dragLayerY = dragLayerPos[1]; switch (action) { case MotionEvent.ACTION_DOWN: // Remember where the motion event started mMotionDownX = dragLayerX; mMotionDownY = dragLayerY; break; } return mDragDriver.onTouchEvent(ev); } /** * Since accessible drag and drop won't cause the same sequence of touch events, we manually * inject the appropriate state. */ public void prepareAccessibleDrag(int x, int y) { mMotionDownX = x; mMotionDownY = y; } /** * As above, since accessible drag and drop won't cause the same sequence of touch events, * we manually ensure appropriate drag and drop events get emulated for accessible drag. */ public void completeAccessibleDrag(int[] location) { final int[] coordinates = mCoordinatesTemp; // We make sure that we prime the target for drop. DropTarget dropTarget = findDropTarget(location[0], location[1], coordinates); mDragObject.x = coordinates[0]; mDragObject.y = coordinates[1]; checkTouchMove(dropTarget); dropTarget.prepareAccessibilityDrop(); // Perform the drop drop(dropTarget, null); endDrag(); } private void drop(DropTarget dropTarget, Runnable flingAnimation) { final int[] coordinates = mCoordinatesTemp; mDragObject.x = coordinates[0]; mDragObject.y = coordinates[1]; // Move dragging to the final target. if (dropTarget != mLastDropTarget) { if (mLastDropTarget != null) { mLastDropTarget.onDragExit(mDragObject); } mLastDropTarget = dropTarget; if (dropTarget != null) { dropTarget.onDragEnter(mDragObject); } } mDragObject.dragComplete = true; // Drop onto the target. boolean accepted = false; if (dropTarget != null) { dropTarget.onDragExit(mDragObject); if (dropTarget.acceptDrop(mDragObject)) { if (flingAnimation != null) { flingAnimation.run(); } else if (!mIsInPreDrag) { dropTarget.onDrop(mDragObject); } accepted = true; } } final View dropTargetAsView = dropTarget instanceof View ? (View) dropTarget : null; if (!mIsInPreDrag) { mLauncher.getUserEventDispatcher().logDragNDrop(mDragObject, dropTargetAsView); mDragObject.dragSource.onDropCompleted( dropTargetAsView, mDragObject, flingAnimation != null, accepted); } } private DropTarget findDropTarget(int x, int y, int[] dropCoordinates) { final Rect r = mRectTemp; final ArrayList dropTargets = mDropTargets; final int count = dropTargets.size(); for (int i=count-1; i>=0; i--) { DropTarget target = dropTargets.get(i); if (!target.isDropEnabled()) continue; target.getHitRectRelativeToDragLayer(r); mDragObject.x = x; mDragObject.y = y; if (r.contains(x, y)) { dropCoordinates[0] = x; dropCoordinates[1] = y; mLauncher.getDragLayer().mapCoordInSelfToDescendant((View) target, dropCoordinates); return target; } } return null; } public void setWindowToken(IBinder token) { mWindowToken = token; } /** * Sets the drag listener which will be notified when a drag starts or ends. */ public void addDragListener(DragListener l) { mListeners.add(l); } /** * Remove a previously installed drag listener. */ public void removeDragListener(DragListener l) { mListeners.remove(l); } /** * Add a DropTarget to the list of potential places to receive drop events. */ public void addDropTarget(DropTarget target) { mDropTargets.add(target); } /** * Don't send drop events to target any more. */ public void removeDropTarget(DropTarget target) { mDropTargets.remove(target); } }