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