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