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