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