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