CellLayout.java revision 19f3792523fe2d55ea791a9286398a6120920690
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 17package com.android.launcher2; 18 19import android.animation.Animator; 20import android.animation.AnimatorListenerAdapter; 21import android.animation.TimeInterpolator; 22import android.animation.ValueAnimator; 23import android.animation.ValueAnimator.AnimatorUpdateListener; 24import android.content.Context; 25import android.content.res.Resources; 26import android.content.res.TypedArray; 27import android.graphics.Bitmap; 28import android.graphics.Canvas; 29import android.graphics.Color; 30import android.graphics.Paint; 31import android.graphics.Point; 32import android.graphics.PointF; 33import android.graphics.PorterDuff; 34import android.graphics.PorterDuffXfermode; 35import android.graphics.Rect; 36import android.graphics.drawable.ColorDrawable; 37import android.graphics.drawable.Drawable; 38import android.graphics.drawable.NinePatchDrawable; 39import android.util.AttributeSet; 40import android.util.Log; 41import android.view.MotionEvent; 42import android.view.View; 43import android.view.ViewDebug; 44import android.view.ViewGroup; 45import android.view.animation.Animation; 46import android.view.animation.DecelerateInterpolator; 47import android.view.animation.LayoutAnimationController; 48 49import com.android.launcher.R; 50import com.android.launcher2.FolderIcon.FolderRingAnimator; 51 52import java.util.ArrayList; 53import java.util.Arrays; 54import java.util.HashMap; 55import java.util.Stack; 56 57public class CellLayout extends ViewGroup { 58 static final String TAG = "CellLayout"; 59 60 private int mOriginalCellWidth; 61 private int mOriginalCellHeight; 62 private int mCellWidth; 63 private int mCellHeight; 64 65 private int mCountX; 66 private int mCountY; 67 68 private int mOriginalWidthGap; 69 private int mOriginalHeightGap; 70 private int mWidthGap; 71 private int mHeightGap; 72 private int mMaxGap; 73 private boolean mScrollingTransformsDirty = false; 74 75 private final Rect mRect = new Rect(); 76 private final CellInfo mCellInfo = new CellInfo(); 77 78 // These are temporary variables to prevent having to allocate a new object just to 79 // return an (x, y) value from helper functions. Do NOT use them to maintain other state. 80 private final int[] mTmpXY = new int[2]; 81 private final int[] mTmpPoint = new int[2]; 82 private final PointF mTmpPointF = new PointF(); 83 int[] mTempLocation = new int[2]; 84 85 boolean[][] mOccupied; 86 boolean[][] mTmpOccupied; 87 private boolean mLastDownOnOccupiedCell = false; 88 89 private OnTouchListener mInterceptTouchListener; 90 91 private ArrayList<FolderRingAnimator> mFolderOuterRings = new ArrayList<FolderRingAnimator>(); 92 private int[] mFolderLeaveBehindCell = {-1, -1}; 93 94 private int mForegroundAlpha = 0; 95 private float mBackgroundAlpha; 96 private float mBackgroundAlphaMultiplier = 1.0f; 97 98 private Drawable mNormalBackground; 99 private Drawable mActiveGlowBackground; 100 private Drawable mOverScrollForegroundDrawable; 101 private Drawable mOverScrollLeft; 102 private Drawable mOverScrollRight; 103 private Rect mBackgroundRect; 104 private Rect mForegroundRect; 105 private int mForegroundPadding; 106 107 // If we're actively dragging something over this screen, mIsDragOverlapping is true 108 private boolean mIsDragOverlapping = false; 109 private final Point mDragCenter = new Point(); 110 111 // These arrays are used to implement the drag visualization on x-large screens. 112 // They are used as circular arrays, indexed by mDragOutlineCurrent. 113 private Rect[] mDragOutlines = new Rect[4]; 114 private float[] mDragOutlineAlphas = new float[mDragOutlines.length]; 115 private InterruptibleInOutAnimator[] mDragOutlineAnims = 116 new InterruptibleInOutAnimator[mDragOutlines.length]; 117 118 // Used as an index into the above 3 arrays; indicates which is the most current value. 119 private int mDragOutlineCurrent = 0; 120 private final Paint mDragOutlinePaint = new Paint(); 121 122 private BubbleTextView mPressedOrFocusedIcon; 123 124 private Drawable mCrosshairsDrawable = null; 125 private InterruptibleInOutAnimator mCrosshairsAnimator = null; 126 private float mCrosshairsVisibility = 0.0f; 127 128 private HashMap<CellLayout.LayoutParams, Animator> mReorderAnimators = new 129 HashMap<CellLayout.LayoutParams, Animator>(); 130 private HashMap<View, ReorderHintAnimation> 131 mShakeAnimators = new HashMap<View, ReorderHintAnimation>(); 132 133 private boolean mItemPlacementDirty = false; 134 135 // When a drag operation is in progress, holds the nearest cell to the touch point 136 private final int[] mDragCell = new int[2]; 137 138 private boolean mDragging = false; 139 140 private TimeInterpolator mEaseOutInterpolator; 141 private ShortcutAndWidgetContainer mShortcutsAndWidgets; 142 143 private boolean mIsHotseat = false; 144 private float mChildScale = 1f; 145 private float mHotseatChildScale = 1f; 146 147 public static final int MODE_DRAG_OVER = 0; 148 public static final int MODE_ON_DROP = 1; 149 public static final int MODE_ON_DROP_EXTERNAL = 2; 150 public static final int MODE_ACCEPT_DROP = 3; 151 private static final boolean DESTRUCTIVE_REORDER = false; 152 private static final boolean DEBUG_VISUALIZE_OCCUPIED = false; 153 154 private static final float REORDER_HINT_MAGNITUDE = 0.27f; 155 private static final int REORDER_ANIMATION_DURATION = 150; 156 private float mReorderHintAnimationMagnitude; 157 158 private ArrayList<View> mIntersectingViews = new ArrayList<View>(); 159 private Rect mOccupiedRect = new Rect(); 160 private int[] mDirectionVector = new int[2]; 161 int[] mPreviousReorderDirection = new int[2]; 162 163 public CellLayout(Context context) { 164 this(context, null); 165 } 166 167 public CellLayout(Context context, AttributeSet attrs) { 168 this(context, attrs, 0); 169 } 170 171 public CellLayout(Context context, AttributeSet attrs, int defStyle) { 172 super(context, attrs, defStyle); 173 174 // A ViewGroup usually does not draw, but CellLayout needs to draw a rectangle to show 175 // the user where a dragged item will land when dropped. 176 setWillNotDraw(false); 177 178 TypedArray a = context.obtainStyledAttributes(attrs, R.styleable.CellLayout, defStyle, 0); 179 180 mOriginalCellWidth = 181 mCellWidth = a.getDimensionPixelSize(R.styleable.CellLayout_cellWidth, 10); 182 mOriginalCellHeight = 183 mCellHeight = a.getDimensionPixelSize(R.styleable.CellLayout_cellHeight, 10); 184 mWidthGap = mOriginalWidthGap = a.getDimensionPixelSize(R.styleable.CellLayout_widthGap, 0); 185 mHeightGap = mOriginalHeightGap = a.getDimensionPixelSize(R.styleable.CellLayout_heightGap, 0); 186 mMaxGap = a.getDimensionPixelSize(R.styleable.CellLayout_maxGap, 0); 187 mCountX = LauncherModel.getCellCountX(); 188 mCountY = LauncherModel.getCellCountY(); 189 mOccupied = new boolean[mCountX][mCountY]; 190 mTmpOccupied = new boolean[mCountX][mCountY]; 191 192 a.recycle(); 193 194 setAlwaysDrawnWithCacheEnabled(false); 195 196 final Resources res = getResources(); 197 198 mNormalBackground = res.getDrawable(R.drawable.homescreen_blue_normal_holo); 199 mActiveGlowBackground = res.getDrawable(R.drawable.homescreen_blue_strong_holo); 200 201 mOverScrollLeft = res.getDrawable(R.drawable.overscroll_glow_left); 202 mOverScrollRight = res.getDrawable(R.drawable.overscroll_glow_right); 203 mForegroundPadding = 204 res.getDimensionPixelSize(R.dimen.workspace_overscroll_drawable_padding); 205 206 mReorderHintAnimationMagnitude = (REORDER_HINT_MAGNITUDE * 207 res.getDimensionPixelSize(R.dimen.app_icon_size)); 208 209 mNormalBackground.setFilterBitmap(true); 210 mActiveGlowBackground.setFilterBitmap(true); 211 212 int iconScale = res.getInteger(R.integer.app_icon_scale_percent); 213 if (iconScale >= 0) { 214 mChildScale = iconScale / 100f; 215 } 216 int hotseatIconScale = res.getInteger(R.integer.app_icon_hotseat_scale_percent); 217 if (hotseatIconScale >= 0) { 218 mHotseatChildScale = hotseatIconScale / 100f; 219 } 220 221 // Initialize the data structures used for the drag visualization. 222 223 mCrosshairsDrawable = res.getDrawable(R.drawable.gardening_crosshairs); 224 mEaseOutInterpolator = new DecelerateInterpolator(2.5f); // Quint ease out 225 226 // Set up the animation for fading the crosshairs in and out 227 int animDuration = res.getInteger(R.integer.config_crosshairsFadeInTime); 228 mCrosshairsAnimator = new InterruptibleInOutAnimator(animDuration, 0.0f, 1.0f); 229 mCrosshairsAnimator.getAnimator().addUpdateListener(new AnimatorUpdateListener() { 230 public void onAnimationUpdate(ValueAnimator animation) { 231 mCrosshairsVisibility = ((Float) animation.getAnimatedValue()).floatValue(); 232 invalidate(); 233 } 234 }); 235 mCrosshairsAnimator.getAnimator().setInterpolator(mEaseOutInterpolator); 236 237 mDragCell[0] = mDragCell[1] = -1; 238 for (int i = 0; i < mDragOutlines.length; i++) { 239 mDragOutlines[i] = new Rect(-1, -1, -1, -1); 240 } 241 242 // When dragging things around the home screens, we show a green outline of 243 // where the item will land. The outlines gradually fade out, leaving a trail 244 // behind the drag path. 245 // Set up all the animations that are used to implement this fading. 246 final int duration = res.getInteger(R.integer.config_dragOutlineFadeTime); 247 final float fromAlphaValue = 0; 248 final float toAlphaValue = (float)res.getInteger(R.integer.config_dragOutlineMaxAlpha); 249 250 Arrays.fill(mDragOutlineAlphas, fromAlphaValue); 251 252 for (int i = 0; i < mDragOutlineAnims.length; i++) { 253 final InterruptibleInOutAnimator anim = 254 new InterruptibleInOutAnimator(duration, fromAlphaValue, toAlphaValue); 255 anim.getAnimator().setInterpolator(mEaseOutInterpolator); 256 final int thisIndex = i; 257 anim.getAnimator().addUpdateListener(new AnimatorUpdateListener() { 258 public void onAnimationUpdate(ValueAnimator animation) { 259 final Bitmap outline = (Bitmap)anim.getTag(); 260 261 // If an animation is started and then stopped very quickly, we can still 262 // get spurious updates we've cleared the tag. Guard against this. 263 if (outline == null) { 264 if (false) { 265 Object val = animation.getAnimatedValue(); 266 Log.d(TAG, "anim " + thisIndex + " update: " + val + 267 ", isStopped " + anim.isStopped()); 268 } 269 // Try to prevent it from continuing to run 270 animation.cancel(); 271 } else { 272 mDragOutlineAlphas[thisIndex] = (Float) animation.getAnimatedValue(); 273 CellLayout.this.invalidate(mDragOutlines[thisIndex]); 274 } 275 } 276 }); 277 // The animation holds a reference to the drag outline bitmap as long is it's 278 // running. This way the bitmap can be GCed when the animations are complete. 279 anim.getAnimator().addListener(new AnimatorListenerAdapter() { 280 @Override 281 public void onAnimationEnd(Animator animation) { 282 if ((Float) ((ValueAnimator) animation).getAnimatedValue() == 0f) { 283 anim.setTag(null); 284 } 285 } 286 }); 287 mDragOutlineAnims[i] = anim; 288 } 289 290 mBackgroundRect = new Rect(); 291 mForegroundRect = new Rect(); 292 293 mShortcutsAndWidgets = new ShortcutAndWidgetContainer(context); 294 mShortcutsAndWidgets.setCellDimensions(mCellWidth, mCellHeight, mWidthGap, mHeightGap); 295 addView(mShortcutsAndWidgets); 296 } 297 298 static int widthInPortrait(Resources r, int numCells) { 299 // We use this method from Workspace to figure out how many rows/columns Launcher should 300 // have. We ignore the left/right padding on CellLayout because it turns out in our design 301 // the padding extends outside the visible screen size, but it looked fine anyway. 302 int cellWidth = r.getDimensionPixelSize(R.dimen.workspace_cell_width); 303 int minGap = Math.min(r.getDimensionPixelSize(R.dimen.workspace_width_gap), 304 r.getDimensionPixelSize(R.dimen.workspace_height_gap)); 305 306 return minGap * (numCells - 1) + cellWidth * numCells; 307 } 308 309 static int heightInLandscape(Resources r, int numCells) { 310 // We use this method from Workspace to figure out how many rows/columns Launcher should 311 // have. We ignore the left/right padding on CellLayout because it turns out in our design 312 // the padding extends outside the visible screen size, but it looked fine anyway. 313 int cellHeight = r.getDimensionPixelSize(R.dimen.workspace_cell_height); 314 int minGap = Math.min(r.getDimensionPixelSize(R.dimen.workspace_width_gap), 315 r.getDimensionPixelSize(R.dimen.workspace_height_gap)); 316 317 return minGap * (numCells - 1) + cellHeight * numCells; 318 } 319 320 public void enableHardwareLayers() { 321 mShortcutsAndWidgets.enableHardwareLayers(); 322 } 323 324 public void setGridSize(int x, int y) { 325 mCountX = x; 326 mCountY = y; 327 mOccupied = new boolean[mCountX][mCountY]; 328 mTmpOccupied = new boolean[mCountX][mCountY]; 329 requestLayout(); 330 } 331 332 private void invalidateBubbleTextView(BubbleTextView icon) { 333 final int padding = icon.getPressedOrFocusedBackgroundPadding(); 334 invalidate(icon.getLeft() + getPaddingLeft() - padding, 335 icon.getTop() + getPaddingTop() - padding, 336 icon.getRight() + getPaddingLeft() + padding, 337 icon.getBottom() + getPaddingTop() + padding); 338 } 339 340 void setOverScrollAmount(float r, boolean left) { 341 if (left && mOverScrollForegroundDrawable != mOverScrollLeft) { 342 mOverScrollForegroundDrawable = mOverScrollLeft; 343 } else if (!left && mOverScrollForegroundDrawable != mOverScrollRight) { 344 mOverScrollForegroundDrawable = mOverScrollRight; 345 } 346 347 mForegroundAlpha = (int) Math.round((r * 255)); 348 mOverScrollForegroundDrawable.setAlpha(mForegroundAlpha); 349 invalidate(); 350 } 351 352 void setPressedOrFocusedIcon(BubbleTextView icon) { 353 // We draw the pressed or focused BubbleTextView's background in CellLayout because it 354 // requires an expanded clip rect (due to the glow's blur radius) 355 BubbleTextView oldIcon = mPressedOrFocusedIcon; 356 mPressedOrFocusedIcon = icon; 357 if (oldIcon != null) { 358 invalidateBubbleTextView(oldIcon); 359 } 360 if (mPressedOrFocusedIcon != null) { 361 invalidateBubbleTextView(mPressedOrFocusedIcon); 362 } 363 } 364 365 void setIsDragOverlapping(boolean isDragOverlapping) { 366 if (mIsDragOverlapping != isDragOverlapping) { 367 mIsDragOverlapping = isDragOverlapping; 368 invalidate(); 369 } 370 } 371 372 boolean getIsDragOverlapping() { 373 return mIsDragOverlapping; 374 } 375 376 protected void setOverscrollTransformsDirty(boolean dirty) { 377 mScrollingTransformsDirty = dirty; 378 } 379 380 protected void resetOverscrollTransforms() { 381 if (mScrollingTransformsDirty) { 382 setOverscrollTransformsDirty(false); 383 setTranslationX(0); 384 setRotationY(0); 385 // It doesn't matter if we pass true or false here, the important thing is that we 386 // pass 0, which results in the overscroll drawable not being drawn any more. 387 setOverScrollAmount(0, false); 388 setPivotX(getMeasuredWidth() / 2); 389 setPivotY(getMeasuredHeight() / 2); 390 } 391 } 392 393 @Override 394 protected void onDraw(Canvas canvas) { 395 // When we're large, we are either drawn in a "hover" state (ie when dragging an item to 396 // a neighboring page) or with just a normal background (if backgroundAlpha > 0.0f) 397 // When we're small, we are either drawn normally or in the "accepts drops" state (during 398 // a drag). However, we also drag the mini hover background *over* one of those two 399 // backgrounds 400 if (mBackgroundAlpha > 0.0f) { 401 Drawable bg; 402 403 if (mIsDragOverlapping) { 404 // In the mini case, we draw the active_glow bg *over* the active background 405 bg = mActiveGlowBackground; 406 } else { 407 bg = mNormalBackground; 408 } 409 410 bg.setAlpha((int) (mBackgroundAlpha * mBackgroundAlphaMultiplier * 255)); 411 bg.setBounds(mBackgroundRect); 412 bg.draw(canvas); 413 } 414 415 if (mCrosshairsVisibility > 0.0f) { 416 final int countX = mCountX; 417 final int countY = mCountY; 418 419 final float MAX_ALPHA = 0.4f; 420 final int MAX_VISIBLE_DISTANCE = 600; 421 final float DISTANCE_MULTIPLIER = 0.002f; 422 423 final Drawable d = mCrosshairsDrawable; 424 final int width = d.getIntrinsicWidth(); 425 final int height = d.getIntrinsicHeight(); 426 427 int x = getPaddingLeft() - (mWidthGap / 2) - (width / 2); 428 for (int col = 0; col <= countX; col++) { 429 int y = getPaddingTop() - (mHeightGap / 2) - (height / 2); 430 for (int row = 0; row <= countY; row++) { 431 mTmpPointF.set(x - mDragCenter.x, y - mDragCenter.y); 432 float dist = mTmpPointF.length(); 433 // Crosshairs further from the drag point are more faint 434 float alpha = Math.min(MAX_ALPHA, 435 DISTANCE_MULTIPLIER * (MAX_VISIBLE_DISTANCE - dist)); 436 if (alpha > 0.0f) { 437 d.setBounds(x, y, x + width, y + height); 438 d.setAlpha((int) (alpha * 255 * mCrosshairsVisibility)); 439 d.draw(canvas); 440 } 441 y += mCellHeight + mHeightGap; 442 } 443 x += mCellWidth + mWidthGap; 444 } 445 } 446 447 final Paint paint = mDragOutlinePaint; 448 for (int i = 0; i < mDragOutlines.length; i++) { 449 final float alpha = mDragOutlineAlphas[i]; 450 if (alpha > 0) { 451 final Rect r = mDragOutlines[i]; 452 final Bitmap b = (Bitmap) mDragOutlineAnims[i].getTag(); 453 paint.setAlpha((int)(alpha + .5f)); 454 canvas.drawBitmap(b, null, r, paint); 455 } 456 } 457 458 // We draw the pressed or focused BubbleTextView's background in CellLayout because it 459 // requires an expanded clip rect (due to the glow's blur radius) 460 if (mPressedOrFocusedIcon != null) { 461 final int padding = mPressedOrFocusedIcon.getPressedOrFocusedBackgroundPadding(); 462 final Bitmap b = mPressedOrFocusedIcon.getPressedOrFocusedBackground(); 463 if (b != null) { 464 canvas.drawBitmap(b, 465 mPressedOrFocusedIcon.getLeft() + getPaddingLeft() - padding, 466 mPressedOrFocusedIcon.getTop() + getPaddingTop() - padding, 467 null); 468 } 469 } 470 471 if (DEBUG_VISUALIZE_OCCUPIED) { 472 int[] pt = new int[2]; 473 ColorDrawable cd = new ColorDrawable(Color.RED); 474 cd.setBounds(0, 0, 80, 80); 475 for (int i = 0; i < mCountX; i++) { 476 for (int j = 0; j < mCountY; j++) { 477 if (mOccupied[i][j]) { 478 cellToPoint(i, j, pt); 479 canvas.save(); 480 canvas.translate(pt[0], pt[1]); 481 cd.draw(canvas); 482 canvas.restore(); 483 } 484 } 485 } 486 } 487 488 // The folder outer / inner ring image(s) 489 for (int i = 0; i < mFolderOuterRings.size(); i++) { 490 FolderRingAnimator fra = mFolderOuterRings.get(i); 491 492 // Draw outer ring 493 Drawable d = FolderRingAnimator.sSharedOuterRingDrawable; 494 int width = (int) fra.getOuterRingSize(); 495 int height = width; 496 cellToPoint(fra.mCellX, fra.mCellY, mTempLocation); 497 498 int centerX = mTempLocation[0] + mCellWidth / 2; 499 int centerY = mTempLocation[1] + FolderRingAnimator.sPreviewSize / 2; 500 501 canvas.save(); 502 canvas.translate(centerX - width / 2, centerY - height / 2); 503 d.setBounds(0, 0, width, height); 504 d.draw(canvas); 505 canvas.restore(); 506 507 // Draw inner ring 508 d = FolderRingAnimator.sSharedInnerRingDrawable; 509 width = (int) fra.getInnerRingSize(); 510 height = width; 511 cellToPoint(fra.mCellX, fra.mCellY, mTempLocation); 512 513 centerX = mTempLocation[0] + mCellWidth / 2; 514 centerY = mTempLocation[1] + FolderRingAnimator.sPreviewSize / 2; 515 canvas.save(); 516 canvas.translate(centerX - width / 2, centerY - width / 2); 517 d.setBounds(0, 0, width, height); 518 d.draw(canvas); 519 canvas.restore(); 520 } 521 522 if (mFolderLeaveBehindCell[0] >= 0 && mFolderLeaveBehindCell[1] >= 0) { 523 Drawable d = FolderIcon.sSharedFolderLeaveBehind; 524 int width = d.getIntrinsicWidth(); 525 int height = d.getIntrinsicHeight(); 526 527 cellToPoint(mFolderLeaveBehindCell[0], mFolderLeaveBehindCell[1], mTempLocation); 528 int centerX = mTempLocation[0] + mCellWidth / 2; 529 int centerY = mTempLocation[1] + FolderRingAnimator.sPreviewSize / 2; 530 531 canvas.save(); 532 canvas.translate(centerX - width / 2, centerY - width / 2); 533 d.setBounds(0, 0, width, height); 534 d.draw(canvas); 535 canvas.restore(); 536 } 537 } 538 539 @Override 540 protected void dispatchDraw(Canvas canvas) { 541 super.dispatchDraw(canvas); 542 if (mForegroundAlpha > 0) { 543 mOverScrollForegroundDrawable.setBounds(mForegroundRect); 544 Paint p = ((NinePatchDrawable) mOverScrollForegroundDrawable).getPaint(); 545 p.setXfermode(new PorterDuffXfermode(PorterDuff.Mode.ADD)); 546 mOverScrollForegroundDrawable.draw(canvas); 547 p.setXfermode(null); 548 } 549 } 550 551 public void showFolderAccept(FolderRingAnimator fra) { 552 mFolderOuterRings.add(fra); 553 } 554 555 public void hideFolderAccept(FolderRingAnimator fra) { 556 if (mFolderOuterRings.contains(fra)) { 557 mFolderOuterRings.remove(fra); 558 } 559 invalidate(); 560 } 561 562 public void setFolderLeaveBehindCell(int x, int y) { 563 mFolderLeaveBehindCell[0] = x; 564 mFolderLeaveBehindCell[1] = y; 565 invalidate(); 566 } 567 568 public void clearFolderLeaveBehind() { 569 mFolderLeaveBehindCell[0] = -1; 570 mFolderLeaveBehindCell[1] = -1; 571 invalidate(); 572 } 573 574 @Override 575 public boolean shouldDelayChildPressedState() { 576 return false; 577 } 578 579 @Override 580 public void cancelLongPress() { 581 super.cancelLongPress(); 582 583 // Cancel long press for all children 584 final int count = getChildCount(); 585 for (int i = 0; i < count; i++) { 586 final View child = getChildAt(i); 587 child.cancelLongPress(); 588 } 589 } 590 591 public void setOnInterceptTouchListener(View.OnTouchListener listener) { 592 mInterceptTouchListener = listener; 593 } 594 595 int getCountX() { 596 return mCountX; 597 } 598 599 int getCountY() { 600 return mCountY; 601 } 602 603 public void setIsHotseat(boolean isHotseat) { 604 mIsHotseat = isHotseat; 605 } 606 607 public float getChildrenScale() { 608 return mIsHotseat ? mHotseatChildScale : mChildScale; 609 } 610 611 public boolean addViewToCellLayout( 612 View child, int index, int childId, LayoutParams params, boolean markCells) { 613 return addViewToCellLayout(child, index, childId, params, markCells, false); 614 } 615 616 private void scaleChild(BubbleTextView bubbleChild, float pivot, float scale) { 617 // If we haven't measured the child yet, do it now 618 // (this happens if we're being dropped from all-apps 619 if (bubbleChild.getLayoutParams() instanceof LayoutParams && 620 (bubbleChild.getMeasuredWidth() | bubbleChild.getMeasuredHeight()) == 0) { 621 getShortcutsAndWidgets().measureChild(bubbleChild); 622 } 623 624 bubbleChild.setScaleX(scale); 625 bubbleChild.setScaleY(scale); 626 } 627 628 private void resetChild(BubbleTextView bubbleChild) { 629 bubbleChild.setScaleX(1f); 630 bubbleChild.setScaleY(1f); 631 632 bubbleChild.setTextColor(getResources().getColor(R.color.workspace_icon_text_color)); 633 } 634 635 public boolean addViewToCellLayout(View child, int index, int childId, LayoutParams params, 636 boolean markCells, boolean allApps) { 637 final LayoutParams lp = params; 638 639 // Hotseat icons - scale down and remove text 640 // Don't scale the all apps button 641 // scale percent set to -1 means do not scale 642 // Only scale BubbleTextViews 643 if (child instanceof BubbleTextView) { 644 BubbleTextView bubbleChild = (BubbleTextView) child; 645 646 // Start the child with 100% scale and visible text 647 resetChild(bubbleChild); 648 649 if (mIsHotseat && !allApps && mHotseatChildScale >= 0) { 650 // Scale/make transparent for a hotseat 651 scaleChild(bubbleChild, 0f, mHotseatChildScale); 652 653 bubbleChild.setTextColor(getResources().getColor(android.R.color.transparent)); 654 } else if (mChildScale >= 0) { 655 // Else possibly still scale it if we need to for smaller icons 656 scaleChild(bubbleChild, 0f, mChildScale); 657 } 658 } 659 660 // Generate an id for each view, this assumes we have at most 256x256 cells 661 // per workspace screen 662 if (lp.cellX >= 0 && lp.cellX <= mCountX - 1 && lp.cellY >= 0 && lp.cellY <= mCountY - 1) { 663 // If the horizontal or vertical span is set to -1, it is taken to 664 // mean that it spans the extent of the CellLayout 665 if (lp.cellHSpan < 0) lp.cellHSpan = mCountX; 666 if (lp.cellVSpan < 0) lp.cellVSpan = mCountY; 667 668 child.setId(childId); 669 670 mShortcutsAndWidgets.addView(child, index, lp); 671 672 if (markCells) markCellsAsOccupiedForView(child); 673 674 return true; 675 } 676 return false; 677 } 678 679 @Override 680 public void removeAllViews() { 681 clearOccupiedCells(); 682 mShortcutsAndWidgets.removeAllViews(); 683 } 684 685 @Override 686 public void removeAllViewsInLayout() { 687 if (mShortcutsAndWidgets.getChildCount() > 0) { 688 clearOccupiedCells(); 689 mShortcutsAndWidgets.removeAllViewsInLayout(); 690 } 691 } 692 693 public void removeViewWithoutMarkingCells(View view) { 694 mShortcutsAndWidgets.removeView(view); 695 } 696 697 @Override 698 public void removeView(View view) { 699 markCellsAsUnoccupiedForView(view); 700 mShortcutsAndWidgets.removeView(view); 701 } 702 703 @Override 704 public void removeViewAt(int index) { 705 markCellsAsUnoccupiedForView(mShortcutsAndWidgets.getChildAt(index)); 706 mShortcutsAndWidgets.removeViewAt(index); 707 } 708 709 @Override 710 public void removeViewInLayout(View view) { 711 markCellsAsUnoccupiedForView(view); 712 mShortcutsAndWidgets.removeViewInLayout(view); 713 } 714 715 @Override 716 public void removeViews(int start, int count) { 717 for (int i = start; i < start + count; i++) { 718 markCellsAsUnoccupiedForView(mShortcutsAndWidgets.getChildAt(i)); 719 } 720 mShortcutsAndWidgets.removeViews(start, count); 721 } 722 723 @Override 724 public void removeViewsInLayout(int start, int count) { 725 for (int i = start; i < start + count; i++) { 726 markCellsAsUnoccupiedForView(mShortcutsAndWidgets.getChildAt(i)); 727 } 728 mShortcutsAndWidgets.removeViewsInLayout(start, count); 729 } 730 731 @Override 732 protected void onAttachedToWindow() { 733 super.onAttachedToWindow(); 734 mCellInfo.screen = ((ViewGroup) getParent()).indexOfChild(this); 735 } 736 737 public void setTagToCellInfoForPoint(int touchX, int touchY) { 738 final CellInfo cellInfo = mCellInfo; 739 Rect frame = mRect; 740 final int x = touchX + mScrollX; 741 final int y = touchY + mScrollY; 742 final int count = mShortcutsAndWidgets.getChildCount(); 743 744 boolean found = false; 745 for (int i = count - 1; i >= 0; i--) { 746 final View child = mShortcutsAndWidgets.getChildAt(i); 747 final LayoutParams lp = (LayoutParams) child.getLayoutParams(); 748 749 if ((child.getVisibility() == VISIBLE || child.getAnimation() != null) && 750 lp.isLockedToGrid) { 751 child.getHitRect(frame); 752 753 float scale = child.getScaleX(); 754 frame = new Rect(child.getLeft(), child.getTop(), child.getRight(), 755 child.getBottom()); 756 // The child hit rect is relative to the CellLayoutChildren parent, so we need to 757 // offset that by this CellLayout's padding to test an (x,y) point that is relative 758 // to this view. 759 frame.offset(mPaddingLeft, mPaddingTop); 760 frame.inset((int) (frame.width() * (1f - scale) / 2), 761 (int) (frame.height() * (1f - scale) / 2)); 762 763 if (frame.contains(x, y)) { 764 cellInfo.cell = child; 765 cellInfo.cellX = lp.cellX; 766 cellInfo.cellY = lp.cellY; 767 cellInfo.spanX = lp.cellHSpan; 768 cellInfo.spanY = lp.cellVSpan; 769 found = true; 770 break; 771 } 772 } 773 } 774 775 mLastDownOnOccupiedCell = found; 776 777 if (!found) { 778 final int cellXY[] = mTmpXY; 779 pointToCellExact(x, y, cellXY); 780 781 cellInfo.cell = null; 782 cellInfo.cellX = cellXY[0]; 783 cellInfo.cellY = cellXY[1]; 784 cellInfo.spanX = 1; 785 cellInfo.spanY = 1; 786 } 787 setTag(cellInfo); 788 } 789 790 @Override 791 public boolean onInterceptTouchEvent(MotionEvent ev) { 792 // First we clear the tag to ensure that on every touch down we start with a fresh slate, 793 // even in the case where we return early. Not clearing here was causing bugs whereby on 794 // long-press we'd end up picking up an item from a previous drag operation. 795 final int action = ev.getAction(); 796 797 if (action == MotionEvent.ACTION_DOWN) { 798 clearTagCellInfo(); 799 } 800 801 if (mInterceptTouchListener != null && mInterceptTouchListener.onTouch(this, ev)) { 802 return true; 803 } 804 805 if (action == MotionEvent.ACTION_DOWN) { 806 setTagToCellInfoForPoint((int) ev.getX(), (int) ev.getY()); 807 } 808 809 return false; 810 } 811 812 private void clearTagCellInfo() { 813 final CellInfo cellInfo = mCellInfo; 814 cellInfo.cell = null; 815 cellInfo.cellX = -1; 816 cellInfo.cellY = -1; 817 cellInfo.spanX = 0; 818 cellInfo.spanY = 0; 819 setTag(cellInfo); 820 } 821 822 public CellInfo getTag() { 823 return (CellInfo) super.getTag(); 824 } 825 826 /** 827 * Given a point, return the cell that strictly encloses that point 828 * @param x X coordinate of the point 829 * @param y Y coordinate of the point 830 * @param result Array of 2 ints to hold the x and y coordinate of the cell 831 */ 832 void pointToCellExact(int x, int y, int[] result) { 833 final int hStartPadding = getPaddingLeft(); 834 final int vStartPadding = getPaddingTop(); 835 836 result[0] = (x - hStartPadding) / (mCellWidth + mWidthGap); 837 result[1] = (y - vStartPadding) / (mCellHeight + mHeightGap); 838 839 final int xAxis = mCountX; 840 final int yAxis = mCountY; 841 842 if (result[0] < 0) result[0] = 0; 843 if (result[0] >= xAxis) result[0] = xAxis - 1; 844 if (result[1] < 0) result[1] = 0; 845 if (result[1] >= yAxis) result[1] = yAxis - 1; 846 } 847 848 /** 849 * Given a point, return the cell that most closely encloses that point 850 * @param x X coordinate of the point 851 * @param y Y coordinate of the point 852 * @param result Array of 2 ints to hold the x and y coordinate of the cell 853 */ 854 void pointToCellRounded(int x, int y, int[] result) { 855 pointToCellExact(x + (mCellWidth / 2), y + (mCellHeight / 2), result); 856 } 857 858 /** 859 * Given a cell coordinate, return the point that represents the upper left corner of that cell 860 * 861 * @param cellX X coordinate of the cell 862 * @param cellY Y coordinate of the cell 863 * 864 * @param result Array of 2 ints to hold the x and y coordinate of the point 865 */ 866 void cellToPoint(int cellX, int cellY, int[] result) { 867 final int hStartPadding = getPaddingLeft(); 868 final int vStartPadding = getPaddingTop(); 869 870 result[0] = hStartPadding + cellX * (mCellWidth + mWidthGap); 871 result[1] = vStartPadding + cellY * (mCellHeight + mHeightGap); 872 } 873 874 /** 875 * Given a cell coordinate, return the point that represents the center of the cell 876 * 877 * @param cellX X coordinate of the cell 878 * @param cellY Y coordinate of the cell 879 * 880 * @param result Array of 2 ints to hold the x and y coordinate of the point 881 */ 882 void cellToCenterPoint(int cellX, int cellY, int[] result) { 883 regionToCenterPoint(cellX, cellY, 1, 1, result); 884 } 885 886 /** 887 * Given a cell coordinate and span return the point that represents the center of the regio 888 * 889 * @param cellX X coordinate of the cell 890 * @param cellY Y coordinate of the cell 891 * 892 * @param result Array of 2 ints to hold the x and y coordinate of the point 893 */ 894 void regionToCenterPoint(int cellX, int cellY, int spanX, int spanY, int[] result) { 895 final int hStartPadding = getPaddingLeft(); 896 final int vStartPadding = getPaddingTop(); 897 result[0] = hStartPadding + cellX * (mCellWidth + mWidthGap) + 898 (spanX * mCellWidth + (spanX - 1) * mWidthGap) / 2; 899 result[1] = vStartPadding + cellY * (mCellHeight + mHeightGap) + 900 (spanY * mCellHeight + (spanY - 1) * mHeightGap) / 2; 901 } 902 903 /** 904 * Given a cell coordinate and span fills out a corresponding pixel rect 905 * 906 * @param cellX X coordinate of the cell 907 * @param cellY Y coordinate of the cell 908 * @param result Rect in which to write the result 909 */ 910 void regionToRect(int cellX, int cellY, int spanX, int spanY, Rect result) { 911 final int hStartPadding = getPaddingLeft(); 912 final int vStartPadding = getPaddingTop(); 913 final int left = hStartPadding + cellX * (mCellWidth + mWidthGap); 914 final int top = vStartPadding + cellY * (mCellHeight + mHeightGap); 915 result.set(left, top, left + (spanX * mCellWidth + (spanX - 1) * mWidthGap), 916 top + (spanY * mCellHeight + (spanY - 1) * mHeightGap)); 917 } 918 919 public float getDistanceFromCell(float x, float y, int[] cell) { 920 cellToCenterPoint(cell[0], cell[1], mTmpPoint); 921 float distance = (float) Math.sqrt( Math.pow(x - mTmpPoint[0], 2) + 922 Math.pow(y - mTmpPoint[1], 2)); 923 return distance; 924 } 925 926 int getCellWidth() { 927 return mCellWidth; 928 } 929 930 int getCellHeight() { 931 return mCellHeight; 932 } 933 934 int getWidthGap() { 935 return mWidthGap; 936 } 937 938 int getHeightGap() { 939 return mHeightGap; 940 } 941 942 Rect getContentRect(Rect r) { 943 if (r == null) { 944 r = new Rect(); 945 } 946 int left = getPaddingLeft(); 947 int top = getPaddingTop(); 948 int right = left + getWidth() - mPaddingLeft - mPaddingRight; 949 int bottom = top + getHeight() - mPaddingTop - mPaddingBottom; 950 r.set(left, top, right, bottom); 951 return r; 952 } 953 954 @Override 955 protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { 956 // TODO: currently ignoring padding 957 958 int widthSpecMode = MeasureSpec.getMode(widthMeasureSpec); 959 int widthSpecSize = MeasureSpec.getSize(widthMeasureSpec); 960 961 int heightSpecMode = MeasureSpec.getMode(heightMeasureSpec); 962 int heightSpecSize = MeasureSpec.getSize(heightMeasureSpec); 963 964 if (widthSpecMode == MeasureSpec.UNSPECIFIED || heightSpecMode == MeasureSpec.UNSPECIFIED) { 965 throw new RuntimeException("CellLayout cannot have UNSPECIFIED dimensions"); 966 } 967 968 int numWidthGaps = mCountX - 1; 969 int numHeightGaps = mCountY - 1; 970 971 if (mOriginalWidthGap < 0 || mOriginalHeightGap < 0) { 972 int hSpace = widthSpecSize - mPaddingLeft - mPaddingRight; 973 int vSpace = heightSpecSize - mPaddingTop - mPaddingBottom; 974 int hFreeSpace = hSpace - (mCountX * mOriginalCellWidth); 975 int vFreeSpace = vSpace - (mCountY * mOriginalCellHeight); 976 mWidthGap = Math.min(mMaxGap, numWidthGaps > 0 ? (hFreeSpace / numWidthGaps) : 0); 977 mHeightGap = Math.min(mMaxGap,numHeightGaps > 0 ? (vFreeSpace / numHeightGaps) : 0); 978 mShortcutsAndWidgets.setCellDimensions(mCellWidth, mCellHeight, mWidthGap, mHeightGap); 979 } else { 980 mWidthGap = mOriginalWidthGap; 981 mHeightGap = mOriginalHeightGap; 982 } 983 984 // Initial values correspond to widthSpecMode == MeasureSpec.EXACTLY 985 int newWidth = widthSpecSize; 986 int newHeight = heightSpecSize; 987 if (widthSpecMode == MeasureSpec.AT_MOST) { 988 newWidth = mPaddingLeft + mPaddingRight + (mCountX * mCellWidth) + 989 ((mCountX - 1) * mWidthGap); 990 newHeight = mPaddingTop + mPaddingBottom + (mCountY * mCellHeight) + 991 ((mCountY - 1) * mHeightGap); 992 setMeasuredDimension(newWidth, newHeight); 993 } 994 995 int count = getChildCount(); 996 for (int i = 0; i < count; i++) { 997 View child = getChildAt(i); 998 int childWidthMeasureSpec = MeasureSpec.makeMeasureSpec(newWidth - mPaddingLeft - 999 mPaddingRight, MeasureSpec.EXACTLY); 1000 int childheightMeasureSpec = MeasureSpec.makeMeasureSpec(newHeight - mPaddingTop - 1001 mPaddingBottom, MeasureSpec.EXACTLY); 1002 child.measure(childWidthMeasureSpec, childheightMeasureSpec); 1003 } 1004 setMeasuredDimension(newWidth, newHeight); 1005 } 1006 1007 @Override 1008 protected void onLayout(boolean changed, int l, int t, int r, int b) { 1009 int count = getChildCount(); 1010 for (int i = 0; i < count; i++) { 1011 View child = getChildAt(i); 1012 child.layout(mPaddingLeft, mPaddingTop, 1013 r - l - mPaddingRight, b - t - mPaddingBottom); 1014 } 1015 } 1016 1017 @Override 1018 protected void onSizeChanged(int w, int h, int oldw, int oldh) { 1019 super.onSizeChanged(w, h, oldw, oldh); 1020 mBackgroundRect.set(0, 0, w, h); 1021 mForegroundRect.set(mForegroundPadding, mForegroundPadding, 1022 w - 2 * mForegroundPadding, h - 2 * mForegroundPadding); 1023 } 1024 1025 @Override 1026 protected void setChildrenDrawingCacheEnabled(boolean enabled) { 1027 mShortcutsAndWidgets.setChildrenDrawingCacheEnabled(enabled); 1028 } 1029 1030 @Override 1031 protected void setChildrenDrawnWithCacheEnabled(boolean enabled) { 1032 mShortcutsAndWidgets.setChildrenDrawnWithCacheEnabled(enabled); 1033 } 1034 1035 public float getBackgroundAlpha() { 1036 return mBackgroundAlpha; 1037 } 1038 1039 public void setBackgroundAlphaMultiplier(float multiplier) { 1040 mBackgroundAlphaMultiplier = multiplier; 1041 } 1042 1043 public float getBackgroundAlphaMultiplier() { 1044 return mBackgroundAlphaMultiplier; 1045 } 1046 1047 public void setBackgroundAlpha(float alpha) { 1048 if (mBackgroundAlpha != alpha) { 1049 mBackgroundAlpha = alpha; 1050 invalidate(); 1051 } 1052 } 1053 1054 public void setShortcutAndWidgetAlpha(float alpha) { 1055 final int childCount = getChildCount(); 1056 for (int i = 0; i < childCount; i++) { 1057 getChildAt(i).setAlpha(alpha); 1058 } 1059 } 1060 1061 public ShortcutAndWidgetContainer getShortcutsAndWidgets() { 1062 if (getChildCount() > 0) { 1063 return (ShortcutAndWidgetContainer) getChildAt(0); 1064 } 1065 return null; 1066 } 1067 1068 public View getChildAt(int x, int y) { 1069 return mShortcutsAndWidgets.getChildAt(x, y); 1070 } 1071 1072 public boolean animateChildToPosition(final View child, int cellX, int cellY, int duration, 1073 int delay, boolean permanent, boolean adjustOccupied) { 1074 ShortcutAndWidgetContainer clc = getShortcutsAndWidgets(); 1075 boolean[][] occupied = mOccupied; 1076 if (!permanent) { 1077 occupied = mTmpOccupied; 1078 } 1079 1080 if (clc.indexOfChild(child) != -1) { 1081 final LayoutParams lp = (LayoutParams) child.getLayoutParams(); 1082 final ItemInfo info = (ItemInfo) child.getTag(); 1083 1084 // We cancel any existing animations 1085 if (mReorderAnimators.containsKey(lp)) { 1086 mReorderAnimators.get(lp).cancel(); 1087 mReorderAnimators.remove(lp); 1088 } 1089 1090 final int oldX = lp.x; 1091 final int oldY = lp.y; 1092 if (adjustOccupied) { 1093 occupied[lp.cellX][lp.cellY] = false; 1094 occupied[cellX][cellY] = true; 1095 } 1096 lp.isLockedToGrid = true; 1097 if (permanent) { 1098 lp.cellX = info.cellX = cellX; 1099 lp.cellY = info.cellY = cellY; 1100 } else { 1101 lp.tmpCellX = cellX; 1102 lp.tmpCellY = cellY; 1103 } 1104 clc.setupLp(lp); 1105 lp.isLockedToGrid = false; 1106 final int newX = lp.x; 1107 final int newY = lp.y; 1108 1109 lp.x = oldX; 1110 lp.y = oldY; 1111 1112 // Exit early if we're not actually moving the view 1113 if (oldX == newX && oldY == newY) { 1114 lp.isLockedToGrid = true; 1115 return true; 1116 } 1117 1118 ValueAnimator va = ValueAnimator.ofFloat(0f, 1f); 1119 va.setDuration(duration); 1120 mReorderAnimators.put(lp, va); 1121 1122 va.addUpdateListener(new AnimatorUpdateListener() { 1123 @Override 1124 public void onAnimationUpdate(ValueAnimator animation) { 1125 float r = ((Float) animation.getAnimatedValue()).floatValue(); 1126 lp.x = (int) ((1 - r) * oldX + r * newX); 1127 lp.y = (int) ((1 - r) * oldY + r * newY); 1128 child.requestLayout(); 1129 } 1130 }); 1131 va.addListener(new AnimatorListenerAdapter() { 1132 boolean cancelled = false; 1133 public void onAnimationEnd(Animator animation) { 1134 // If the animation was cancelled, it means that another animation 1135 // has interrupted this one, and we don't want to lock the item into 1136 // place just yet. 1137 if (!cancelled) { 1138 lp.isLockedToGrid = true; 1139 child.requestLayout(); 1140 } 1141 if (mReorderAnimators.containsKey(lp)) { 1142 mReorderAnimators.remove(lp); 1143 } 1144 } 1145 public void onAnimationCancel(Animator animation) { 1146 cancelled = true; 1147 } 1148 }); 1149 va.setStartDelay(delay); 1150 va.start(); 1151 return true; 1152 } 1153 return false; 1154 } 1155 1156 /** 1157 * Estimate where the top left cell of the dragged item will land if it is dropped. 1158 * 1159 * @param originX The X value of the top left corner of the item 1160 * @param originY The Y value of the top left corner of the item 1161 * @param spanX The number of horizontal cells that the item spans 1162 * @param spanY The number of vertical cells that the item spans 1163 * @param result The estimated drop cell X and Y. 1164 */ 1165 void estimateDropCell(int originX, int originY, int spanX, int spanY, int[] result) { 1166 final int countX = mCountX; 1167 final int countY = mCountY; 1168 1169 // pointToCellRounded takes the top left of a cell but will pad that with 1170 // cellWidth/2 and cellHeight/2 when finding the matching cell 1171 pointToCellRounded(originX, originY, result); 1172 1173 // If the item isn't fully on this screen, snap to the edges 1174 int rightOverhang = result[0] + spanX - countX; 1175 if (rightOverhang > 0) { 1176 result[0] -= rightOverhang; // Snap to right 1177 } 1178 result[0] = Math.max(0, result[0]); // Snap to left 1179 int bottomOverhang = result[1] + spanY - countY; 1180 if (bottomOverhang > 0) { 1181 result[1] -= bottomOverhang; // Snap to bottom 1182 } 1183 result[1] = Math.max(0, result[1]); // Snap to top 1184 } 1185 1186 void visualizeDropLocation(View v, Bitmap dragOutline, int originX, int originY, int cellX, 1187 int cellY, int spanX, int spanY, boolean resize, Point dragOffset, Rect dragRegion) { 1188 final int oldDragCellX = mDragCell[0]; 1189 final int oldDragCellY = mDragCell[1]; 1190 1191 if (v != null && dragOffset == null) { 1192 mDragCenter.set(originX + (v.getWidth() / 2), originY + (v.getHeight() / 2)); 1193 } else { 1194 mDragCenter.set(originX, originY); 1195 } 1196 1197 if (dragOutline == null && v == null) { 1198 if (mCrosshairsDrawable != null) { 1199 invalidate(); 1200 } 1201 return; 1202 } 1203 1204 if (cellX != oldDragCellX || cellY != oldDragCellY) { 1205 mDragCell[0] = cellX; 1206 mDragCell[1] = cellY; 1207 // Find the top left corner of the rect the object will occupy 1208 final int[] topLeft = mTmpPoint; 1209 cellToPoint(cellX, cellY, topLeft); 1210 1211 int left = topLeft[0]; 1212 int top = topLeft[1]; 1213 1214 if (v != null && dragOffset == null) { 1215 // When drawing the drag outline, it did not account for margin offsets 1216 // added by the view's parent. 1217 MarginLayoutParams lp = (MarginLayoutParams) v.getLayoutParams(); 1218 left += lp.leftMargin; 1219 top += lp.topMargin; 1220 1221 // Offsets due to the size difference between the View and the dragOutline. 1222 // There is a size difference to account for the outer blur, which may lie 1223 // outside the bounds of the view. 1224 top += (v.getHeight() - dragOutline.getHeight()) / 2; 1225 // We center about the x axis 1226 left += ((mCellWidth * spanX) + ((spanX - 1) * mWidthGap) 1227 - dragOutline.getWidth()) / 2; 1228 } else { 1229 if (dragOffset != null && dragRegion != null) { 1230 // Center the drag region *horizontally* in the cell and apply a drag 1231 // outline offset 1232 left += dragOffset.x + ((mCellWidth * spanX) + ((spanX - 1) * mWidthGap) 1233 - dragRegion.width()) / 2; 1234 top += dragOffset.y; 1235 } else { 1236 // Center the drag outline in the cell 1237 left += ((mCellWidth * spanX) + ((spanX - 1) * mWidthGap) 1238 - dragOutline.getWidth()) / 2; 1239 top += ((mCellHeight * spanY) + ((spanY - 1) * mHeightGap) 1240 - dragOutline.getHeight()) / 2; 1241 } 1242 } 1243 final int oldIndex = mDragOutlineCurrent; 1244 mDragOutlineAnims[oldIndex].animateOut(); 1245 mDragOutlineCurrent = (oldIndex + 1) % mDragOutlines.length; 1246 Rect r = mDragOutlines[mDragOutlineCurrent]; 1247 r.set(left, top, left + dragOutline.getWidth(), top + dragOutline.getHeight()); 1248 if (resize) { 1249 cellToRect(cellX, cellY, spanX, spanY, r); 1250 } 1251 1252 mDragOutlineAnims[mDragOutlineCurrent].setTag(dragOutline); 1253 mDragOutlineAnims[mDragOutlineCurrent].animateIn(); 1254 } 1255 1256 // If we are drawing crosshairs, the entire CellLayout needs to be invalidated 1257 if (mCrosshairsDrawable != null) { 1258 invalidate(); 1259 } 1260 } 1261 1262 public void clearDragOutlines() { 1263 final int oldIndex = mDragOutlineCurrent; 1264 mDragOutlineAnims[oldIndex].animateOut(); 1265 mDragCell[0] = mDragCell[1] = -1; 1266 } 1267 1268 /** 1269 * Find a vacant area that will fit the given bounds nearest the requested 1270 * cell location. Uses Euclidean distance to score multiple vacant areas. 1271 * 1272 * @param pixelX The X location at which you want to search for a vacant area. 1273 * @param pixelY The Y location at which you want to search for a vacant area. 1274 * @param spanX Horizontal span of the object. 1275 * @param spanY Vertical span of the object. 1276 * @param result Array in which to place the result, or null (in which case a new array will 1277 * be allocated) 1278 * @return The X, Y cell of a vacant area that can contain this object, 1279 * nearest the requested location. 1280 */ 1281 int[] findNearestVacantArea(int pixelX, int pixelY, int spanX, int spanY, 1282 int[] result) { 1283 return findNearestVacantArea(pixelX, pixelY, spanX, spanY, null, result); 1284 } 1285 1286 /** 1287 * Find a vacant area that will fit the given bounds nearest the requested 1288 * cell location. Uses Euclidean distance to score multiple vacant areas. 1289 * 1290 * @param pixelX The X location at which you want to search for a vacant area. 1291 * @param pixelY The Y location at which you want to search for a vacant area. 1292 * @param minSpanX The minimum horizontal span required 1293 * @param minSpanY The minimum vertical span required 1294 * @param spanX Horizontal span of the object. 1295 * @param spanY Vertical span of the object. 1296 * @param result Array in which to place the result, or null (in which case a new array will 1297 * be allocated) 1298 * @return The X, Y cell of a vacant area that can contain this object, 1299 * nearest the requested location. 1300 */ 1301 int[] findNearestVacantArea(int pixelX, int pixelY, int minSpanX, int minSpanY, int spanX, 1302 int spanY, int[] result, int[] resultSpan) { 1303 return findNearestVacantArea(pixelX, pixelY, minSpanX, minSpanY, spanX, spanY, null, 1304 result, resultSpan); 1305 } 1306 1307 /** 1308 * Find a vacant area that will fit the given bounds nearest the requested 1309 * cell location. Uses Euclidean distance to score multiple vacant areas. 1310 * 1311 * @param pixelX The X location at which you want to search for a vacant area. 1312 * @param pixelY The Y location at which you want to search for a vacant area. 1313 * @param spanX Horizontal span of the object. 1314 * @param spanY Vertical span of the object. 1315 * @param ignoreOccupied If true, the result can be an occupied cell 1316 * @param result Array in which to place the result, or null (in which case a new array will 1317 * be allocated) 1318 * @return The X, Y cell of a vacant area that can contain this object, 1319 * nearest the requested location. 1320 */ 1321 int[] findNearestArea(int pixelX, int pixelY, int spanX, int spanY, View ignoreView, 1322 boolean ignoreOccupied, int[] result) { 1323 return findNearestArea(pixelX, pixelY, spanX, spanY, 1324 spanX, spanY, ignoreView, ignoreOccupied, result, null, mOccupied); 1325 } 1326 1327 private final Stack<Rect> mTempRectStack = new Stack<Rect>(); 1328 private void lazyInitTempRectStack() { 1329 if (mTempRectStack.isEmpty()) { 1330 for (int i = 0; i < mCountX * mCountY; i++) { 1331 mTempRectStack.push(new Rect()); 1332 } 1333 } 1334 } 1335 1336 private void recycleTempRects(Stack<Rect> used) { 1337 while (!used.isEmpty()) { 1338 mTempRectStack.push(used.pop()); 1339 } 1340 } 1341 1342 /** 1343 * Find a vacant area that will fit the given bounds nearest the requested 1344 * cell location. Uses Euclidean distance to score multiple vacant areas. 1345 * 1346 * @param pixelX The X location at which you want to search for a vacant area. 1347 * @param pixelY The Y location at which you want to search for a vacant area. 1348 * @param minSpanX The minimum horizontal span required 1349 * @param minSpanY The minimum vertical span required 1350 * @param spanX Horizontal span of the object. 1351 * @param spanY Vertical span of the object. 1352 * @param ignoreOccupied If true, the result can be an occupied cell 1353 * @param result Array in which to place the result, or null (in which case a new array will 1354 * be allocated) 1355 * @return The X, Y cell of a vacant area that can contain this object, 1356 * nearest the requested location. 1357 */ 1358 int[] findNearestArea(int pixelX, int pixelY, int minSpanX, int minSpanY, int spanX, int spanY, 1359 View ignoreView, boolean ignoreOccupied, int[] result, int[] resultSpan, 1360 boolean[][] occupied) { 1361 lazyInitTempRectStack(); 1362 // mark space take by ignoreView as available (method checks if ignoreView is null) 1363 markCellsAsUnoccupiedForView(ignoreView, occupied); 1364 1365 // For items with a spanX / spanY > 1, the passed in point (pixelX, pixelY) corresponds 1366 // to the center of the item, but we are searching based on the top-left cell, so 1367 // we translate the point over to correspond to the top-left. 1368 pixelX -= (mCellWidth + mWidthGap) * (spanX - 1) / 2f; 1369 pixelY -= (mCellHeight + mHeightGap) * (spanY - 1) / 2f; 1370 1371 // Keep track of best-scoring drop area 1372 final int[] bestXY = result != null ? result : new int[2]; 1373 double bestDistance = Double.MAX_VALUE; 1374 final Rect bestRect = new Rect(-1, -1, -1, -1); 1375 final Stack<Rect> validRegions = new Stack<Rect>(); 1376 1377 final int countX = mCountX; 1378 final int countY = mCountY; 1379 1380 if (minSpanX <= 0 || minSpanY <= 0 || spanX <= 0 || spanY <= 0 || 1381 spanX < minSpanX || spanY < minSpanY) { 1382 return bestXY; 1383 } 1384 1385 for (int y = 0; y < countY - (minSpanY - 1); y++) { 1386 inner: 1387 for (int x = 0; x < countX - (minSpanX - 1); x++) { 1388 int ySize = -1; 1389 int xSize = -1; 1390 if (ignoreOccupied) { 1391 // First, let's see if this thing fits anywhere 1392 for (int i = 0; i < minSpanX; i++) { 1393 for (int j = 0; j < minSpanY; j++) { 1394 if (occupied[x + i][y + j]) { 1395 continue inner; 1396 } 1397 } 1398 } 1399 xSize = minSpanX; 1400 ySize = minSpanY; 1401 1402 // We know that the item will fit at _some_ acceptable size, now let's see 1403 // how big we can make it. We'll alternate between incrementing x and y spans 1404 // until we hit a limit. 1405 boolean incX = true; 1406 boolean hitMaxX = xSize >= spanX; 1407 boolean hitMaxY = ySize >= spanY; 1408 while (!(hitMaxX && hitMaxY)) { 1409 if (incX && !hitMaxX) { 1410 for (int j = 0; j < ySize; j++) { 1411 if (x + xSize > countX -1 || occupied[x + xSize][y + j]) { 1412 // We can't move out horizontally 1413 hitMaxX = true; 1414 } 1415 } 1416 if (!hitMaxX) { 1417 xSize++; 1418 } 1419 } else if (!hitMaxY) { 1420 for (int i = 0; i < xSize; i++) { 1421 if (y + ySize > countY - 1 || occupied[x + i][y + ySize]) { 1422 // We can't move out vertically 1423 hitMaxY = true; 1424 } 1425 } 1426 if (!hitMaxY) { 1427 ySize++; 1428 } 1429 } 1430 hitMaxX |= xSize >= spanX; 1431 hitMaxY |= ySize >= spanY; 1432 incX = !incX; 1433 } 1434 incX = true; 1435 hitMaxX = xSize >= spanX; 1436 hitMaxY = ySize >= spanY; 1437 } 1438 final int[] cellXY = mTmpXY; 1439 cellToCenterPoint(x, y, cellXY); 1440 1441 // We verify that the current rect is not a sub-rect of any of our previous 1442 // candidates. In this case, the current rect is disqualified in favour of the 1443 // containing rect. 1444 Rect currentRect = mTempRectStack.pop(); 1445 currentRect.set(x, y, x + xSize, y + ySize); 1446 boolean contained = false; 1447 for (Rect r : validRegions) { 1448 if (r.contains(currentRect)) { 1449 contained = true; 1450 break; 1451 } 1452 } 1453 validRegions.push(currentRect); 1454 double distance = Math.sqrt(Math.pow(cellXY[0] - pixelX, 2) 1455 + Math.pow(cellXY[1] - pixelY, 2)); 1456 1457 if ((distance <= bestDistance && !contained) || 1458 currentRect.contains(bestRect)) { 1459 bestDistance = distance; 1460 bestXY[0] = x; 1461 bestXY[1] = y; 1462 if (resultSpan != null) { 1463 resultSpan[0] = xSize; 1464 resultSpan[1] = ySize; 1465 } 1466 bestRect.set(currentRect); 1467 } 1468 } 1469 } 1470 // re-mark space taken by ignoreView as occupied 1471 markCellsAsOccupiedForView(ignoreView, occupied); 1472 1473 // Return -1, -1 if no suitable location found 1474 if (bestDistance == Double.MAX_VALUE) { 1475 bestXY[0] = -1; 1476 bestXY[1] = -1; 1477 } 1478 recycleTempRects(validRegions); 1479 return bestXY; 1480 } 1481 1482 /** 1483 * Find a vacant area that will fit the given bounds nearest the requested 1484 * cell location, and will also weigh in a suggested direction vector of the 1485 * desired location. This method computers distance based on unit grid distances, 1486 * not pixel distances. 1487 * 1488 * @param cellX The X cell nearest to which you want to search for a vacant area. 1489 * @param cellY The Y cell nearest which you want to search for a vacant area. 1490 * @param spanX Horizontal span of the object. 1491 * @param spanY Vertical span of the object. 1492 * @param direction The favored direction in which the views should move from x, y 1493 * @param exactDirectionOnly If this parameter is true, then only solutions where the direction 1494 * matches exactly. Otherwise we find the best matching direction. 1495 * @param occoupied The array which represents which cells in the CellLayout are occupied 1496 * @param blockOccupied The array which represents which cells in the specified block (cellX, 1497 * cellY, spanX, spanY) are occupied. This is used when try to move a group of views. 1498 * @param result Array in which to place the result, or null (in which case a new array will 1499 * be allocated) 1500 * @return The X, Y cell of a vacant area that can contain this object, 1501 * nearest the requested location. 1502 */ 1503 private int[] findNearestArea(int cellX, int cellY, int spanX, int spanY, int[] direction, 1504 boolean[][] occupied, boolean blockOccupied[][], int[] result) { 1505 // Keep track of best-scoring drop area 1506 final int[] bestXY = result != null ? result : new int[2]; 1507 float bestDistance = Float.MAX_VALUE; 1508 int bestDirectionScore = Integer.MIN_VALUE; 1509 1510 final int countX = mCountX; 1511 final int countY = mCountY; 1512 1513 for (int y = 0; y < countY - (spanY - 1); y++) { 1514 inner: 1515 for (int x = 0; x < countX - (spanX - 1); x++) { 1516 // First, let's see if this thing fits anywhere 1517 for (int i = 0; i < spanX; i++) { 1518 for (int j = 0; j < spanY; j++) { 1519 if (occupied[x + i][y + j] && (blockOccupied == null || blockOccupied[i][j])) { 1520 continue inner; 1521 } 1522 } 1523 } 1524 1525 float distance = (float) 1526 Math.sqrt((x - cellX) * (x - cellX) + (y - cellY) * (y - cellY)); 1527 int[] curDirection = mTmpPoint; 1528 computeDirectionVector(x - cellX, y - cellY, curDirection); 1529 // The direction score is just the dot product of the two candidate direction 1530 // and that passed in. 1531 int curDirectionScore = direction[0] * curDirection[0] + 1532 direction[1] * curDirection[1]; 1533 boolean exactDirectionOnly = false; 1534 boolean directionMatches = direction[0] == curDirection[0] && 1535 direction[0] == curDirection[0]; 1536 if ((directionMatches || !exactDirectionOnly) && 1537 Float.compare(distance, bestDistance) < 0 || (Float.compare(distance, 1538 bestDistance) == 0 && curDirectionScore > bestDirectionScore)) { 1539 bestDistance = distance; 1540 bestDirectionScore = curDirectionScore; 1541 bestXY[0] = x; 1542 bestXY[1] = y; 1543 } 1544 } 1545 } 1546 1547 // Return -1, -1 if no suitable location found 1548 if (bestDistance == Float.MAX_VALUE) { 1549 bestXY[0] = -1; 1550 bestXY[1] = -1; 1551 } 1552 return bestXY; 1553 } 1554 1555 private int[] findNearestAreaInDirection(int cellX, int cellY, int spanX, int spanY, 1556 int[] direction,boolean[][] occupied, 1557 boolean blockOccupied[][], int[] result) { 1558 // Keep track of best-scoring drop area 1559 final int[] bestXY = result != null ? result : new int[2]; 1560 bestXY[0] = -1; 1561 bestXY[1] = -1; 1562 float bestDistance = Float.MAX_VALUE; 1563 1564 // We use this to march in a single direction 1565 if (direction[0] != 0 && direction[1] != 0) { 1566 return bestXY; 1567 } 1568 1569 // This will only incrememnet one of x or y based on the assertion above 1570 int x = cellX + direction[0]; 1571 int y = cellY + direction[1]; 1572 while (x >= 0 && x + spanX <= mCountX && y >= 0 && y + spanY <= mCountY) { 1573 1574 boolean fail = false; 1575 for (int i = 0; i < spanX; i++) { 1576 for (int j = 0; j < spanY; j++) { 1577 if (occupied[x + i][y + j] && (blockOccupied == null || blockOccupied[i][j])) { 1578 fail = true; 1579 } 1580 } 1581 } 1582 if (!fail) { 1583 float distance = (float) 1584 Math.sqrt((x - cellX) * (x - cellX) + (y - cellY) * (y - cellY)); 1585 if (Float.compare(distance, bestDistance) < 0) { 1586 bestDistance = distance; 1587 bestXY[0] = x; 1588 bestXY[1] = y; 1589 } 1590 } 1591 x += direction[0]; 1592 y += direction[1]; 1593 } 1594 return bestXY; 1595 } 1596 1597 private boolean addViewToTempLocation(View v, Rect rectOccupiedByPotentialDrop, 1598 int[] direction, ItemConfiguration currentState) { 1599 CellAndSpan c = currentState.map.get(v); 1600 boolean success = false; 1601 markCellsForView(c.x, c.y, c.spanX, c.spanY, mTmpOccupied, false); 1602 markCellsForRect(rectOccupiedByPotentialDrop, mTmpOccupied, true); 1603 1604 findNearestArea(c.x, c.y, c.spanX, c.spanY, direction, mTmpOccupied, null, mTempLocation); 1605 1606 if (mTempLocation[0] >= 0 && mTempLocation[1] >= 0) { 1607 c.x = mTempLocation[0]; 1608 c.y = mTempLocation[1]; 1609 success = true; 1610 1611 } 1612 markCellsForView(c.x, c.y, c.spanX, c.spanY, mTmpOccupied, true); 1613 return success; 1614 } 1615 1616 // This method looks in the specified direction to see if there is an additional view 1617 // immediately adjecent in that direction 1618 private boolean addViewInDirection(ArrayList<View> views, Rect boundingRect, int[] direction, 1619 boolean[][] occupied, View dragView, ItemConfiguration currentState) { 1620 boolean found = false; 1621 1622 int childCount = mShortcutsAndWidgets.getChildCount(); 1623 Rect r0 = new Rect(boundingRect); 1624 Rect r1 = new Rect(); 1625 1626 int deltaX = 0; 1627 int deltaY = 0; 1628 if (direction[1] < 0) { 1629 r0.set(r0.left, r0.top - 1, r0.right, r0.bottom); 1630 deltaY = -1; 1631 } else if (direction[1] > 0) { 1632 r0.set(r0.left, r0.top, r0.right, r0.bottom + 1); 1633 deltaY = 1; 1634 } else if (direction[0] < 0) { 1635 r0.set(r0.left - 1, r0.top, r0.right, r0.bottom); 1636 deltaX = -1; 1637 } else if (direction[0] > 0) { 1638 r0.set(r0.left, r0.top, r0.right + 1, r0.bottom); 1639 deltaX = 1; 1640 } 1641 1642 for (int i = 0; i < childCount; i++) { 1643 View child = mShortcutsAndWidgets.getChildAt(i); 1644 if (views.contains(child) || child == dragView) continue; 1645 CellAndSpan c = currentState.map.get(child); 1646 1647 LayoutParams lp = (LayoutParams) child.getLayoutParams(); 1648 r1.set(c.x, c.y, c.x + c.spanX, c.y + c.spanY); 1649 if (Rect.intersects(r0, r1)) { 1650 if (!lp.canReorder) { 1651 return false; 1652 } 1653 boolean pushed = false; 1654 for (int x = c.x; x < c.x + c.spanX; x++) { 1655 for (int y = c.y; y < c.y + c.spanY; y++) { 1656 boolean inBounds = x - deltaX >= 0 && x -deltaX < mCountX 1657 && y - deltaY >= 0 && y - deltaY < mCountY; 1658 if (inBounds && occupied[x - deltaX][y - deltaY]) { 1659 pushed = true; 1660 } 1661 } 1662 } 1663 if (pushed) { 1664 views.add(child); 1665 boundingRect.union(c.x, c.y, c.x + c.spanX, c.y + c.spanY); 1666 found = true; 1667 } 1668 } 1669 } 1670 return found; 1671 } 1672 1673 private boolean addViewsToTempLocation(ArrayList<View> views, Rect rectOccupiedByPotentialDrop, 1674 int[] direction, boolean push, View dragView, ItemConfiguration currentState) { 1675 if (views.size() == 0) return true; 1676 1677 boolean success = false; 1678 Rect boundingRect = null; 1679 // We construct a rect which represents the entire group of views passed in 1680 for (View v: views) { 1681 CellAndSpan c = currentState.map.get(v); 1682 if (boundingRect == null) { 1683 boundingRect = new Rect(c.x, c.y, c.x + c.spanX, c.y + c.spanY); 1684 } else { 1685 boundingRect.union(c.x, c.y, c.x + c.spanX, c.y + c.spanY); 1686 } 1687 } 1688 1689 @SuppressWarnings("unchecked") 1690 ArrayList<View> dup = (ArrayList<View>) views.clone(); 1691 // We try and expand the group of views in the direction vector passed, based on 1692 // whether they are physically adjacent, ie. based on "push mechanics". 1693 while (push && addViewInDirection(dup, boundingRect, direction, mTmpOccupied, dragView, 1694 currentState)) { 1695 } 1696 1697 // Mark the occupied state as false for the group of views we want to move. 1698 for (View v: dup) { 1699 CellAndSpan c = currentState.map.get(v); 1700 markCellsForView(c.x, c.y, c.spanX, c.spanY, mTmpOccupied, false); 1701 } 1702 1703 boolean[][] blockOccupied = new boolean[boundingRect.width()][boundingRect.height()]; 1704 int top = boundingRect.top; 1705 int left = boundingRect.left; 1706 // We mark more precisely which parts of the bounding rect are truly occupied, allowing 1707 // for tetris-style interlocking. 1708 for (View v: dup) { 1709 CellAndSpan c = currentState.map.get(v); 1710 markCellsForView(c.x - left, c.y - top, c.spanX, c.spanY, blockOccupied, true); 1711 } 1712 1713 markCellsForRect(rectOccupiedByPotentialDrop, mTmpOccupied, true); 1714 1715 if (push) { 1716 findNearestAreaInDirection(boundingRect.left, boundingRect.top, boundingRect.width(), 1717 boundingRect.height(), direction, mTmpOccupied, blockOccupied, mTempLocation); 1718 } else { 1719 findNearestArea(boundingRect.left, boundingRect.top, boundingRect.width(), 1720 boundingRect.height(), direction, mTmpOccupied, blockOccupied, mTempLocation); 1721 } 1722 1723 // If we successfuly found a location by pushing the block of views, we commit it 1724 if (mTempLocation[0] >= 0 && mTempLocation[1] >= 0) { 1725 int deltaX = mTempLocation[0] - boundingRect.left; 1726 int deltaY = mTempLocation[1] - boundingRect.top; 1727 for (View v: dup) { 1728 CellAndSpan c = currentState.map.get(v); 1729 c.x += deltaX; 1730 c.y += deltaY; 1731 } 1732 success = true; 1733 } 1734 1735 // In either case, we set the occupied array as marked for the location of the views 1736 for (View v: dup) { 1737 CellAndSpan c = currentState.map.get(v); 1738 markCellsForView(c.x, c.y, c.spanX, c.spanY, mTmpOccupied, true); 1739 } 1740 return success; 1741 } 1742 1743 private void markCellsForRect(Rect r, boolean[][] occupied, boolean value) { 1744 markCellsForView(r.left, r.top, r.width(), r.height(), occupied, value); 1745 } 1746 1747 private boolean rearrangementExists(int cellX, int cellY, int spanX, int spanY, int[] direction, 1748 View ignoreView, ItemConfiguration solution) { 1749 1750 mIntersectingViews.clear(); 1751 mOccupiedRect.set(cellX, cellY, cellX + spanX, cellY + spanY); 1752 1753 // Mark the desired location of the view currently being dragged. 1754 if (ignoreView != null) { 1755 CellAndSpan c = solution.map.get(ignoreView); 1756 if (c != null) { 1757 c.x = cellX; 1758 c.y = cellY; 1759 } 1760 } 1761 Rect r0 = new Rect(cellX, cellY, cellX + spanX, cellY + spanY); 1762 Rect r1 = new Rect(); 1763 for (View child: solution.map.keySet()) { 1764 if (child == ignoreView) continue; 1765 CellAndSpan c = solution.map.get(child); 1766 LayoutParams lp = (LayoutParams) child.getLayoutParams(); 1767 r1.set(c.x, c.y, c.x + c.spanX, c.y + c.spanY); 1768 if (Rect.intersects(r0, r1)) { 1769 if (!lp.canReorder) { 1770 return false; 1771 } 1772 mIntersectingViews.add(child); 1773 } 1774 } 1775 1776 // We try to move the intersecting views as a block using the push mechanic 1777 if (addViewsToTempLocation(mIntersectingViews, mOccupiedRect, direction, true, ignoreView, 1778 solution)) { 1779 return true; 1780 } 1781 // Try the opposite direction 1782 direction[0] *= -1; 1783 direction[1] *= -1; 1784 if (addViewsToTempLocation(mIntersectingViews, mOccupiedRect, direction, true, ignoreView, 1785 solution)) { 1786 return true; 1787 } 1788 // Switch the direction back 1789 direction[0] *= -1; 1790 direction[1] *= -1; 1791 1792 // Next we try moving the views as a block , but without requiring the push mechanic 1793 if (addViewsToTempLocation(mIntersectingViews, mOccupiedRect, direction, false, ignoreView, 1794 solution)) { 1795 return true; 1796 } 1797 1798 // Ok, they couldn't move as a block, let's move them individually 1799 for (View v : mIntersectingViews) { 1800 if (!addViewToTempLocation(v, mOccupiedRect, direction, solution)) { 1801 return false; 1802 } 1803 } 1804 return true; 1805 } 1806 1807 /* 1808 * Returns a pair (x, y), where x,y are in {-1, 0, 1} corresponding to vector between 1809 * the provided point and the provided cell 1810 */ 1811 private void computeDirectionVector(float deltaX, float deltaY, int[] result) { 1812 double angle = Math.atan(((float) deltaY) / deltaX); 1813 1814 result[0] = 0; 1815 result[1] = 0; 1816 if (Math.abs(Math.cos(angle)) > 0.5f) { 1817 result[0] = (int) Math.signum(deltaX); 1818 } 1819 if (Math.abs(Math.sin(angle)) > 0.5f) { 1820 result[1] = (int) Math.signum(deltaY); 1821 } 1822 } 1823 1824 private void copyOccupiedArray(boolean[][] occupied) { 1825 for (int i = 0; i < mCountX; i++) { 1826 for (int j = 0; j < mCountY; j++) { 1827 occupied[i][j] = mOccupied[i][j]; 1828 } 1829 } 1830 } 1831 1832 ItemConfiguration simpleSwap(int pixelX, int pixelY, int minSpanX, int minSpanY, int spanX, 1833 int spanY, int[] direction, View dragView, boolean decX, ItemConfiguration solution) { 1834 // Copy the current state into the solution. This solution will be manipulated as necessary. 1835 copyCurrentStateToSolution(solution, false); 1836 // Copy the current occupied array into the temporary occupied array. This array will be 1837 // manipulated as necessary to find a solution. 1838 copyOccupiedArray(mTmpOccupied); 1839 1840 // We find the nearest cell into which we would place the dragged item, assuming there's 1841 // nothing in its way. 1842 int result[] = new int[2]; 1843 result = findNearestArea(pixelX, pixelY, spanX, spanY, result); 1844 1845 boolean success = false; 1846 // First we try the exact nearest position of the item being dragged, 1847 // we will then want to try to move this around to other neighbouring positions 1848 success = rearrangementExists(result[0], result[1], spanX, spanY, direction, dragView, 1849 solution); 1850 1851 if (!success) { 1852 // We try shrinking the widget down to size in an alternating pattern, shrink 1 in 1853 // x, then 1 in y etc. 1854 if (spanX > minSpanX && (minSpanY == spanY || decX)) { 1855 return simpleSwap(pixelX, pixelY, minSpanX, minSpanY, spanX - 1, spanY, direction, 1856 dragView, false, solution); 1857 } else if (spanY > minSpanY) { 1858 return simpleSwap(pixelX, pixelY, minSpanX, minSpanY, spanX, spanY - 1, direction, 1859 dragView, true, solution); 1860 } 1861 solution.isSolution = false; 1862 } else { 1863 solution.isSolution = true; 1864 solution.dragViewX = result[0]; 1865 solution.dragViewY = result[1]; 1866 solution.dragViewSpanX = spanX; 1867 solution.dragViewSpanY = spanY; 1868 } 1869 return solution; 1870 } 1871 1872 private void copyCurrentStateToSolution(ItemConfiguration solution, boolean temp) { 1873 int childCount = mShortcutsAndWidgets.getChildCount(); 1874 for (int i = 0; i < childCount; i++) { 1875 View child = mShortcutsAndWidgets.getChildAt(i); 1876 LayoutParams lp = (LayoutParams) child.getLayoutParams(); 1877 CellAndSpan c; 1878 if (temp) { 1879 c = new CellAndSpan(lp.tmpCellX, lp.tmpCellY, lp.cellHSpan, lp.cellVSpan); 1880 } else { 1881 c = new CellAndSpan(lp.cellX, lp.cellY, lp.cellHSpan, lp.cellVSpan); 1882 } 1883 solution.map.put(child, c); 1884 } 1885 } 1886 1887 private void copySolutionToTempState(ItemConfiguration solution, View dragView) { 1888 for (int i = 0; i < mCountX; i++) { 1889 for (int j = 0; j < mCountY; j++) { 1890 mTmpOccupied[i][j] = false; 1891 } 1892 } 1893 1894 int childCount = mShortcutsAndWidgets.getChildCount(); 1895 for (int i = 0; i < childCount; i++) { 1896 View child = mShortcutsAndWidgets.getChildAt(i); 1897 if (child == dragView) continue; 1898 LayoutParams lp = (LayoutParams) child.getLayoutParams(); 1899 CellAndSpan c = solution.map.get(child); 1900 if (c != null) { 1901 lp.tmpCellX = c.x; 1902 lp.tmpCellY = c.y; 1903 lp.cellHSpan = c.spanX; 1904 lp.cellVSpan = c.spanY; 1905 markCellsForView(c.x, c.y, c.spanX, c.spanY, mTmpOccupied, true); 1906 } 1907 } 1908 markCellsForView(solution.dragViewX, solution.dragViewY, solution.dragViewSpanX, 1909 solution.dragViewSpanY, mTmpOccupied, true); 1910 } 1911 1912 private void animateItemsToSolution(ItemConfiguration solution, View dragView, boolean 1913 commitDragView) { 1914 1915 boolean[][] occupied = DESTRUCTIVE_REORDER ? mOccupied : mTmpOccupied; 1916 for (int i = 0; i < mCountX; i++) { 1917 for (int j = 0; j < mCountY; j++) { 1918 occupied[i][j] = false; 1919 } 1920 } 1921 1922 int childCount = mShortcutsAndWidgets.getChildCount(); 1923 for (int i = 0; i < childCount; i++) { 1924 View child = mShortcutsAndWidgets.getChildAt(i); 1925 if (child == dragView) continue; 1926 CellAndSpan c = solution.map.get(child); 1927 if (c != null) { 1928 animateChildToPosition(child, c.x, c.y, REORDER_ANIMATION_DURATION, 0, 1929 DESTRUCTIVE_REORDER, false); 1930 markCellsForView(c.x, c.y, c.spanX, c.spanY, occupied, true); 1931 } 1932 } 1933 if (commitDragView) { 1934 markCellsForView(solution.dragViewX, solution.dragViewY, solution.dragViewSpanX, 1935 solution.dragViewSpanY, occupied, true); 1936 } 1937 } 1938 1939 // This method starts or changes the reorder hint animations 1940 private void beginOrAdjustHintAnimations(ItemConfiguration solution, View dragView, int delay) { 1941 int childCount = mShortcutsAndWidgets.getChildCount(); 1942 int timeForPriorAnimationToComplete = getMaxCompletionTime(); 1943 for (int i = 0; i < childCount; i++) { 1944 View child = mShortcutsAndWidgets.getChildAt(i); 1945 if (child == dragView) continue; 1946 CellAndSpan c = solution.map.get(child); 1947 LayoutParams lp = (LayoutParams) child.getLayoutParams(); 1948 if (c != null) { 1949 ReorderHintAnimation rha = new ReorderHintAnimation(child, lp.cellX, lp.cellY, 1950 c.x, c.y, c.spanX, c.spanY); 1951 rha.animate(timeForPriorAnimationToComplete); 1952 } 1953 } 1954 } 1955 1956 // Class which represents the reorder hint animations. These animations show that an item is 1957 // in a temporary state, and hint at where the item will return to. 1958 class ReorderHintAnimation { 1959 View child; 1960 float deltaX; 1961 float deltaY; 1962 private static final int DURATION = 140; 1963 private int repeatCount; 1964 private boolean cancelOnCycleComplete = false; 1965 ValueAnimator va; 1966 1967 public ReorderHintAnimation(View child, int cellX0, int cellY0, int cellX1, int cellY1, 1968 int spanX, int spanY) { 1969 regionToCenterPoint(cellX0, cellY0, spanX, spanY, mTmpPoint); 1970 final int x0 = mTmpPoint[0]; 1971 final int y0 = mTmpPoint[1]; 1972 regionToCenterPoint(cellX1, cellY1, spanX, spanY, mTmpPoint); 1973 final int x1 = mTmpPoint[0]; 1974 final int y1 = mTmpPoint[1]; 1975 final int dX = x1 - x0; 1976 final int dY = y1 - y0; 1977 deltaX = 0; 1978 deltaY = 0; 1979 if (dX == dY && dX == 0) { 1980 } else { 1981 if (dY == 0) { 1982 deltaX = mReorderHintAnimationMagnitude; 1983 } else if (dX == 0) { 1984 deltaY = mReorderHintAnimationMagnitude; 1985 } else { 1986 double angle = Math.atan( (float) (dY) / dX); 1987 deltaX = (int) (Math.cos(angle) * mReorderHintAnimationMagnitude); 1988 deltaY = (int) (Math.sin(angle) * mReorderHintAnimationMagnitude); 1989 } 1990 } 1991 this.child = child; 1992 } 1993 1994 void animate(int delay) { 1995 if (mShakeAnimators.containsKey(child)) { 1996 ReorderHintAnimation oldAnimation = mShakeAnimators.get(child); 1997 oldAnimation.completeAnimation(); 1998 mShakeAnimators.remove(child); 1999 } 2000 if (deltaX == 0 && deltaY == 0) { 2001 return; 2002 } 2003 va = ValueAnimator.ofFloat(0f, 1f); 2004 va.setRepeatMode(ValueAnimator.REVERSE); 2005 va.setRepeatCount(ValueAnimator.INFINITE); 2006 va.setDuration(DURATION); 2007 va.addUpdateListener(new AnimatorUpdateListener() { 2008 @Override 2009 public void onAnimationUpdate(ValueAnimator animation) { 2010 float r = ((Float) animation.getAnimatedValue()).floatValue(); 2011 float x = r * deltaX; 2012 float y = r * deltaY; 2013 child.setTranslationX(x); 2014 child.setTranslationY(y); 2015 } 2016 }); 2017 va.addListener(new AnimatorListenerAdapter() { 2018 public void onAnimationRepeat(Animator animation) { 2019 repeatCount++; 2020 // We make sure to end only after a full period 2021 if (cancelOnCycleComplete && repeatCount % 2 == 0) { 2022 va.cancel(); 2023 } 2024 } 2025 }); 2026 va.setStartDelay(Math.max(REORDER_ANIMATION_DURATION, delay)); 2027 mShakeAnimators.put(child, this); 2028 va.start(); 2029 } 2030 2031 2032 private void completeAnimation() { 2033 cancelOnCycleComplete = true; 2034 } 2035 2036 // Returns the time required to complete the current oscillating animation 2037 private int completionTime() { 2038 if (repeatCount % 2 == 0) { 2039 return (int) (va.getDuration() - va.getCurrentPlayTime() + DURATION); 2040 } else { 2041 return (int) (va.getDuration() - va.getCurrentPlayTime()); 2042 } 2043 } 2044 } 2045 2046 private void completeAndClearReorderHintAnimations() { 2047 for (ReorderHintAnimation a: mShakeAnimators.values()) { 2048 a.completeAnimation(); 2049 } 2050 mShakeAnimators.clear(); 2051 } 2052 2053 private int getMaxCompletionTime() { 2054 int maxTime = 0; 2055 for (ReorderHintAnimation a: mShakeAnimators.values()) { 2056 maxTime = Math.max(maxTime, a.completionTime()); 2057 } 2058 return maxTime; 2059 } 2060 2061 private void commitTempPlacement() { 2062 for (int i = 0; i < mCountX; i++) { 2063 for (int j = 0; j < mCountY; j++) { 2064 mOccupied[i][j] = mTmpOccupied[i][j]; 2065 } 2066 } 2067 int childCount = mShortcutsAndWidgets.getChildCount(); 2068 for (int i = 0; i < childCount; i++) { 2069 LayoutParams lp = (LayoutParams) mShortcutsAndWidgets.getChildAt(i).getLayoutParams(); 2070 lp.cellX = lp.tmpCellX; 2071 lp.cellY = lp.tmpCellY; 2072 } 2073 } 2074 2075 public void setUseTempCoords(boolean useTempCoords) { 2076 int childCount = mShortcutsAndWidgets.getChildCount(); 2077 for (int i = 0; i < childCount; i++) { 2078 LayoutParams lp = (LayoutParams) mShortcutsAndWidgets.getChildAt(i).getLayoutParams(); 2079 lp.useTmpCoords = useTempCoords; 2080 } 2081 } 2082 2083 ItemConfiguration findConfigurationNoShuffle(int pixelX, int pixelY, int minSpanX, int minSpanY, 2084 int spanX, int spanY, View dragView, ItemConfiguration solution) { 2085 int[] result = new int[2]; 2086 int[] resultSpan = new int[2]; 2087 findNearestVacantArea(pixelX, pixelY, minSpanX, minSpanY, spanX, spanY, null, result, 2088 resultSpan); 2089 if (result[0] >= 0 && result[1] >= 0) { 2090 copyCurrentStateToSolution(solution, false); 2091 solution.dragViewX = result[0]; 2092 solution.dragViewY = result[1]; 2093 solution.dragViewSpanX = resultSpan[0]; 2094 solution.dragViewSpanY = resultSpan[1]; 2095 solution.isSolution = true; 2096 } else { 2097 solution.isSolution = false; 2098 } 2099 return solution; 2100 } 2101 2102 public void prepareChildForDrag(View child) { 2103 markCellsAsUnoccupiedForView(child); 2104 } 2105 2106 /* This seems like it should be obvious and straight-forward, but when the direction vector 2107 needs to match with the notion of the dragView pushing other views, we have to employ 2108 a slightly more subtle notion of the direction vector. The question is what two points is 2109 the vector between? The center of the dragView and its desired destination? Not quite, as 2110 this doesn't necessarily coincide with the interaction of the dragView and items occupying 2111 those cells. Instead we use some heuristics to often lock the vector to up, down, left 2112 or right, which helps make pushing feel right. 2113 */ 2114 private void getDirectionVectorForDrop(int dragViewCenterX, int dragViewCenterY, int spanX, 2115 int spanY, View dragView, int[] resultDirection) { 2116 int[] targetDestination = new int[2]; 2117 2118 findNearestArea(dragViewCenterX, dragViewCenterY, spanX, spanY, targetDestination); 2119 Rect dragRect = new Rect(); 2120 regionToRect(targetDestination[0], targetDestination[1], spanX, spanY, dragRect); 2121 dragRect.offset(dragViewCenterX - dragRect.centerX(), dragViewCenterY - dragRect.centerY()); 2122 2123 Rect dropRegionRect = new Rect(); 2124 getViewsIntersectingRegion(targetDestination[0], targetDestination[1], spanX, spanY, 2125 dragView, dropRegionRect, mIntersectingViews); 2126 2127 int dropRegionSpanX = dropRegionRect.width(); 2128 int dropRegionSpanY = dropRegionRect.height(); 2129 2130 regionToRect(dropRegionRect.left, dropRegionRect.top, dropRegionRect.width(), 2131 dropRegionRect.height(), dropRegionRect); 2132 2133 int deltaX = (dropRegionRect.centerX() - dragViewCenterX) / spanX; 2134 int deltaY = (dropRegionRect.centerY() - dragViewCenterY) / spanY; 2135 2136 if (dropRegionSpanX == mCountX || spanX == mCountX) { 2137 deltaX = 0; 2138 } 2139 if (dropRegionSpanY == mCountY || spanY == mCountY) { 2140 deltaY = 0; 2141 } 2142 2143 if (deltaX == 0 && deltaY == 0) { 2144 // No idea what to do, give a random direction. 2145 resultDirection[0] = 1; 2146 resultDirection[1] = 0; 2147 } else { 2148 computeDirectionVector(deltaX, deltaY, resultDirection); 2149 } 2150 } 2151 2152 // For a given cell and span, fetch the set of views intersecting the region. 2153 private void getViewsIntersectingRegion(int cellX, int cellY, int spanX, int spanY, 2154 View dragView, Rect boundingRect, ArrayList<View> intersectingViews) { 2155 if (boundingRect != null) { 2156 boundingRect.set(cellX, cellY, cellX + spanX, cellY + spanY); 2157 } 2158 intersectingViews.clear(); 2159 Rect r0 = new Rect(cellX, cellY, cellX + spanX, cellY + spanY); 2160 Rect r1 = new Rect(); 2161 final int count = mShortcutsAndWidgets.getChildCount(); 2162 for (int i = 0; i < count; i++) { 2163 View child = mShortcutsAndWidgets.getChildAt(i); 2164 if (child == dragView) continue; 2165 LayoutParams lp = (LayoutParams) child.getLayoutParams(); 2166 r1.set(lp.cellX, lp.cellY, lp.cellX + lp.cellHSpan, lp.cellY + lp.cellVSpan); 2167 if (Rect.intersects(r0, r1)) { 2168 mIntersectingViews.add(child); 2169 if (boundingRect != null) { 2170 boundingRect.union(r1); 2171 } 2172 } 2173 } 2174 } 2175 2176 boolean isNearestDropLocationOccupied(int pixelX, int pixelY, int spanX, int spanY, 2177 View dragView, int[] result) { 2178 result = findNearestArea(pixelX, pixelY, spanX, spanY, result); 2179 getViewsIntersectingRegion(result[0], result[1], spanX, spanY, dragView, null, 2180 mIntersectingViews); 2181 return !mIntersectingViews.isEmpty(); 2182 } 2183 2184 void revertTempState() { 2185 if (!isItemPlacementDirty() || DESTRUCTIVE_REORDER) return; 2186 final int count = mShortcutsAndWidgets.getChildCount(); 2187 for (int i = 0; i < count; i++) { 2188 View child = mShortcutsAndWidgets.getChildAt(i); 2189 LayoutParams lp = (LayoutParams) child.getLayoutParams(); 2190 if (lp.tmpCellX != lp.cellX || lp.tmpCellY != lp.cellY) { 2191 lp.tmpCellX = lp.cellX; 2192 lp.tmpCellY = lp.cellY; 2193 animateChildToPosition(child, lp.cellX, lp.cellY, REORDER_ANIMATION_DURATION, 2194 0, false, false); 2195 } 2196 } 2197 completeAndClearReorderHintAnimations(); 2198 setItemPlacementDirty(false); 2199 } 2200 2201 int[] createArea(int pixelX, int pixelY, int minSpanX, int minSpanY, int spanX, int spanY, 2202 View dragView, int[] result, int resultSpan[], int mode) { 2203 // First we determine if things have moved enough to cause a different layout 2204 result = findNearestArea(pixelX, pixelY, spanX, spanY, result); 2205 2206 if (resultSpan == null) { 2207 resultSpan = new int[2]; 2208 } 2209 2210 // When we are checking drop validity or actually dropping, we don't recompute the 2211 // direction vector, since we want the solution to match the preview, and it's possible 2212 // that the exact position of the item has changed to result in a new reordering outcome. 2213 if ((mode == MODE_ON_DROP || mode == MODE_ACCEPT_DROP) 2214 && mPreviousReorderDirection[0] != -1) { 2215 mDirectionVector[0] = mPreviousReorderDirection[0]; 2216 mDirectionVector[1] = mPreviousReorderDirection[1]; 2217 // We reset this vector after drop 2218 if (mode == MODE_ON_DROP) { 2219 mPreviousReorderDirection[0] = -1; 2220 mPreviousReorderDirection[1] = -1; 2221 } 2222 } else { 2223 getDirectionVectorForDrop(pixelX, pixelY, spanX, spanY, dragView, mDirectionVector); 2224 mPreviousReorderDirection[0] = mDirectionVector[0]; 2225 mPreviousReorderDirection[1] = mDirectionVector[1]; 2226 } 2227 2228 ItemConfiguration swapSolution = simpleSwap(pixelX, pixelY, minSpanX, minSpanY, 2229 spanX, spanY, mDirectionVector, dragView, true, new ItemConfiguration()); 2230 2231 // We attempt the approach which doesn't shuffle views at all 2232 ItemConfiguration noShuffleSolution = findConfigurationNoShuffle(pixelX, pixelY, minSpanX, 2233 minSpanY, spanX, spanY, dragView, new ItemConfiguration()); 2234 2235 ItemConfiguration finalSolution = null; 2236 if (swapSolution.isSolution && swapSolution.area() >= noShuffleSolution.area()) { 2237 finalSolution = swapSolution; 2238 } else if (noShuffleSolution.isSolution) { 2239 finalSolution = noShuffleSolution; 2240 } 2241 2242 boolean foundSolution = true; 2243 if (!DESTRUCTIVE_REORDER) { 2244 setUseTempCoords(true); 2245 } 2246 2247 if (finalSolution != null) { 2248 result[0] = finalSolution.dragViewX; 2249 result[1] = finalSolution.dragViewY; 2250 resultSpan[0] = finalSolution.dragViewSpanX; 2251 resultSpan[1] = finalSolution.dragViewSpanY; 2252 2253 // If we're just testing for a possible location (MODE_ACCEPT_DROP), we don't bother 2254 // committing anything or animating anything as we just want to determine if a solution 2255 // exists 2256 if (mode == MODE_DRAG_OVER || mode == MODE_ON_DROP || mode == MODE_ON_DROP_EXTERNAL) { 2257 if (!DESTRUCTIVE_REORDER) { 2258 copySolutionToTempState(finalSolution, dragView); 2259 } 2260 setItemPlacementDirty(true); 2261 animateItemsToSolution(finalSolution, dragView, mode == MODE_ON_DROP); 2262 2263 if (!DESTRUCTIVE_REORDER && 2264 (mode == MODE_ON_DROP || mode == MODE_ON_DROP_EXTERNAL)) { 2265 commitTempPlacement(); 2266 completeAndClearReorderHintAnimations(); 2267 setItemPlacementDirty(false); 2268 } else { 2269 beginOrAdjustHintAnimations(finalSolution, dragView, 2270 REORDER_ANIMATION_DURATION); 2271 } 2272 } 2273 } else { 2274 foundSolution = false; 2275 result[0] = result[1] = resultSpan[0] = resultSpan[1] = -1; 2276 } 2277 2278 if ((mode == MODE_ON_DROP || !foundSolution) && !DESTRUCTIVE_REORDER) { 2279 setUseTempCoords(false); 2280 } 2281 2282 mShortcutsAndWidgets.requestLayout(); 2283 return result; 2284 } 2285 2286 void setItemPlacementDirty(boolean dirty) { 2287 mItemPlacementDirty = dirty; 2288 } 2289 boolean isItemPlacementDirty() { 2290 return mItemPlacementDirty; 2291 } 2292 2293 private class ItemConfiguration { 2294 HashMap<View, CellAndSpan> map = new HashMap<View, CellAndSpan>(); 2295 boolean isSolution = false; 2296 int dragViewX, dragViewY, dragViewSpanX, dragViewSpanY; 2297 2298 int area() { 2299 return dragViewSpanX * dragViewSpanY; 2300 } 2301 } 2302 2303 private class CellAndSpan { 2304 int x, y; 2305 int spanX, spanY; 2306 2307 public CellAndSpan(int x, int y, int spanX, int spanY) { 2308 this.x = x; 2309 this.y = y; 2310 this.spanX = spanX; 2311 this.spanY = spanY; 2312 } 2313 } 2314 2315 /** 2316 * Find a vacant area that will fit the given bounds nearest the requested 2317 * cell location. Uses Euclidean distance to score multiple vacant areas. 2318 * 2319 * @param pixelX The X location at which you want to search for a vacant area. 2320 * @param pixelY The Y location at which you want to search for a vacant area. 2321 * @param spanX Horizontal span of the object. 2322 * @param spanY Vertical span of the object. 2323 * @param ignoreView Considers space occupied by this view as unoccupied 2324 * @param result Previously returned value to possibly recycle. 2325 * @return The X, Y cell of a vacant area that can contain this object, 2326 * nearest the requested location. 2327 */ 2328 int[] findNearestVacantArea( 2329 int pixelX, int pixelY, int spanX, int spanY, View ignoreView, int[] result) { 2330 return findNearestArea(pixelX, pixelY, spanX, spanY, ignoreView, true, result); 2331 } 2332 2333 /** 2334 * Find a vacant area that will fit the given bounds nearest the requested 2335 * cell location. Uses Euclidean distance to score multiple vacant areas. 2336 * 2337 * @param pixelX The X location at which you want to search for a vacant area. 2338 * @param pixelY The Y location at which you want to search for a vacant area. 2339 * @param minSpanX The minimum horizontal span required 2340 * @param minSpanY The minimum vertical span required 2341 * @param spanX Horizontal span of the object. 2342 * @param spanY Vertical span of the object. 2343 * @param ignoreView Considers space occupied by this view as unoccupied 2344 * @param result Previously returned value to possibly recycle. 2345 * @return The X, Y cell of a vacant area that can contain this object, 2346 * nearest the requested location. 2347 */ 2348 int[] findNearestVacantArea(int pixelX, int pixelY, int minSpanX, int minSpanY, 2349 int spanX, int spanY, View ignoreView, int[] result, int[] resultSpan) { 2350 return findNearestArea(pixelX, pixelY, minSpanX, minSpanY, spanX, spanY, ignoreView, true, 2351 result, resultSpan, mOccupied); 2352 } 2353 2354 /** 2355 * Find a starting cell position that will fit the given bounds nearest the requested 2356 * cell location. Uses Euclidean distance to score multiple vacant areas. 2357 * 2358 * @param pixelX The X location at which you want to search for a vacant area. 2359 * @param pixelY The Y location at which you want to search for a vacant area. 2360 * @param spanX Horizontal span of the object. 2361 * @param spanY Vertical span of the object. 2362 * @param ignoreView Considers space occupied by this view as unoccupied 2363 * @param result Previously returned value to possibly recycle. 2364 * @return The X, Y cell of a vacant area that can contain this object, 2365 * nearest the requested location. 2366 */ 2367 int[] findNearestArea( 2368 int pixelX, int pixelY, int spanX, int spanY, int[] result) { 2369 return findNearestArea(pixelX, pixelY, spanX, spanY, null, false, result); 2370 } 2371 2372 boolean existsEmptyCell() { 2373 return findCellForSpan(null, 1, 1); 2374 } 2375 2376 /** 2377 * Finds the upper-left coordinate of the first rectangle in the grid that can 2378 * hold a cell of the specified dimensions. If intersectX and intersectY are not -1, 2379 * then this method will only return coordinates for rectangles that contain the cell 2380 * (intersectX, intersectY) 2381 * 2382 * @param cellXY The array that will contain the position of a vacant cell if such a cell 2383 * can be found. 2384 * @param spanX The horizontal span of the cell we want to find. 2385 * @param spanY The vertical span of the cell we want to find. 2386 * 2387 * @return True if a vacant cell of the specified dimension was found, false otherwise. 2388 */ 2389 boolean findCellForSpan(int[] cellXY, int spanX, int spanY) { 2390 return findCellForSpanThatIntersectsIgnoring(cellXY, spanX, spanY, -1, -1, null, mOccupied); 2391 } 2392 2393 /** 2394 * Like above, but ignores any cells occupied by the item "ignoreView" 2395 * 2396 * @param cellXY The array that will contain the position of a vacant cell if such a cell 2397 * can be found. 2398 * @param spanX The horizontal span of the cell we want to find. 2399 * @param spanY The vertical span of the cell we want to find. 2400 * @param ignoreView The home screen item we should treat as not occupying any space 2401 * @return 2402 */ 2403 boolean findCellForSpanIgnoring(int[] cellXY, int spanX, int spanY, View ignoreView) { 2404 return findCellForSpanThatIntersectsIgnoring(cellXY, spanX, spanY, -1, -1, 2405 ignoreView, mOccupied); 2406 } 2407 2408 /** 2409 * Like above, but if intersectX and intersectY are not -1, then this method will try to 2410 * return coordinates for rectangles that contain the cell [intersectX, intersectY] 2411 * 2412 * @param spanX The horizontal span of the cell we want to find. 2413 * @param spanY The vertical span of the cell we want to find. 2414 * @param ignoreView The home screen item we should treat as not occupying any space 2415 * @param intersectX The X coordinate of the cell that we should try to overlap 2416 * @param intersectX The Y coordinate of the cell that we should try to overlap 2417 * 2418 * @return True if a vacant cell of the specified dimension was found, false otherwise. 2419 */ 2420 boolean findCellForSpanThatIntersects(int[] cellXY, int spanX, int spanY, 2421 int intersectX, int intersectY) { 2422 return findCellForSpanThatIntersectsIgnoring( 2423 cellXY, spanX, spanY, intersectX, intersectY, null, mOccupied); 2424 } 2425 2426 /** 2427 * The superset of the above two methods 2428 */ 2429 boolean findCellForSpanThatIntersectsIgnoring(int[] cellXY, int spanX, int spanY, 2430 int intersectX, int intersectY, View ignoreView, boolean occupied[][]) { 2431 // mark space take by ignoreView as available (method checks if ignoreView is null) 2432 markCellsAsUnoccupiedForView(ignoreView, occupied); 2433 2434 boolean foundCell = false; 2435 while (true) { 2436 int startX = 0; 2437 if (intersectX >= 0) { 2438 startX = Math.max(startX, intersectX - (spanX - 1)); 2439 } 2440 int endX = mCountX - (spanX - 1); 2441 if (intersectX >= 0) { 2442 endX = Math.min(endX, intersectX + (spanX - 1) + (spanX == 1 ? 1 : 0)); 2443 } 2444 int startY = 0; 2445 if (intersectY >= 0) { 2446 startY = Math.max(startY, intersectY - (spanY - 1)); 2447 } 2448 int endY = mCountY - (spanY - 1); 2449 if (intersectY >= 0) { 2450 endY = Math.min(endY, intersectY + (spanY - 1) + (spanY == 1 ? 1 : 0)); 2451 } 2452 2453 for (int y = startY; y < endY && !foundCell; y++) { 2454 inner: 2455 for (int x = startX; x < endX; x++) { 2456 for (int i = 0; i < spanX; i++) { 2457 for (int j = 0; j < spanY; j++) { 2458 if (occupied[x + i][y + j]) { 2459 // small optimization: we can skip to after the column we just found 2460 // an occupied cell 2461 x += i; 2462 continue inner; 2463 } 2464 } 2465 } 2466 if (cellXY != null) { 2467 cellXY[0] = x; 2468 cellXY[1] = y; 2469 } 2470 foundCell = true; 2471 break; 2472 } 2473 } 2474 if (intersectX == -1 && intersectY == -1) { 2475 break; 2476 } else { 2477 // if we failed to find anything, try again but without any requirements of 2478 // intersecting 2479 intersectX = -1; 2480 intersectY = -1; 2481 continue; 2482 } 2483 } 2484 2485 // re-mark space taken by ignoreView as occupied 2486 markCellsAsOccupiedForView(ignoreView, occupied); 2487 return foundCell; 2488 } 2489 2490 /** 2491 * A drag event has begun over this layout. 2492 * It may have begun over this layout (in which case onDragChild is called first), 2493 * or it may have begun on another layout. 2494 */ 2495 void onDragEnter() { 2496 if (!mDragging) { 2497 // Fade in the drag indicators 2498 if (mCrosshairsAnimator != null) { 2499 mCrosshairsAnimator.animateIn(); 2500 } 2501 } 2502 mDragging = true; 2503 } 2504 2505 /** 2506 * Called when drag has left this CellLayout or has been completed (successfully or not) 2507 */ 2508 void onDragExit() { 2509 // This can actually be called when we aren't in a drag, e.g. when adding a new 2510 // item to this layout via the customize drawer. 2511 // Guard against that case. 2512 if (mDragging) { 2513 mDragging = false; 2514 2515 // Fade out the drag indicators 2516 if (mCrosshairsAnimator != null) { 2517 mCrosshairsAnimator.animateOut(); 2518 } 2519 } 2520 2521 // Invalidate the drag data 2522 mDragCell[0] = mDragCell[1] = -1; 2523 mDragOutlineAnims[mDragOutlineCurrent].animateOut(); 2524 mDragOutlineCurrent = (mDragOutlineCurrent + 1) % mDragOutlineAnims.length; 2525 revertTempState(); 2526 setIsDragOverlapping(false); 2527 } 2528 2529 /** 2530 * Mark a child as having been dropped. 2531 * At the beginning of the drag operation, the child may have been on another 2532 * screen, but it is re-parented before this method is called. 2533 * 2534 * @param child The child that is being dropped 2535 */ 2536 void onDropChild(View child) { 2537 if (child != null) { 2538 LayoutParams lp = (LayoutParams) child.getLayoutParams(); 2539 lp.dropped = true; 2540 child.requestLayout(); 2541 } 2542 } 2543 2544 /** 2545 * Computes a bounding rectangle for a range of cells 2546 * 2547 * @param cellX X coordinate of upper left corner expressed as a cell position 2548 * @param cellY Y coordinate of upper left corner expressed as a cell position 2549 * @param cellHSpan Width in cells 2550 * @param cellVSpan Height in cells 2551 * @param resultRect Rect into which to put the results 2552 */ 2553 public void cellToRect(int cellX, int cellY, int cellHSpan, int cellVSpan, Rect resultRect) { 2554 final int cellWidth = mCellWidth; 2555 final int cellHeight = mCellHeight; 2556 final int widthGap = mWidthGap; 2557 final int heightGap = mHeightGap; 2558 2559 final int hStartPadding = getPaddingLeft(); 2560 final int vStartPadding = getPaddingTop(); 2561 2562 int width = cellHSpan * cellWidth + ((cellHSpan - 1) * widthGap); 2563 int height = cellVSpan * cellHeight + ((cellVSpan - 1) * heightGap); 2564 2565 int x = hStartPadding + cellX * (cellWidth + widthGap); 2566 int y = vStartPadding + cellY * (cellHeight + heightGap); 2567 2568 resultRect.set(x, y, x + width, y + height); 2569 } 2570 2571 /** 2572 * Computes the required horizontal and vertical cell spans to always 2573 * fit the given rectangle. 2574 * 2575 * @param width Width in pixels 2576 * @param height Height in pixels 2577 * @param result An array of length 2 in which to store the result (may be null). 2578 */ 2579 public int[] rectToCell(int width, int height, int[] result) { 2580 return rectToCell(getResources(), width, height, result); 2581 } 2582 2583 public static int[] rectToCell(Resources resources, int width, int height, int[] result) { 2584 // Always assume we're working with the smallest span to make sure we 2585 // reserve enough space in both orientations. 2586 int actualWidth = resources.getDimensionPixelSize(R.dimen.workspace_cell_width); 2587 int actualHeight = resources.getDimensionPixelSize(R.dimen.workspace_cell_height); 2588 int smallerSize = Math.min(actualWidth, actualHeight); 2589 2590 // Always round up to next largest cell 2591 int spanX = (int) Math.ceil(width / (float) smallerSize); 2592 int spanY = (int) Math.ceil(height / (float) smallerSize); 2593 2594 if (result == null) { 2595 return new int[] { spanX, spanY }; 2596 } 2597 result[0] = spanX; 2598 result[1] = spanY; 2599 return result; 2600 } 2601 2602 public int[] cellSpansToSize(int hSpans, int vSpans) { 2603 int[] size = new int[2]; 2604 size[0] = hSpans * mCellWidth + (hSpans - 1) * mWidthGap; 2605 size[1] = vSpans * mCellHeight + (vSpans - 1) * mHeightGap; 2606 return size; 2607 } 2608 2609 /** 2610 * Calculate the grid spans needed to fit given item 2611 */ 2612 public void calculateSpans(ItemInfo info) { 2613 final int minWidth; 2614 final int minHeight; 2615 2616 if (info instanceof LauncherAppWidgetInfo) { 2617 minWidth = ((LauncherAppWidgetInfo) info).minWidth; 2618 minHeight = ((LauncherAppWidgetInfo) info).minHeight; 2619 } else if (info instanceof PendingAddWidgetInfo) { 2620 minWidth = ((PendingAddWidgetInfo) info).minWidth; 2621 minHeight = ((PendingAddWidgetInfo) info).minHeight; 2622 } else { 2623 // It's not a widget, so it must be 1x1 2624 info.spanX = info.spanY = 1; 2625 return; 2626 } 2627 int[] spans = rectToCell(minWidth, minHeight, null); 2628 info.spanX = spans[0]; 2629 info.spanY = spans[1]; 2630 } 2631 2632 /** 2633 * Find the first vacant cell, if there is one. 2634 * 2635 * @param vacant Holds the x and y coordinate of the vacant cell 2636 * @param spanX Horizontal cell span. 2637 * @param spanY Vertical cell span. 2638 * 2639 * @return True if a vacant cell was found 2640 */ 2641 public boolean getVacantCell(int[] vacant, int spanX, int spanY) { 2642 2643 return findVacantCell(vacant, spanX, spanY, mCountX, mCountY, mOccupied); 2644 } 2645 2646 static boolean findVacantCell(int[] vacant, int spanX, int spanY, 2647 int xCount, int yCount, boolean[][] occupied) { 2648 2649 for (int y = 0; y < yCount; y++) { 2650 for (int x = 0; x < xCount; x++) { 2651 boolean available = !occupied[x][y]; 2652out: for (int i = x; i < x + spanX - 1 && x < xCount; i++) { 2653 for (int j = y; j < y + spanY - 1 && y < yCount; j++) { 2654 available = available && !occupied[i][j]; 2655 if (!available) break out; 2656 } 2657 } 2658 2659 if (available) { 2660 vacant[0] = x; 2661 vacant[1] = y; 2662 return true; 2663 } 2664 } 2665 } 2666 2667 return false; 2668 } 2669 2670 private void clearOccupiedCells() { 2671 for (int x = 0; x < mCountX; x++) { 2672 for (int y = 0; y < mCountY; y++) { 2673 mOccupied[x][y] = false; 2674 } 2675 } 2676 } 2677 2678 /** 2679 * Given a view, determines how much that view can be expanded in all directions, in terms of 2680 * whether or not there are other items occupying adjacent cells. Used by the 2681 * AppWidgetResizeFrame to determine how the widget can be resized. 2682 */ 2683 public void getExpandabilityArrayForView(View view, int[] expandability) { 2684 final LayoutParams lp = (LayoutParams) view.getLayoutParams(); 2685 boolean flag; 2686 2687 expandability[AppWidgetResizeFrame.LEFT] = 0; 2688 for (int x = lp.cellX - 1; x >= 0; x--) { 2689 flag = false; 2690 for (int y = lp.cellY; y < lp.cellY + lp.cellVSpan; y++) { 2691 if (mOccupied[x][y]) flag = true; 2692 } 2693 if (flag) break; 2694 expandability[AppWidgetResizeFrame.LEFT]++; 2695 } 2696 2697 expandability[AppWidgetResizeFrame.TOP] = 0; 2698 for (int y = lp.cellY - 1; y >= 0; y--) { 2699 flag = false; 2700 for (int x = lp.cellX; x < lp.cellX + lp.cellHSpan; x++) { 2701 if (mOccupied[x][y]) flag = true; 2702 } 2703 if (flag) break; 2704 expandability[AppWidgetResizeFrame.TOP]++; 2705 } 2706 2707 expandability[AppWidgetResizeFrame.RIGHT] = 0; 2708 for (int x = lp.cellX + lp.cellHSpan; x < mCountX; x++) { 2709 flag = false; 2710 for (int y = lp.cellY; y < lp.cellY + lp.cellVSpan; y++) { 2711 if (mOccupied[x][y]) flag = true; 2712 } 2713 if (flag) break; 2714 expandability[AppWidgetResizeFrame.RIGHT]++; 2715 } 2716 2717 expandability[AppWidgetResizeFrame.BOTTOM] = 0; 2718 for (int y = lp.cellY + lp.cellVSpan; y < mCountY; y++) { 2719 flag = false; 2720 for (int x = lp.cellX; x < lp.cellX + lp.cellHSpan; x++) { 2721 if (mOccupied[x][y]) flag = true; 2722 } 2723 if (flag) break; 2724 expandability[AppWidgetResizeFrame.BOTTOM]++; 2725 } 2726 } 2727 2728 public void onMove(View view, int newCellX, int newCellY, int newSpanX, int newSpanY) { 2729 markCellsAsUnoccupiedForView(view); 2730 markCellsForView(newCellX, newCellY, newSpanX, newSpanY, mOccupied, true); 2731 } 2732 2733 public void markCellsAsOccupiedForView(View view) { 2734 markCellsAsOccupiedForView(view, mOccupied); 2735 } 2736 public void markCellsAsOccupiedForView(View view, boolean[][] occupied) { 2737 if (view == null || view.getParent() != mShortcutsAndWidgets) return; 2738 LayoutParams lp = (LayoutParams) view.getLayoutParams(); 2739 markCellsForView(lp.cellX, lp.cellY, lp.cellHSpan, lp.cellVSpan, occupied, true); 2740 } 2741 2742 public void markCellsAsUnoccupiedForView(View view) { 2743 markCellsAsUnoccupiedForView(view, mOccupied); 2744 } 2745 public void markCellsAsUnoccupiedForView(View view, boolean occupied[][]) { 2746 if (view == null || view.getParent() != mShortcutsAndWidgets) return; 2747 LayoutParams lp = (LayoutParams) view.getLayoutParams(); 2748 markCellsForView(lp.cellX, lp.cellY, lp.cellHSpan, lp.cellVSpan, occupied, false); 2749 } 2750 2751 private void markCellsForView(int cellX, int cellY, int spanX, int spanY, boolean[][] occupied, 2752 boolean value) { 2753 if (cellX < 0 || cellY < 0) return; 2754 for (int x = cellX; x < cellX + spanX && x < mCountX; x++) { 2755 for (int y = cellY; y < cellY + spanY && y < mCountY; y++) { 2756 occupied[x][y] = value; 2757 } 2758 } 2759 } 2760 2761 public int getDesiredWidth() { 2762 return mPaddingLeft + mPaddingRight + (mCountX * mCellWidth) + 2763 (Math.max((mCountX - 1), 0) * mWidthGap); 2764 } 2765 2766 public int getDesiredHeight() { 2767 return mPaddingTop + mPaddingBottom + (mCountY * mCellHeight) + 2768 (Math.max((mCountY - 1), 0) * mHeightGap); 2769 } 2770 2771 public boolean isOccupied(int x, int y) { 2772 if (x < mCountX && y < mCountY) { 2773 return mOccupied[x][y]; 2774 } else { 2775 throw new RuntimeException("Position exceeds the bound of this CellLayout"); 2776 } 2777 } 2778 2779 @Override 2780 public ViewGroup.LayoutParams generateLayoutParams(AttributeSet attrs) { 2781 return new CellLayout.LayoutParams(getContext(), attrs); 2782 } 2783 2784 @Override 2785 protected boolean checkLayoutParams(ViewGroup.LayoutParams p) { 2786 return p instanceof CellLayout.LayoutParams; 2787 } 2788 2789 @Override 2790 protected ViewGroup.LayoutParams generateLayoutParams(ViewGroup.LayoutParams p) { 2791 return new CellLayout.LayoutParams(p); 2792 } 2793 2794 public static class CellLayoutAnimationController extends LayoutAnimationController { 2795 public CellLayoutAnimationController(Animation animation, float delay) { 2796 super(animation, delay); 2797 } 2798 2799 @Override 2800 protected long getDelayForView(View view) { 2801 return (int) (Math.random() * 150); 2802 } 2803 } 2804 2805 public static class LayoutParams extends ViewGroup.MarginLayoutParams { 2806 /** 2807 * Horizontal location of the item in the grid. 2808 */ 2809 @ViewDebug.ExportedProperty 2810 public int cellX; 2811 2812 /** 2813 * Vertical location of the item in the grid. 2814 */ 2815 @ViewDebug.ExportedProperty 2816 public int cellY; 2817 2818 /** 2819 * Temporary horizontal location of the item in the grid during reorder 2820 */ 2821 public int tmpCellX; 2822 2823 /** 2824 * Temporary vertical location of the item in the grid during reorder 2825 */ 2826 public int tmpCellY; 2827 2828 /** 2829 * Indicates that the temporary coordinates should be used to layout the items 2830 */ 2831 public boolean useTmpCoords; 2832 2833 /** 2834 * Number of cells spanned horizontally by the item. 2835 */ 2836 @ViewDebug.ExportedProperty 2837 public int cellHSpan; 2838 2839 /** 2840 * Number of cells spanned vertically by the item. 2841 */ 2842 @ViewDebug.ExportedProperty 2843 public int cellVSpan; 2844 2845 /** 2846 * Indicates whether the item will set its x, y, width and height parameters freely, 2847 * or whether these will be computed based on cellX, cellY, cellHSpan and cellVSpan. 2848 */ 2849 public boolean isLockedToGrid = true; 2850 2851 /** 2852 * Indicates whether this item can be reordered. Always true except in the case of the 2853 * the AllApps button. 2854 */ 2855 public boolean canReorder = true; 2856 2857 // X coordinate of the view in the layout. 2858 @ViewDebug.ExportedProperty 2859 int x; 2860 // Y coordinate of the view in the layout. 2861 @ViewDebug.ExportedProperty 2862 int y; 2863 2864 boolean dropped; 2865 2866 public LayoutParams(Context c, AttributeSet attrs) { 2867 super(c, attrs); 2868 cellHSpan = 1; 2869 cellVSpan = 1; 2870 } 2871 2872 public LayoutParams(ViewGroup.LayoutParams source) { 2873 super(source); 2874 cellHSpan = 1; 2875 cellVSpan = 1; 2876 } 2877 2878 public LayoutParams(LayoutParams source) { 2879 super(source); 2880 this.cellX = source.cellX; 2881 this.cellY = source.cellY; 2882 this.cellHSpan = source.cellHSpan; 2883 this.cellVSpan = source.cellVSpan; 2884 } 2885 2886 public LayoutParams(int cellX, int cellY, int cellHSpan, int cellVSpan) { 2887 super(LayoutParams.MATCH_PARENT, LayoutParams.MATCH_PARENT); 2888 this.cellX = cellX; 2889 this.cellY = cellY; 2890 this.cellHSpan = cellHSpan; 2891 this.cellVSpan = cellVSpan; 2892 } 2893 2894 public void setup(int cellWidth, int cellHeight, int widthGap, int heightGap) { 2895 if (isLockedToGrid) { 2896 final int myCellHSpan = cellHSpan; 2897 final int myCellVSpan = cellVSpan; 2898 final int myCellX = useTmpCoords ? tmpCellX : cellX; 2899 final int myCellY = useTmpCoords ? tmpCellY : cellY; 2900 2901 width = myCellHSpan * cellWidth + ((myCellHSpan - 1) * widthGap) - 2902 leftMargin - rightMargin; 2903 height = myCellVSpan * cellHeight + ((myCellVSpan - 1) * heightGap) - 2904 topMargin - bottomMargin; 2905 x = (int) (myCellX * (cellWidth + widthGap) + leftMargin); 2906 y = (int) (myCellY * (cellHeight + heightGap) + topMargin); 2907 } 2908 } 2909 2910 public String toString() { 2911 return "(" + this.cellX + ", " + this.cellY + ")"; 2912 } 2913 2914 public void setWidth(int width) { 2915 this.width = width; 2916 } 2917 2918 public int getWidth() { 2919 return width; 2920 } 2921 2922 public void setHeight(int height) { 2923 this.height = height; 2924 } 2925 2926 public int getHeight() { 2927 return height; 2928 } 2929 2930 public void setX(int x) { 2931 this.x = x; 2932 } 2933 2934 public int getX() { 2935 return x; 2936 } 2937 2938 public void setY(int y) { 2939 this.y = y; 2940 } 2941 2942 public int getY() { 2943 return y; 2944 } 2945 } 2946 2947 // This class stores info for two purposes: 2948 // 1. When dragging items (mDragInfo in Workspace), we store the View, its cellX & cellY, 2949 // its spanX, spanY, and the screen it is on 2950 // 2. When long clicking on an empty cell in a CellLayout, we save information about the 2951 // cellX and cellY coordinates and which page was clicked. We then set this as a tag on 2952 // the CellLayout that was long clicked 2953 static final class CellInfo { 2954 View cell; 2955 int cellX = -1; 2956 int cellY = -1; 2957 int spanX; 2958 int spanY; 2959 int screen; 2960 long container; 2961 2962 @Override 2963 public String toString() { 2964 return "Cell[view=" + (cell == null ? "null" : cell.getClass()) 2965 + ", x=" + cellX + ", y=" + cellY + "]"; 2966 } 2967 } 2968 2969 public boolean lastDownOnOccupiedCell() { 2970 return mLastDownOnOccupiedCell; 2971 } 2972} 2973