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