When dragging items on the home screen, visualize where they will drop.

Draw a rectangle around the area where an item will land if it is dropped.
If the area is vacant, the rectangle is green; otherwise, it is red.

Change-Id: I859b52514566fa55f8c7a04493b8088d12baa476
This commit is contained in:
Patrick Dubroy 2010-07-12 14:25:18 -07:00
parent 4ab2f860e7
commit 6569f2c80e
8 changed files with 249 additions and 58 deletions

Binary file not shown.

After

Width:  |  Height:  |  Size: 361 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 342 B

View File

@ -16,7 +16,7 @@
package com.android.launcher2;
import java.util.ArrayList;
import com.android.launcher.R;
import android.app.WallpaperManager;
import android.content.Context;
@ -25,6 +25,7 @@ import android.content.res.TypedArray;
import android.graphics.Canvas;
import android.graphics.Rect;
import android.graphics.RectF;
import android.graphics.drawable.Drawable;
import android.util.AttributeSet;
import android.view.ContextMenu;
import android.view.MotionEvent;
@ -34,7 +35,8 @@ import android.view.ViewGroup;
import android.view.animation.Animation;
import android.view.animation.LayoutAnimationController;
import com.android.launcher.R;
import java.util.ArrayList;
import java.util.Arrays;
public class CellLayout extends ViewGroup {
static final String TAG = "CellLayout";
@ -63,10 +65,25 @@ public class CellLayout extends ViewGroup {
private final Rect mRect = new Rect();
private final CellInfo mCellInfo = new CellInfo();
int[] mCellXY = new int[2];
// This is a temporary variable to prevent having to allocate a new object just to
// return an (x, y) value from helper functions. Do NOT use it to maintain other state.
private final int[] mTmpCellXY = new int[2];
boolean[][] mOccupied;
private RectF mDragRect = new RectF();
private final RectF mDragRect = new RectF();
// When dragging, used to indicate a vacant drop location
private Drawable mVacantDrawable;
// When dragging, used to indicate an occupied drop location
private Drawable mOccupiedDrawable;
// Updated to point to mVacantDrawable or mOccupiedDrawable, as appropriate
private Drawable mDragRectDrawable;
// When a drag operation is in progress, holds the nearest cell to the touch point
private final int[] mDragCell = new int[2];
private boolean mDirtyTag;
private boolean mLastDownOnOccupiedCell = false;
@ -83,6 +100,13 @@ public class CellLayout extends ViewGroup {
public CellLayout(Context context, AttributeSet attrs, int defStyle) {
super(context, attrs, defStyle);
// A ViewGroup usually does not draw, but CellLayout needs to draw a rectangle to show
// the user where a dragged item will land when dropped.
setWillNotDraw(false);
mVacantDrawable = getResources().getDrawable(R.drawable.rounded_rect_green);
mOccupiedDrawable = getResources().getDrawable(R.drawable.rounded_rect_red);
TypedArray a = context.obtainStyledAttributes(attrs, R.styleable.CellLayout, defStyle, 0);
mCellWidth = a.getDimensionPixelSize(R.styleable.CellLayout_cellWidth, 10);
@ -112,6 +136,18 @@ public class CellLayout extends ViewGroup {
super.dispatchDraw(canvas);
}
@Override
protected void onDraw(Canvas canvas) {
if (!mDragRect.isEmpty()) {
mDragRectDrawable.setBounds(
(int)mDragRect.left,
(int)mDragRect.top,
(int)mDragRect.right,
(int)mDragRect.bottom);
mDragRectDrawable.draw(canvas);
}
}
@Override
public void cancelLongPress() {
super.cancelLongPress();
@ -199,7 +235,7 @@ public class CellLayout extends ViewGroup {
mLastDownOnOccupiedCell = found;
if (!found) {
int cellXY[] = mCellXY;
final int cellXY[] = mTmpCellXY;
pointToCellExact(x, y, cellXY);
final boolean portrait = mPortrait;
@ -207,7 +243,7 @@ public class CellLayout extends ViewGroup {
final int yCount = portrait ? mLongAxisCells : mShortAxisCells;
final boolean[][] occupied = mOccupied;
findOccupiedCells(xCount, yCount, occupied, null);
findOccupiedCells(xCount, yCount, occupied, null, true);
cellInfo.cell = null;
cellInfo.cellX = cellXY[0];
@ -258,7 +294,7 @@ public class CellLayout extends ViewGroup {
final int yCount = portrait ? mLongAxisCells : mShortAxisCells;
final boolean[][] occupied = mOccupied;
findOccupiedCells(xCount, yCount, occupied, null);
findOccupiedCells(xCount, yCount, occupied, null, true);
findIntersectingVacantCells(info, info.cellX, info.cellY, xCount, yCount, occupied);
@ -340,6 +376,9 @@ public class CellLayout extends ViewGroup {
cellInfo.vacantCells.add(cell);
}
/**
* Check if the column 'x' is empty from rows 'top' to 'bottom', inclusive.
*/
private static boolean isColumnEmpty(int x, int top, int bottom, boolean[][] occupied) {
for (int y = top; y <= bottom; y++) {
if (occupied[x][y]) {
@ -349,6 +388,9 @@ public class CellLayout extends ViewGroup {
return true;
}
/**
* Check if the row 'y' is empty from columns 'left' to 'right', inclusive.
*/
private static boolean isRowEmpty(int y, int left, int right, boolean[][] occupied) {
for (int x = left; x <= right; x++) {
if (occupied[x][y]) {
@ -372,7 +414,7 @@ public class CellLayout extends ViewGroup {
}
}
} else {
findOccupiedCells(xCount, yCount, occupied, ignoreView);
findOccupiedCells(xCount, yCount, occupied, ignoreView, true);
}
CellInfo cellInfo = new CellInfo();
@ -600,7 +642,7 @@ public class CellLayout extends ViewGroup {
if (lp.dropped) {
lp.dropped = false;
final int[] cellXY = mCellXY;
final int[] cellXY = mTmpCellXY;
getLocationOnScreen(cellXY);
mWallpaperManager.sendWallpaperCommand(getWindowToken(), "android.home.drop",
cellXY[0] + childLeft + lp.width / 2,
@ -626,6 +668,79 @@ public class CellLayout extends ViewGroup {
super.setChildrenDrawnWithCacheEnabled(enabled);
}
private boolean isVacant(int originX, int originY, int spanX, int spanY) {
for (int i = 0; i < spanY; i++) {
if (!isRowEmpty(originY + i, originX, originX + spanX - 1, mOccupied)) {
return false;
}
}
return true;
}
/**
* Estimate where the top left cell of the dragged item will land if it is dropped.
*
* @param originX The X value of the top left corner of the item
* @param originY The Y value of the top left corner of the item
* @param spanX The number of horizontal cells that the item spans
* @param spanY The number of vertical cells that the item spans
* @param result The estimated drop cell X and Y.
*/
void estimateDropCell(int originX, int originY, int spanX, int spanY, int[] result) {
final int countX = getCountX();
final int countY = getCountY();
pointToCellRounded(originX, originY, result);
// If the item isn't fully on this screen, snap to the edges
int rightOverhang = result[0] + spanX - countX;
if (rightOverhang > 0) {
result[0] -= rightOverhang; // Snap to right
}
result[0] = Math.max(0, result[0]); // Snap to left
int bottomOverhang = result[1] + spanY - countY;
if (bottomOverhang > 0) {
result[1] -= bottomOverhang; // Snap to bottom
}
result[1] = Math.max(0, result[1]); // Snap to top
}
void visualizeDropLocation(View view, int originX, int originY, int spanX, int spanY) {
final int[] originCell = mDragCell;
final int[] cellXY = mTmpCellXY;
estimateDropCell(originX, originY, spanX, spanY, cellXY);
// Only recalculate the bounding rect when necessary
if (!Arrays.equals(cellXY, originCell)) {
originCell[0] = cellXY[0];
originCell[1] = cellXY[1];
// Find the top left corner of the rect the object will occupy
final int[] topLeft = mTmpCellXY;
cellToPoint(originCell[0], originCell[1], topLeft);
final int left = topLeft[0];
final int top = topLeft[1];
// Now find the bottom right
final int[] bottomRight = mTmpCellXY;
cellToPoint(originCell[0] + spanX - 1, originCell[1] + spanY - 1, bottomRight);
bottomRight[0] += mCellWidth;
bottomRight[1] += mCellHeight;
final int countX = mPortrait ? mShortAxisCells : mLongAxisCells;
final int countY = mPortrait ? mLongAxisCells : mShortAxisCells;
// TODO: It's not necessary to do this every time, but it's not especially expensive
findOccupiedCells(countX, countY, mOccupied, view, false);
boolean vacant = isVacant(originCell[0], originCell[1], spanX, spanY);
mDragRectDrawable = vacant ? mVacantDrawable : mOccupiedDrawable;
// mDragRect will be rendered in onDraw()
mDragRect.set(left, top, bottomRight[0], bottomRight[1]);
invalidate();
}
}
/**
* Find a vacant area that will fit the given bounds nearest the requested
* cell location. Uses Euclidean distance to score multiple vacant areas.
@ -644,7 +759,6 @@ public class CellLayout extends ViewGroup {
// Keep track of best-scoring drop area
final int[] bestXY = recycle != null ? recycle : new int[2];
final int[] cellXY = mCellXY;
double bestDistance = Double.MAX_VALUE;
// Bail early if vacant cells aren't valid
@ -662,7 +776,8 @@ public class CellLayout extends ViewGroup {
continue;
}
// Score is center distance from requested pixel
// Score is distance from requested pixel to the top left of each cell
final int[] cellXY = mTmpCellXY;
cellToPoint(cell.cellX, cell.cellY, cellXY);
double distance = Math.sqrt(Math.pow(cellXY[0] - pixelX, 2)
@ -682,6 +797,18 @@ public class CellLayout extends ViewGroup {
}
}
/**
* Called when a drag and drop operation has finished (successfully or not).
*/
void onDragComplete() {
// Invalidate the drag data
mDragCell[0] = -1;
mDragCell[1] = -1;
mDragRect.setEmpty();
invalidate();
}
/**
* Mark a child as having been dropped.
*
@ -694,16 +821,15 @@ public class CellLayout extends ViewGroup {
lp.dropped = true;
mDragRect.setEmpty();
child.requestLayout();
invalidate();
}
onDragComplete();
}
void onDropAborted(View child) {
if (child != null) {
((LayoutParams) child.getLayoutParams()).isDragging = false;
invalidate();
}
mDragRect.setEmpty();
onDragComplete();
}
/**
@ -717,21 +843,6 @@ public class CellLayout extends ViewGroup {
mDragRect.setEmpty();
}
/**
* Drag a child over the specified position
*
* @param child The child that is being dropped
* @param cellX The child's new x cell location
* @param cellY The child's new y cell location
*/
void onDragOverChild(View child, int cellX, int cellY) {
int[] cellXY = mCellXY;
pointToCellRounded(cellX, cellY, cellXY);
LayoutParams lp = (LayoutParams) child.getLayoutParams();
cellToRect(cellXY[0], cellXY[1], lp.cellHSpan, lp.cellVSpan, mDragRect);
invalidate();
}
/**
* Computes a bounding rectangle for a range of cells
*
@ -739,9 +850,10 @@ public class CellLayout extends ViewGroup {
* @param cellY Y coordinate of upper left corner expressed as a cell position
* @param cellHSpan Width in cells
* @param cellVSpan Height in cells
* @param dragRect Rectnagle into which to put the results
* @param resultRect Rect into which to put the results
*/
public void cellToRect(int cellX, int cellY, int cellHSpan, int cellVSpan, RectF dragRect) {
public void cellToRect(int cellX, int cellY, int cellHSpan, int cellVSpan, RectF resultRect) {
final boolean portrait = mPortrait;
final int cellWidth = mCellWidth;
final int cellHeight = mCellHeight;
final int widthGap = mWidthGap;
@ -756,7 +868,7 @@ public class CellLayout extends ViewGroup {
int x = hStartPadding + cellX * (cellWidth + widthGap);
int y = vStartPadding + cellY * (cellHeight + heightGap);
dragRect.set(x, y, x + width, y + height);
resultRect.set(x, y, x + width, y + height);
}
/**
@ -796,7 +908,7 @@ public class CellLayout extends ViewGroup {
final int yCount = portrait ? mLongAxisCells : mShortAxisCells;
final boolean[][] occupied = mOccupied;
findOccupiedCells(xCount, yCount, occupied, null);
findOccupiedCells(xCount, yCount, occupied, null, true);
return findVacantCell(vacant, spanX, spanY, xCount, yCount, occupied);
}
@ -825,13 +937,16 @@ out: for (int i = x; i < x + spanX - 1 && x < xCount; i++) {
return false;
}
boolean[] getOccupiedCells() {
/**
* Update the array of occupied cells (mOccupied), and return a flattened copy of the array.
*/
boolean[] getOccupiedCellsFlattened() {
final boolean portrait = mPortrait;
final int xCount = portrait ? mShortAxisCells : mLongAxisCells;
final int yCount = portrait ? mLongAxisCells : mShortAxisCells;
final boolean[][] occupied = mOccupied;
findOccupiedCells(xCount, yCount, occupied, null);
findOccupiedCells(xCount, yCount, occupied, null, true);
final boolean[] flat = new boolean[xCount * yCount];
for (int y = 0; y < yCount; y++) {
@ -843,7 +958,14 @@ out: for (int i = x; i < x + spanX - 1 && x < xCount; i++) {
return flat;
}
private void findOccupiedCells(int xCount, int yCount, boolean[][] occupied, View ignoreView) {
/**
* Update the array of occupied cells.
* @param ignoreView If non-null, the space occupied by this View is treated as vacant
* @param ignoreFolders If true, a cell occupied by a Folder is treated as vacant
*/
private void findOccupiedCells(
int xCount, int yCount, boolean[][] occupied, View ignoreView, boolean ignoreFolders) {
for (int x = 0; x < xCount; x++) {
for (int y = 0; y < yCount; y++) {
occupied[x][y] = false;
@ -853,7 +975,7 @@ out: for (int i = x; i < x + spanX - 1 && x < xCount; i++) {
int count = getChildCount();
for (int i = 0; i < count; i++) {
View child = getChildAt(i);
if (child instanceof Folder || child.equals(ignoreView)) {
if ((ignoreFolders && child instanceof Folder) || child.equals(ignoreView)) {
continue;
}
LayoutParams lp = (LayoutParams) child.getLayoutParams();

View File

@ -1088,7 +1088,7 @@ public final class Launcher extends Activity
outState.putInt(RUNTIME_STATE_PENDING_ADD_COUNT_X, layout.getCountX());
outState.putInt(RUNTIME_STATE_PENDING_ADD_COUNT_Y, layout.getCountY());
outState.putBooleanArray(RUNTIME_STATE_PENDING_ADD_OCCUPIED_CELLS,
layout.getOccupiedCells());
layout.getOccupiedCellsFlattened());
}
if (mFolderInfo != null && mWaitingForResult) {

View File

@ -21,23 +21,46 @@ import android.content.ComponentName;
import android.content.ContentValues;
/**
* Represents a widget, which just contains an identifier.
* Represents a widget (either instantiated or about to be) in the Launcher.
*/
class LauncherAppWidgetInfo extends ItemInfo {
/**
* Indicates that the widget hasn't been instantiated yet.
*/
static final int NO_ID = -1;
/**
* Identifier for this widget when talking with
* {@link android.appwidget.AppWidgetManager} for updates.
*/
int appWidgetId;
int appWidgetId = NO_ID;
ComponentName providerName;
// TODO: Are these necessary here?
int minWidth = -1;
int minHeight = -1;
/**
* View that holds this widget after it's been created. This view isn't created
* until Launcher knows it's needed.
*/
AppWidgetHostView hostView = null;
/**
* Constructor for use with AppWidgets that haven't been instantiated yet.
*/
LauncherAppWidgetInfo(ComponentName providerName) {
itemType = LauncherSettings.Favorites.ITEM_TYPE_APPWIDGET;
this.providerName = providerName;
// Since the widget isn't instantiated yet, we don't know these values. Set them to -1
// to indicate that they should be calculated based on the layout and minWidth/minHeight
spanX = -1;
spanY = -1;
}
LauncherAppWidgetInfo(int appWidgetId) {
itemType = LauncherSettings.Favorites.ITEM_TYPE_APPWIDGET;
this.appWidgetId = appWidgetId;
@ -54,7 +77,6 @@ class LauncherAppWidgetInfo extends ItemInfo {
return "AppWidget(id=" + Integer.toString(appWidgetId) + ")";
}
@Override
void unbind() {
super.unbind();

View File

@ -34,8 +34,11 @@ public class WidgetChooser extends HomeCustomizationItemGallery implements DragS
int screenX = mMotionDownRawX - (w / 2);
int screenY = mMotionDownRawY - h;
LauncherAppWidgetInfo dragInfo = new LauncherAppWidgetInfo(-1);
dragInfo.providerName = info.provider;
AppWidgetProviderInfo appWidgetInfo = (AppWidgetProviderInfo)view.getTag();
LauncherAppWidgetInfo dragInfo = new LauncherAppWidgetInfo(info.provider);
// TODO: Is this really the best place to do this?
dragInfo.minWidth = appWidgetInfo.minWidth;
dragInfo.minHeight = appWidgetInfo.minHeight;
mDragController.startDrag(bmp, screenX, screenY,
0, 0, w, h, this, dragInfo, DragController.DRAG_ACTION_COPY);
return true;

View File

@ -85,6 +85,8 @@ public class WidgetListAdapter extends BaseAdapter {
image.setBounds(0, 0, image.getIntrinsicWidth(), image.getIntrinsicHeight());
textView.setCompoundDrawables(null, image, null, null);
textView.setText(info.label);
// Store the widget info on the associated view so we can easily fetch it later
textView.setTag(info);
return textView;
}

View File

@ -28,6 +28,7 @@ import android.content.pm.PackageManager;
import android.content.pm.ProviderInfo;
import android.content.res.TypedArray;
import android.graphics.Canvas;
import android.graphics.Paint;
import android.graphics.Rect;
import android.graphics.drawable.Drawable;
import android.net.Uri;
@ -43,9 +44,9 @@ import android.view.ViewConfiguration;
import android.view.ViewGroup;
import android.view.ViewParent;
import android.view.animation.Animation;
import android.view.animation.Animation.AnimationListener;
import android.view.animation.Interpolator;
import android.view.animation.RotateAnimation;
import android.view.animation.Animation.AnimationListener;
import android.widget.Scroller;
import android.widget.TextView;
import android.widget.Toast;
@ -90,6 +91,11 @@ public class Workspace extends ViewGroup implements DropTarget, DragSource, Drag
*/
private int[] mTargetCell = null;
/**
* The CellLayout that is currently being dragged over
*/
private CellLayout mDragTargetLayout = null;
private float mLastMotionX;
private float mLastMotionY;
@ -136,6 +142,8 @@ public class Workspace extends ViewGroup implements DropTarget, DragSource, Drag
private static final float BASELINE_FLING_VELOCITY = 2500.f;
private static final float FLING_VELOCITY_INFLUENCE = 0.4f;
private Paint mDropIndicatorPaint;
private static class WorkspaceOvershootInterpolator implements Interpolator {
private static final float DEFAULT_TENSION = 1.3f;
private float mTension;
@ -480,13 +488,12 @@ public class Workspace extends ViewGroup implements DropTarget, DragSource, Drag
lp.cellVSpan = spanY;
}
// get the canonical child id to uniquely represent this view in this
// screen
// Get the canonical child id to uniquely represent this view in this screen
int childId = LauncherModel.getCanonicalCellLayoutChildId(child.getId(), screen, x, y, spanX, spanY);
if (!group.addViewToCellLayout(child, insert ? 0 : -1, childId, lp)) {
// TODO: This branch occurs when the workspace is adding views
// outside of the defined grid
// maybe we should be deleting these items from the LauncherMode?
// maybe we should be deleting these items from the LauncherModel?
Log.w(TAG, "Failed to add to item at (" + lp.cellX + "," + lp.cellY + ") to CellLayout");
}
@ -1212,13 +1219,45 @@ public class Workspace extends ViewGroup implements DropTarget, DragSource, Drag
clearVacantCache();
}
public void onDragOver(DragSource source, int x, int y, int xOffset,
int yOffset, DragView dragView, Object dragInfo) {
public void onDragOver(DragSource source, int x, int y, int xOffset, int yOffset,
DragView dragView, Object dragInfo) {
ItemInfo item = (ItemInfo)dragInfo;
CellLayout currentLayout = getCurrentDropLayout();
if (dragInfo instanceof LauncherAppWidgetInfo) {
LauncherAppWidgetInfo widgetInfo = (LauncherAppWidgetInfo)dragInfo;
if (widgetInfo.spanX == -1) {
// Calculate the grid spans needed to fit this widget
int[] spans = currentLayout.rectToCell(widgetInfo.minWidth, widgetInfo.minHeight);
item.spanX = spans[0];
item.spanY = spans[1];
}
}
if (currentLayout != mDragTargetLayout) {
if (mDragTargetLayout != null) {
mDragTargetLayout.onDragComplete();
}
mDragTargetLayout = currentLayout;
}
// Find the top left corner of the item
int originX = x - xOffset;
int originY = y - yOffset;
final View child = (mDragInfo == null) ? null : mDragInfo.cell;
currentLayout.visualizeDropLocation(child, originX, originY, item.spanX, item.spanY);
}
public void onDragExit(DragSource source, int x, int y, int xOffset,
int yOffset, DragView dragView, Object dragInfo) {
clearVacantCache();
if (mDragTargetLayout != null) {
mDragTargetLayout.onDragComplete();
mDragTargetLayout = null;
}
}
private void onDropExternal(int x, int y, Object dragInfo,
@ -1257,16 +1296,15 @@ public class Workspace extends ViewGroup implements DropTarget, DragSource, Drag
+ info.itemType);
}
// addAppWidgetFromDrop already took care of attaching the widget view to the appropriate cell
// TODO why aren't we calling addInScreen here?
if (info.itemType != LauncherSettings.Favorites.ITEM_TYPE_APPWIDGET) {
mTargetCell = estimateDropCell(x, y, 1, 1, view, cellLayout,
mTargetCell);
// If the view is null, it has already been added.
if (view == null) {
cellLayout.onDragComplete();
} else {
mTargetCell = estimateDropCell(x, y, 1, 1, view, cellLayout, mTargetCell);
addInScreen(view, indexOfChild(cellLayout), mTargetCell[0],
mTargetCell[1], info.spanX, info.spanY, insertAtFirst);
cellLayout.onDropChild(view);
CellLayout.LayoutParams lp = (CellLayout.LayoutParams) view
.getLayoutParams();
CellLayout.LayoutParams lp = (CellLayout.LayoutParams) view.getLayoutParams();
LauncherModel.addOrMoveItemInDatabase(mLauncher, info,
LauncherSettings.Favorites.CONTAINER_DESKTOP, mCurrentScreen,
@ -1344,14 +1382,18 @@ public class Workspace extends ViewGroup implements DropTarget, DragSource, Drag
*/
private int[] estimateDropCell(int pixelX, int pixelY,
int spanX, int spanY, View ignoreView, CellLayout layout, int[] recycle) {
final int[] cellXY = mTempCell;
layout.estimateDropCell(pixelX, pixelY, spanX, spanY, cellXY);
layout.cellToPoint(cellXY[0], cellXY[1], mTempEstimate);
// Create vacant cell cache if none exists
if (mVacantCache == null) {
mVacantCache = layout.findAllVacantCells(null, ignoreView);
}
// Find the best target drop location
return layout.findNearestVacantArea(pixelX, pixelY,
spanX, spanY, mVacantCache, recycle);
return layout.findNearestVacantArea(mTempEstimate[0], mTempEstimate[1], spanX, spanY, mVacantCache, recycle);
}
void setLauncher(Launcher launcher) {