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