/* * 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.launcher2; import java.util.ArrayList; import android.content.Context; import android.graphics.Bitmap; import android.graphics.Rect; import android.graphics.RectF; import android.os.Handler; import android.os.IBinder; import android.os.Vibrator; import android.util.DisplayMetrics; import android.util.Log; import android.view.KeyEvent; import android.view.MotionEvent; import android.view.View; import android.view.ViewConfiguration; import android.view.WindowManager; import android.view.inputmethod.InputMethodManager; import com.android.launcher.R; /** * Class for initiating a drag within a view or across multiple views. */ public class DragController { @SuppressWarnings({"UnusedDeclaration"}) private static final String TAG = "Launcher.DragController"; /** Indicates the drag is a move. */ public static int DRAG_ACTION_MOVE = 0; /** Indicates the drag is a copy. */ public static int DRAG_ACTION_COPY = 1; private static final int SCROLL_DELAY = 600; private static final int VIBRATE_DURATION = 35; private static final boolean PROFILE_DRAWING_DURING_DRAG = false; private static final int SCROLL_OUTSIDE_ZONE = 0; private static final int SCROLL_WAITING_IN_ZONE = 1; static final int SCROLL_NONE = -1; static final int SCROLL_LEFT = 0; static final int SCROLL_RIGHT = 1; private Context mContext; private Handler mHandler; private final Vibrator mVibrator = new Vibrator(); // temporaries to avoid gc thrash private Rect mRectTemp = new Rect(); private final int[] mCoordinatesTemp = new int[2]; /** Whether or not we're dragging. */ private boolean mDragging; /** X coordinate of the down event. */ private int mMotionDownX; /** Y coordinate of the down event. */ private int mMotionDownY; /** Info about the screen for clamping. */ private DisplayMetrics mDisplayMetrics = new DisplayMetrics(); /** Original view that is being dragged. */ private View mOriginator; /** X offset from the upper-left corner of the cell to where we touched. */ private int mTouchOffsetX; /** Y offset from the upper-left corner of the cell to where we touched. */ private int mTouchOffsetY; /** the area at the edge of the screen that makes the workspace go left * or right while you're dragging. */ private int mScrollZone; /** Where the drag originated */ private DragSource mDragSource; /** The data associated with the object being dragged */ private Object mDragInfo; /** The view that moves around while you drag. */ private DragView mDragView; /** 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; /** The view that will be scrolled when dragging to the left and right edges of the screen. */ private View mScrollView; private View mMoveTarget; private DragScroller mDragScroller; private int mScrollState = SCROLL_OUTSIDE_ZONE; private ScrollRunnable mScrollRunnable = new ScrollRunnable(); private RectF mDeleteRegion; private DropTarget mLastDropTarget; private InputMethodManager mInputMethodManager; private int mLastTouch[] = new int[2]; private int mDistanceSinceScroll = 0; /** * Interface to receive notifications when a drag starts or stops */ interface DragListener { /** * A drag has begun * * @param source An object representing where the drag originated * @param info The data associated with the object that is being dragged * @param dragAction The drag action: either {@link DragController#DRAG_ACTION_MOVE} * or {@link DragController#DRAG_ACTION_COPY} */ void onDragStart(DragSource source, Object info, int dragAction); /** * The drag has ended */ void onDragEnd(); } /** * Used to create a new DragLayer from XML. * * @param context The application's context. */ public DragController(Context context) { mContext = context; mHandler = new Handler(); mScrollZone = context.getResources().getDimensionPixelSize(R.dimen.scroll_zone); } public boolean dragging() { return mDragging; } /** * Starts a drag. * * @param v The view that is being dragged * @param source An object representing where the drag originated * @param dragInfo The data associated with the object that is being dragged * @param dragAction The drag action: either {@link #DRAG_ACTION_MOVE} or * {@link #DRAG_ACTION_COPY} */ public void startDrag(View v, DragSource source, Object dragInfo, int dragAction) { startDrag(v, source, dragInfo, dragAction, null); } /** * Starts a drag. * * @param v The view that is being dragged * @param source An object representing where the drag originated * @param dragInfo The data associated with the object that is being dragged * @param dragAction The drag action: either {@link #DRAG_ACTION_MOVE} or * {@link #DRAG_ACTION_COPY} * @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 void startDrag(View v, DragSource source, Object dragInfo, int dragAction, Rect dragRegion) { mOriginator = v; Bitmap b = getViewBitmap(v); if (b == null) { // out of memory? return; } int[] loc = mCoordinatesTemp; v.getLocationOnScreen(loc); int screenX = loc[0]; int screenY = loc[1]; startDrag(b, screenX, screenY, source, dragInfo, dragAction, dragRegion); b.recycle(); if (dragAction == DRAG_ACTION_MOVE) { v.setVisibility(View.GONE); } } /** * Starts a drag. * * @param v The view that is being dragged * @param bmp The bitmap that represents the view being dragged * @param source An object representing where the drag originated * @param dragInfo The data associated with the object that is being dragged * @param dragAction The drag action: either {@link #DRAG_ACTION_MOVE} or * {@link #DRAG_ACTION_COPY} * @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 void startDrag(View v, Bitmap bmp, DragSource source, Object dragInfo, int dragAction, Rect dragRegion) { mOriginator = v; int[] loc = mCoordinatesTemp; v.getLocationOnScreen(loc); int screenX = loc[0]; int screenY = loc[1]; startDrag(bmp, screenX, screenY, source, dragInfo, dragAction, dragRegion); if (dragAction == DRAG_ACTION_MOVE) { v.setVisibility(View.GONE); } } /** * Starts a drag. * * @param b The bitmap to display as the drag image. It will be re-scaled to the * enlarged size. * @param screenX The x position on screen of the left-top of the bitmap. * @param screenY The y position on screen 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 dragAction The drag action: either {@link #DRAG_ACTION_MOVE} or * {@link #DRAG_ACTION_COPY} */ public void startDrag(Bitmap b, int screenX, int screenY, DragSource source, Object dragInfo, int dragAction) { startDrag(b, screenX, screenY, source, dragInfo, dragAction, null); } /** * Starts a drag. * * @param b The bitmap to display as the drag image. It will be re-scaled to the * enlarged size. * @param screenX The x position on screen of the left-top of the bitmap. * @param screenY The y position on screen 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 dragAction The drag action: either {@link #DRAG_ACTION_MOVE} or * {@link #DRAG_ACTION_COPY} * @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 void startDrag(Bitmap b, int screenX, int screenY, DragSource source, Object dragInfo, int dragAction, Rect dragRegion) { if (PROFILE_DRAWING_DURING_DRAG) { android.os.Debug.startMethodTracing("Launcher"); } // Hide soft keyboard, if visible if (mInputMethodManager == null) { mInputMethodManager = (InputMethodManager) mContext.getSystemService(Context.INPUT_METHOD_SERVICE); } mInputMethodManager.hideSoftInputFromWindow(mWindowToken, 0); for (DragListener listener : mListeners) { listener.onDragStart(source, dragInfo, dragAction); } final int registrationX = ((int)mMotionDownX) - screenX; final int registrationY = ((int)mMotionDownY) - screenY; final int dragRegionLeft = dragRegion == null ? 0 : dragRegion.left; final int dragRegionTop = dragRegion == null ? 0 : dragRegion.top; mTouchOffsetX = mMotionDownX - (screenX + dragRegionLeft); mTouchOffsetY = mMotionDownY - (screenY + dragRegionTop); mDragging = true; mDragSource = source; mDragInfo = dragInfo; mVibrator.vibrate(VIBRATE_DURATION); DragView dragView = mDragView = new DragView(mContext, b, registrationX, registrationY, 0, 0, b.getWidth(), b.getHeight()); final DragSource dragSource = source; dragView.setOnDrawRunnable(new Runnable() { public void run() { dragSource.onDragViewVisible(); }; }); if (dragRegion != null) { dragView.setDragRegion(new Rect(dragRegion)); } dragView.show(mWindowToken, (int)mMotionDownX, (int)mMotionDownY); handleMoveEvent((int) mMotionDownX, (int) mMotionDownY); } /** * Draw the view into a bitmap. */ Bitmap getViewBitmap(View v) { v.clearFocus(); v.setPressed(false); boolean willNotCache = v.willNotCacheDrawing(); v.setWillNotCacheDrawing(false); // Reset the drawing cache background color to fully transparent // for the duration of this operation int color = v.getDrawingCacheBackgroundColor(); v.setDrawingCacheBackgroundColor(0); float alpha = v.getAlpha(); v.setAlpha(1.0f); if (color != 0) { v.destroyDrawingCache(); } v.buildDrawingCache(); Bitmap cacheBitmap = v.getDrawingCache(); if (cacheBitmap == null) { Log.e(TAG, "failed getViewBitmap(" + v + ")", new RuntimeException()); return null; } Bitmap bitmap = Bitmap.createBitmap(cacheBitmap); // Restore the view v.destroyDrawingCache(); v.setAlpha(alpha); v.setWillNotCacheDrawing(willNotCache); v.setDrawingCacheBackgroundColor(color); return bitmap; } /** * Call this from a drag source view like this: * *
     *  @Override
     *  public boolean dispatchKeyEvent(KeyEvent event) {
     *      return mDragController.dispatchKeyEvent(this, event)
     *              || super.dispatchKeyEvent(event);
     * 
*/ @SuppressWarnings({"UnusedDeclaration"}) public boolean dispatchKeyEvent(KeyEvent event) { return mDragging; } public boolean isDragging() { return mDragging; } /** * Stop dragging without dropping. */ public void cancelDrag() { if (mDragging) { // Should we also be calling onDragExit() here? mDragSource.onDropCompleted(null, mDragInfo, false); } endDrag(); } private void endDrag() { if (mDragging) { mDragging = false; if (mOriginator != null) { mOriginator.setVisibility(View.VISIBLE); } for (DragListener listener : mListeners) { listener.onDragEnd(); } if (mDragView != null) { mDragView.remove(); mDragView = null; } } } /** * Call this from a drag source view. */ public boolean onInterceptTouchEvent(MotionEvent ev) { if (false) { Log.d(Launcher.TAG, "DragController.onInterceptTouchEvent " + ev + " mDragging=" + mDragging); } final int action = ev.getAction(); if (action == MotionEvent.ACTION_DOWN) { recordScreenSize(); } final int screenX = clamp((int)ev.getRawX(), 0, mDisplayMetrics.widthPixels); final int screenY = clamp((int)ev.getRawY(), 0, mDisplayMetrics.heightPixels); switch (action) { case MotionEvent.ACTION_MOVE: break; case MotionEvent.ACTION_DOWN: // Remember location of down touch mMotionDownX = screenX; mMotionDownY = screenY; mLastDropTarget = null; break; case MotionEvent.ACTION_UP: if (mDragging) { drop(screenX, screenY); } endDrag(); break; case MotionEvent.ACTION_CANCEL: cancelDrag(); break; } return mDragging; } /** * Sets the view that should handle move events. */ 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) { mDragView.move(x, y); // Drop on someone? final int[] coordinates = mCoordinatesTemp; DropTarget dropTarget = findDropTarget(x, y, coordinates); if (dropTarget != null) { DropTarget delegate = dropTarget.getDropTargetDelegate( mDragSource, coordinates[0], coordinates[1], (int) mTouchOffsetX, (int) mTouchOffsetY, mDragView, mDragInfo); if (delegate != null) { dropTarget = delegate; } if (mLastDropTarget != dropTarget) { if (mLastDropTarget != null) { mLastDropTarget.onDragExit(mDragSource, coordinates[0], coordinates[1], (int) mTouchOffsetX, (int) mTouchOffsetY, mDragView, mDragInfo); } dropTarget.onDragEnter(mDragSource, coordinates[0], coordinates[1], (int) mTouchOffsetX, (int) mTouchOffsetY, mDragView, mDragInfo); } dropTarget.onDragOver(mDragSource, coordinates[0], coordinates[1], (int) mTouchOffsetX, (int) mTouchOffsetY, mDragView, mDragInfo); } else { if (mLastDropTarget != null) { mLastDropTarget.onDragExit(mDragSource, coordinates[0], coordinates[1], (int) mTouchOffsetX, (int) mTouchOffsetY, mDragView, mDragInfo); } } mLastDropTarget = dropTarget; // Scroll, maybe, but not if we're in the delete region. boolean inDeleteRegion = false; if (mDeleteRegion != null) { inDeleteRegion = mDeleteRegion.contains(x, y); } // After a scroll, the touch point will still be in the scroll region. // Rather than scrolling immediately, require a bit of twiddling to scroll again final int slop = ViewConfiguration.get(mContext).getScaledWindowTouchSlop(); mDistanceSinceScroll += Math.sqrt(Math.pow(mLastTouch[0] - x, 2) + Math.pow(mLastTouch[1] - y, 2)); mLastTouch[0] = x; mLastTouch[1] = y; if (!inDeleteRegion && x < mScrollZone) { if (mScrollState == SCROLL_OUTSIDE_ZONE && mDistanceSinceScroll > slop) { mScrollState = SCROLL_WAITING_IN_ZONE; mScrollRunnable.setDirection(SCROLL_LEFT); mHandler.postDelayed(mScrollRunnable, SCROLL_DELAY); mDragScroller.onEnterScrollArea(SCROLL_LEFT); } } else if (!inDeleteRegion && x > mScrollView.getWidth() - mScrollZone) { if (mScrollState == SCROLL_OUTSIDE_ZONE && mDistanceSinceScroll > slop) { mScrollState = SCROLL_WAITING_IN_ZONE; mScrollRunnable.setDirection(SCROLL_RIGHT); mHandler.postDelayed(mScrollRunnable, SCROLL_DELAY); mDragScroller.onEnterScrollArea(SCROLL_RIGHT); } } else { if (mScrollState == SCROLL_WAITING_IN_ZONE) { mScrollState = SCROLL_OUTSIDE_ZONE; mScrollRunnable.setDirection(SCROLL_RIGHT); mHandler.removeCallbacks(mScrollRunnable); mDragScroller.onExitScrollArea(); } } } /** * Call this from a drag source view. */ public boolean onTouchEvent(MotionEvent ev) { if (!mDragging) { return false; } final int action = ev.getAction(); final int screenX = clamp((int)ev.getRawX(), 0, mDisplayMetrics.widthPixels); final int screenY = clamp((int)ev.getRawY(), 0, mDisplayMetrics.heightPixels); switch (action) { case MotionEvent.ACTION_DOWN: // Remember where the motion event started mMotionDownX = screenX; mMotionDownY = screenY; if ((screenX < mScrollZone) || (screenX > mScrollView.getWidth() - mScrollZone)) { mScrollState = SCROLL_WAITING_IN_ZONE; mHandler.postDelayed(mScrollRunnable, SCROLL_DELAY); } else { mScrollState = SCROLL_OUTSIDE_ZONE; } break; case MotionEvent.ACTION_MOVE: handleMoveEvent(screenX, screenY); break; case MotionEvent.ACTION_UP: // Ensure that we've processed a move event at the current pointer location. handleMoveEvent(screenX, screenY); mHandler.removeCallbacks(mScrollRunnable); if (mDragging) { drop(screenX, screenY); } endDrag(); break; case MotionEvent.ACTION_CANCEL: cancelDrag(); break; } return true; } private void drop(float x, float y) { final int[] coordinates = mCoordinatesTemp; final DropTarget dropTarget = findDropTarget((int) x, (int) y, coordinates); boolean accepted = false; if (dropTarget != null) { dropTarget.onDragExit(mDragSource, coordinates[0], coordinates[1], (int) mTouchOffsetX, (int) mTouchOffsetY, mDragView, mDragInfo); if (dropTarget.acceptDrop(mDragSource, coordinates[0], coordinates[1], (int) mTouchOffsetX, (int) mTouchOffsetY, mDragView, mDragInfo)) { dropTarget.onDrop(mDragSource, coordinates[0], coordinates[1], (int) mTouchOffsetX, (int) mTouchOffsetY, mDragView, mDragInfo); accepted = true; } } mDragSource.onDropCompleted((View) dropTarget, mDragInfo, 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.getHitRect(r); // Convert the hit rect to screen coordinates target.getLocationOnScreen(dropCoordinates); r.offset(dropCoordinates[0] - target.getLeft(), dropCoordinates[1] - target.getTop()); if (r.contains(x, y)) { DropTarget delegate = target.getDropTargetDelegate(mDragSource, x, y, (int)mTouchOffsetX, (int)mTouchOffsetY, mDragView, mDragInfo); if (delegate != null) { target = delegate; target.getLocationOnScreen(dropCoordinates); } // Make dropCoordinates relative to the DropTarget dropCoordinates[0] = x - dropCoordinates[0]; dropCoordinates[1] = y - dropCoordinates[1]; return target; } } return null; } /** * Get the screen size so we can clamp events to the screen size so even if * you drag off the edge of the screen, we find something. */ private void recordScreenSize() { ((WindowManager)mContext.getSystemService(Context.WINDOW_SERVICE)) .getDefaultDisplay().getMetrics(mDisplayMetrics); } /** * Clamp val to be >= min and < max. */ private static int clamp(int val, int min, int max) { if (val < min) { return min; } else if (val >= max) { return max - 1; } else { return val; } } public void setDragScoller(DragScroller scroller) { mDragScroller = scroller; } public void setWindowToken(IBinder token) { mWindowToken = token; } /** * Sets the drag listner 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); } /** * Set which view scrolls for touch events near the edge of the screen. */ public void setScrollView(View v) { mScrollView = v; } /** * Specifies the delete region. We won't scroll on touch events over the delete region. * * @param region The rectangle in screen coordinates of the delete region. */ void setDeleteRegion(RectF region) { mDeleteRegion = region; } DragView getDragView() { return mDragView; } private class ScrollRunnable implements Runnable { private int mDirection; ScrollRunnable() { } public void run() { if (mDragScroller != null) { if (mDirection == SCROLL_LEFT) { mDragScroller.scrollLeft(); } else { mDragScroller.scrollRight(); } mScrollState = SCROLL_OUTSIDE_ZONE; mDistanceSinceScroll = 0; mDragScroller.onExitScrollArea(); } } void setDirection(int direction) { mDirection = direction; } } }