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