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