/* * 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.launcher; import android.content.Context; import android.graphics.Bitmap; import android.graphics.Canvas; import android.graphics.Matrix; import android.graphics.Rect; import android.graphics.RectF; import android.graphics.Paint; import android.graphics.PorterDuffColorFilter; import android.graphics.PorterDuff; import android.os.Vibrator; import android.os.SystemClock; import android.util.AttributeSet; import android.view.MotionEvent; import android.view.View; import android.view.ViewGroup; import android.view.KeyEvent; import android.widget.FrameLayout; /** * A ViewGroup that coordinated dragging across its dscendants */ public class DragLayer extends FrameLayout implements DragController { private static final int SCROLL_DELAY = 600; private static final int SCROLL_ZONE = 20; private static final int VIBRATE_DURATION = 35; private static final int ANIMATION_SCALE_UP_DURATION = 110; private static final boolean PROFILE_DRAWING_DURING_DRAG = false; // Number of pixels to add to the dragged item for scaling private static final float DRAG_SCALE = 24.0f; private boolean mDragging = false; private boolean mShouldDrop; private float mLastMotionX; private float mLastMotionY; /** * The bitmap that is currently being dragged */ private Bitmap mDragBitmap = null; private View mOriginator; private int mBitmapOffsetX; private int mBitmapOffsetY; /** * X offset from where we touched on the cell to its upper-left corner */ private float mTouchOffsetX; /** * Y offset from where we touched on the cell to its upper-left corner */ private float mTouchOffsetY; /** * Utility rectangle */ private Rect mDragRect = new Rect(); /** * Where the drag originated */ private DragSource mDragSource; /** * The data associated with the object being dragged */ private Object mDragInfo; private final Rect mRect = new Rect(); private final int[] mDropCoordinates = new int[2]; private final Vibrator mVibrator = new Vibrator(); private DragListener mListener; private DragScroller mDragScroller; private static final int SCROLL_OUTSIDE_ZONE = 0; private static final int SCROLL_WAITING_IN_ZONE = 1; private static final int SCROLL_LEFT = 0; private static final int SCROLL_RIGHT = 1; private int mScrollState = SCROLL_OUTSIDE_ZONE; private ScrollRunnable mScrollRunnable = new ScrollRunnable(); private View mIgnoredDropTarget; private RectF mDragRegion; private boolean mEnteredRegion; private DropTarget mLastDropTarget; private final Paint mTrashPaint = new Paint(); private Paint mDragPaint; private static final int ANIMATION_STATE_STARTING = 1; private static final int ANIMATION_STATE_RUNNING = 2; private static final int ANIMATION_STATE_DONE = 3; private static final int ANIMATION_TYPE_SCALE = 1; private float mAnimationFrom; private float mAnimationTo; private int mAnimationDuration; private long mAnimationStartTime; private int mAnimationType; private int mAnimationState = ANIMATION_STATE_DONE; /** * Used to create a new DragLayer from XML. * * @param context The application's context. * @param attrs The attribtues set containing the Workspace's customization values. */ public DragLayer(Context context, AttributeSet attrs) { super(context, attrs); final int srcColor = context.getResources().getColor(R.color.delete_color_filter); mTrashPaint.setColorFilter(new PorterDuffColorFilter(srcColor, PorterDuff.Mode.SRC_ATOP)); } public void startDrag(View v, DragSource source, Object dragInfo, int dragAction) { if (PROFILE_DRAWING_DURING_DRAG) { android.os.Debug.startMethodTracing("Launcher"); } if (mListener != null) { mListener.onDragStart(v, source, dragInfo, dragAction); } Rect r = mDragRect; r.set(v.getScrollX(), v.getScrollY(), 0, 0); offsetDescendantRectToMyCoords(v, r); mTouchOffsetX = mLastMotionX - r.left; mTouchOffsetY = mLastMotionY - r.top; v.clearFocus(); v.setPressed(false); boolean willNotCache = v.willNotCacheDrawing(); v.setWillNotCacheDrawing(false); v.buildDrawingCache(); Bitmap viewBitmap = v.getDrawingCache(); int width = viewBitmap.getWidth(); int height = viewBitmap.getHeight(); Matrix scale = new Matrix(); float scaleFactor = v.getWidth(); scaleFactor = (scaleFactor + DRAG_SCALE) /scaleFactor; scale.setScale(scaleFactor, scaleFactor); mAnimationTo = 1.0f; mAnimationFrom = 1.0f / scaleFactor; mAnimationDuration = ANIMATION_SCALE_UP_DURATION; mAnimationState = ANIMATION_STATE_STARTING; mAnimationType = ANIMATION_TYPE_SCALE; mDragBitmap = Bitmap.createBitmap(viewBitmap, 0, 0, width, height, scale, true); v.destroyDrawingCache(); v.setWillNotCacheDrawing(willNotCache); final Bitmap dragBitmap = mDragBitmap; mBitmapOffsetX = (dragBitmap.getWidth() - width) / 2; mBitmapOffsetY = (dragBitmap.getHeight() - height) / 2; if (dragAction == DRAG_ACTION_MOVE) { v.setVisibility(GONE); } mDragPaint = null; mDragging = true; mShouldDrop = true; mOriginator = v; mDragSource = source; mDragInfo = dragInfo; mVibrator.vibrate(VIBRATE_DURATION); mEnteredRegion = false; invalidate(); } @Override public boolean dispatchKeyEvent(KeyEvent event) { return mDragging || super.dispatchKeyEvent(event); } @Override protected void dispatchDraw(Canvas canvas) { super.dispatchDraw(canvas); if (mDragging && mDragBitmap != null) { if (mAnimationState == ANIMATION_STATE_STARTING) { mAnimationStartTime = SystemClock.uptimeMillis(); mAnimationState = ANIMATION_STATE_RUNNING; } if (mAnimationState == ANIMATION_STATE_RUNNING) { float normalized = (float) (SystemClock.uptimeMillis() - mAnimationStartTime) / mAnimationDuration; if (normalized >= 1.0f) { mAnimationState = ANIMATION_STATE_DONE; } normalized = Math.min(normalized, 1.0f); final float value = mAnimationFrom + (mAnimationTo - mAnimationFrom) * normalized; switch (mAnimationType) { case ANIMATION_TYPE_SCALE: final Bitmap dragBitmap = mDragBitmap; canvas.save(); canvas.translate(mScrollX + mLastMotionX - mTouchOffsetX - mBitmapOffsetX, mScrollY + mLastMotionY - mTouchOffsetY - mBitmapOffsetY); canvas.translate((dragBitmap.getWidth() * (1.0f - value)) / 2, (dragBitmap.getHeight() * (1.0f - value)) / 2); canvas.scale(value, value); canvas.drawBitmap(dragBitmap, 0.0f, 0.0f, mDragPaint); canvas.restore(); break; } } else { canvas.drawBitmap(mDragBitmap, mScrollX + mLastMotionX - mTouchOffsetX - mBitmapOffsetX, mScrollY + mLastMotionY - mTouchOffsetY - mBitmapOffsetY, mDragPaint); } } } private void endDrag() { if (mDragging) { mDragging = false; if (mDragBitmap != null) { mDragBitmap.recycle(); } if (mOriginator != null) { mOriginator.setVisibility(VISIBLE); } if (mListener != null) { mListener.onDragEnd(); } } } @Override public boolean onInterceptTouchEvent(MotionEvent ev) { final int action = ev.getAction(); final float x = ev.getX(); final float y = ev.getY(); switch (action) { case MotionEvent.ACTION_MOVE: break; case MotionEvent.ACTION_DOWN: // Remember location of down touch mLastMotionX = x; mLastMotionY = y; mLastDropTarget = null; break; case MotionEvent.ACTION_CANCEL: case MotionEvent.ACTION_UP: if (mShouldDrop && drop(x, y)) { mShouldDrop = false; } endDrag(); break; } return mDragging; } @Override public boolean onTouchEvent(MotionEvent ev) { if (!mDragging) { return false; } final int action = ev.getAction(); final float x = ev.getX(); final float y = ev.getY(); switch (action) { case MotionEvent.ACTION_DOWN: // Remember where the motion event started mLastMotionX = x; mLastMotionY = y; if ((x < SCROLL_ZONE) || (x > getWidth() - SCROLL_ZONE)) { mScrollState = SCROLL_WAITING_IN_ZONE; postDelayed(mScrollRunnable, SCROLL_DELAY); } else { mScrollState = SCROLL_OUTSIDE_ZONE; } break; case MotionEvent.ACTION_MOVE: final int scrollX = mScrollX; final int scrollY = mScrollY; final float touchX = mTouchOffsetX; final float touchY = mTouchOffsetY; final int offsetX = mBitmapOffsetX; final int offsetY = mBitmapOffsetY; int left = (int) (scrollX + mLastMotionX - touchX - offsetX); int top = (int) (scrollY + mLastMotionY - touchY - offsetY); final Bitmap dragBitmap = mDragBitmap; final int width = dragBitmap.getWidth(); final int height = dragBitmap.getHeight(); final Rect rect = mRect; rect.set(left - 1, top - 1, left + width + 1, top + height + 1); mLastMotionX = x; mLastMotionY = y; left = (int) (scrollX + x - touchX - offsetX); top = (int) (scrollY + y - touchY - offsetY); rect.union(left - 1, top - 1, left + width + 1, top + height + 1); invalidate(rect); final int[] coordinates = mDropCoordinates; DropTarget dropTarget = findDropTarget((int) x, (int) y, coordinates); if (dropTarget != null) { if (mLastDropTarget == dropTarget) { dropTarget.onDragOver(mDragSource, coordinates[0], coordinates[1], (int) mTouchOffsetX, (int) mTouchOffsetY, mDragInfo); } else { if (mLastDropTarget != null) { mLastDropTarget.onDragExit(mDragSource, coordinates[0], coordinates[1], (int) mTouchOffsetX, (int) mTouchOffsetY, mDragInfo); } dropTarget.onDragEnter(mDragSource, coordinates[0], coordinates[1], (int) mTouchOffsetX, (int) mTouchOffsetY, mDragInfo); } } else { if (mLastDropTarget != null) { mLastDropTarget.onDragExit(mDragSource, coordinates[0], coordinates[1], (int) mTouchOffsetX, (int) mTouchOffsetY, mDragInfo); } } mLastDropTarget = dropTarget; boolean inDragRegion = false; if (mDragRegion != null) { final RectF region = mDragRegion; final boolean inRegion = region.contains(ev.getRawX(), ev.getRawY()); if (!mEnteredRegion && inRegion) { mDragPaint = mTrashPaint; mEnteredRegion = true; inDragRegion = true; } else if (mEnteredRegion && !inRegion) { mDragPaint = null; mEnteredRegion = false; } } if (!inDragRegion && x < SCROLL_ZONE) { if (mScrollState == SCROLL_OUTSIDE_ZONE) { mScrollState = SCROLL_WAITING_IN_ZONE; mScrollRunnable.setDirection(SCROLL_LEFT); postDelayed(mScrollRunnable, SCROLL_DELAY); } } else if (!inDragRegion && x > getWidth() - SCROLL_ZONE) { if (mScrollState == SCROLL_OUTSIDE_ZONE) { mScrollState = SCROLL_WAITING_IN_ZONE; mScrollRunnable.setDirection(SCROLL_RIGHT); postDelayed(mScrollRunnable, SCROLL_DELAY); } } else { if (mScrollState == SCROLL_WAITING_IN_ZONE) { mScrollState = SCROLL_OUTSIDE_ZONE; mScrollRunnable.setDirection(SCROLL_RIGHT); removeCallbacks(mScrollRunnable); } } break; case MotionEvent.ACTION_UP: removeCallbacks(mScrollRunnable); if (mShouldDrop) { drop(x, y); mShouldDrop = false; } endDrag(); break; case MotionEvent.ACTION_CANCEL: endDrag(); } return true; } private boolean drop(float x, float y) { invalidate(); final int[] coordinates = mDropCoordinates; DropTarget dropTarget = findDropTarget((int) x, (int) y, coordinates); if (dropTarget != null) { dropTarget.onDragExit(mDragSource, coordinates[0], coordinates[1], (int) mTouchOffsetX, (int) mTouchOffsetY, mDragInfo); if (dropTarget.acceptDrop(mDragSource, coordinates[0], coordinates[1], (int) mTouchOffsetX, (int) mTouchOffsetY, mDragInfo)) { dropTarget.onDrop(mDragSource, coordinates[0], coordinates[1], (int) mTouchOffsetX, (int) mTouchOffsetY, mDragInfo); mDragSource.onDropCompleted((View) dropTarget, true); return true; } else { mDragSource.onDropCompleted((View) dropTarget, false); return true; } } return false; } DropTarget findDropTarget(int x, int y, int[] dropCoordinates) { return findDropTarget(this, x, y, dropCoordinates); } private DropTarget findDropTarget(ViewGroup container, int x, int y, int[] dropCoordinates) { final Rect r = mDragRect; final int count = container.getChildCount(); final int scrolledX = x + container.getScrollX(); final int scrolledY = y + container.getScrollY(); final View ignoredDropTarget = mIgnoredDropTarget; for (int i = count - 1; i >= 0; i--) { final View child = container.getChildAt(i); if (child.getVisibility() == VISIBLE && child != ignoredDropTarget) { child.getHitRect(r); if (r.contains(scrolledX, scrolledY)) { DropTarget target = null; if (child instanceof ViewGroup) { x = scrolledX - child.getLeft(); y = scrolledY - child.getTop(); target = findDropTarget((ViewGroup) child, x, y, dropCoordinates); } if (target == null) { if (child instanceof DropTarget) { dropCoordinates[0] = x; dropCoordinates[1] = y; return (DropTarget) child; } } else { return target; } } } } return null; } public void setDragScoller(DragScroller scroller) { mDragScroller = scroller; } public void setDragListener(DragListener l) { mListener = l; } public void removeDragListener(DragListener l) { mListener = null; } /** * Specifies the view that must be ignored when looking for a drop target. * * @param view The view that will not be taken into account while looking * for a drop target. */ void setIgnoredDropTarget(View view) { mIgnoredDropTarget = view; } /** * Specifies the delete region. * * @param region The rectangle in screen coordinates of the delete region. */ void setDeleteRegion(RectF region) { mDragRegion = region; } 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; } } void setDirection(int direction) { mDirection = direction; } } }