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