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