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 com.android.launcher3.dragndrop.DraggableView.DRAGGABLE_ICON; 20 import static com.android.launcher3.icons.IconNormalizer.ICON_VISIBLE_AREA_FACTOR; 21 import static com.android.launcher3.util.MultiTranslateDelegate.INDEX_REORDER_PREVIEW_OFFSET; 22 23 import android.animation.Animator; 24 import android.animation.AnimatorListenerAdapter; 25 import android.animation.TimeInterpolator; 26 import android.animation.ValueAnimator; 27 import android.animation.ValueAnimator.AnimatorUpdateListener; 28 import android.annotation.SuppressLint; 29 import android.content.Context; 30 import android.content.res.Resources; 31 import android.content.res.TypedArray; 32 import android.graphics.Canvas; 33 import android.graphics.Color; 34 import android.graphics.Paint; 35 import android.graphics.Point; 36 import android.graphics.Rect; 37 import android.graphics.RectF; 38 import android.graphics.drawable.Drawable; 39 import android.os.Parcelable; 40 import android.util.ArrayMap; 41 import android.util.AttributeSet; 42 import android.util.FloatProperty; 43 import android.util.Log; 44 import android.util.SparseArray; 45 import android.view.MotionEvent; 46 import android.view.View; 47 import android.view.ViewDebug; 48 import android.view.ViewGroup; 49 import android.view.accessibility.AccessibilityEvent; 50 51 import androidx.annotation.IntDef; 52 import androidx.annotation.Nullable; 53 import androidx.annotation.Px; 54 import androidx.core.graphics.ColorUtils; 55 import androidx.core.view.ViewCompat; 56 57 import com.android.app.animation.Interpolators; 58 import com.android.launcher3.LauncherSettings.Favorites; 59 import com.android.launcher3.accessibility.DragAndDropAccessibilityDelegate; 60 import com.android.launcher3.celllayout.CellLayoutLayoutParams; 61 import com.android.launcher3.celllayout.CellPosMapper.CellPos; 62 import com.android.launcher3.celllayout.DelegatedCellDrawing; 63 import com.android.launcher3.celllayout.ItemConfiguration; 64 import com.android.launcher3.celllayout.ReorderAlgorithm; 65 import com.android.launcher3.celllayout.ReorderParameters; 66 import com.android.launcher3.celllayout.ReorderPreviewAnimation; 67 import com.android.launcher3.config.FeatureFlags; 68 import com.android.launcher3.dragndrop.DraggableView; 69 import com.android.launcher3.folder.PreviewBackground; 70 import com.android.launcher3.model.data.ItemInfo; 71 import com.android.launcher3.model.data.LauncherAppWidgetInfo; 72 import com.android.launcher3.util.CellAndSpan; 73 import com.android.launcher3.util.GridOccupancy; 74 import com.android.launcher3.util.MultiTranslateDelegate; 75 import com.android.launcher3.util.ParcelableSparseArray; 76 import com.android.launcher3.util.Themes; 77 import com.android.launcher3.util.Thunk; 78 import com.android.launcher3.views.ActivityContext; 79 import com.android.launcher3.widget.LauncherAppWidgetHostView; 80 81 import java.lang.annotation.Retention; 82 import java.lang.annotation.RetentionPolicy; 83 import java.util.ArrayList; 84 import java.util.Arrays; 85 import java.util.Stack; 86 87 public class CellLayout extends ViewGroup { 88 private static final String TAG = "CellLayout"; 89 private static final boolean LOGD = false; 90 91 /** The color of the "leave-behind" shape when a folder is opened from Hotseat. */ 92 private static final int FOLDER_LEAVE_BEHIND_COLOR = Color.argb(160, 245, 245, 245); 93 94 protected final ActivityContext mActivity; 95 @ViewDebug.ExportedProperty(category = "launcher") 96 @Thunk int mCellWidth; 97 @ViewDebug.ExportedProperty(category = "launcher") 98 @Thunk int mCellHeight; 99 private int mFixedCellWidth; 100 private int mFixedCellHeight; 101 @ViewDebug.ExportedProperty(category = "launcher") 102 protected Point mBorderSpace; 103 104 @ViewDebug.ExportedProperty(category = "launcher") 105 protected int mCountX; 106 @ViewDebug.ExportedProperty(category = "launcher") 107 protected int mCountY; 108 109 private boolean mDropPending = false; 110 111 // These are temporary variables to prevent having to allocate a new object just to 112 // return an (x, y) value from helper functions. Do NOT use them to maintain other state. 113 @Thunk final int[] mTmpPoint = new int[2]; 114 @Thunk final int[] mTempLocation = new int[2]; 115 116 @Thunk final Rect mTempOnDrawCellToRect = new Rect(); 117 118 protected GridOccupancy mOccupied; 119 public GridOccupancy mTmpOccupied; 120 121 private OnTouchListener mInterceptTouchListener; 122 123 private final ArrayList<DelegatedCellDrawing> mDelegatedCellDrawings = new ArrayList<>(); 124 final PreviewBackground mFolderLeaveBehind = new PreviewBackground(getContext()); 125 126 private static final int[] BACKGROUND_STATE_ACTIVE = new int[] { android.R.attr.state_active }; 127 private static final int[] BACKGROUND_STATE_DEFAULT = EMPTY_STATE_SET; 128 protected final Drawable mBackground; 129 130 // These values allow a fixed measurement to be set on the CellLayout. 131 private int mFixedWidth = -1; 132 private int mFixedHeight = -1; 133 134 // If we're actively dragging something over this screen, mIsDragOverlapping is true 135 private boolean mIsDragOverlapping = false; 136 137 // These arrays are used to implement the drag visualization on x-large screens. 138 // They are used as circular arrays, indexed by mDragOutlineCurrent. 139 @Thunk final CellLayoutLayoutParams[] mDragOutlines = new CellLayoutLayoutParams[4]; 140 @Thunk final float[] mDragOutlineAlphas = new float[mDragOutlines.length]; 141 private final InterruptibleInOutAnimator[] mDragOutlineAnims = 142 new InterruptibleInOutAnimator[mDragOutlines.length]; 143 144 // Used as an index into the above 3 arrays; indicates which is the most current value. 145 private int mDragOutlineCurrent = 0; 146 private final Paint mDragOutlinePaint = new Paint(); 147 148 @Thunk final ArrayMap<CellLayoutLayoutParams, Animator> mReorderAnimators = new ArrayMap<>(); 149 @Thunk final ArrayMap<Reorderable, ReorderPreviewAnimation> mShakeAnimators = new ArrayMap<>(); 150 151 private boolean mItemPlacementDirty = false; 152 153 // Used to visualize the grid and drop locations 154 private boolean mVisualizeCells = false; 155 private boolean mVisualizeDropLocation = true; 156 private RectF mVisualizeGridRect = new RectF(); 157 private Paint mVisualizeGridPaint = new Paint(); 158 private int mGridVisualizationRoundingRadius; 159 private float mGridAlpha = 0f; 160 private int mGridColor = 0; 161 protected float mSpringLoadedProgress = 0f; 162 private float mScrollProgress = 0f; 163 164 // When a drag operation is in progress, holds the nearest cell to the touch point 165 private final int[] mDragCell = new int[2]; 166 private final int[] mDragCellSpan = new int[2]; 167 168 private boolean mDragging = false; 169 170 private final TimeInterpolator mEaseOutInterpolator; 171 protected final ShortcutAndWidgetContainer mShortcutsAndWidgets; 172 @Px 173 protected int mSpaceBetweenCellLayoutsPx = 0; 174 175 @Retention(RetentionPolicy.SOURCE) 176 @IntDef({WORKSPACE, HOTSEAT, FOLDER}) 177 public @interface ContainerType{} 178 public static final int WORKSPACE = 0; 179 public static final int HOTSEAT = 1; 180 public static final int FOLDER = 2; 181 182 @ContainerType private final int mContainerType; 183 184 public static final float DEFAULT_SCALE = 1f; 185 186 public static final int MODE_SHOW_REORDER_HINT = 0; 187 public static final int MODE_DRAG_OVER = 1; 188 public static final int MODE_ON_DROP = 2; 189 public static final int MODE_ON_DROP_EXTERNAL = 3; 190 public static final int MODE_ACCEPT_DROP = 4; 191 private static final boolean DESTRUCTIVE_REORDER = false; 192 private static final boolean DEBUG_VISUALIZE_OCCUPIED = false; 193 194 public static final float REORDER_PREVIEW_MAGNITUDE = 0.12f; 195 public static final int REORDER_ANIMATION_DURATION = 150; 196 @Thunk final float mReorderPreviewAnimationMagnitude; 197 198 public final int[] mDirectionVector = new int[2]; 199 200 ItemConfiguration mPreviousSolution = null; 201 202 private final Rect mTempRect = new Rect(); 203 204 private static final Paint sPaint = new Paint(); 205 206 // Related to accessible drag and drop 207 DragAndDropAccessibilityDelegate mTouchHelper; 208 209 CellLayoutContainer mCellLayoutContainer; 210 211 public static final FloatProperty<CellLayout> SPRING_LOADED_PROGRESS = 212 new FloatProperty<CellLayout>("spring_loaded_progress") { 213 @Override 214 public Float get(CellLayout cl) { 215 return cl.getSpringLoadedProgress(); 216 } 217 218 @Override 219 public void setValue(CellLayout cl, float progress) { 220 cl.setSpringLoadedProgress(progress); 221 } 222 }; 223 CellLayout(Context context, CellLayoutContainer container)224 public CellLayout(Context context, CellLayoutContainer container) { 225 this(context, (AttributeSet) null); 226 this.mCellLayoutContainer = container; 227 } 228 CellLayout(Context context, AttributeSet attrs)229 public CellLayout(Context context, AttributeSet attrs) { 230 this(context, attrs, 0); 231 } 232 CellLayout(Context context, AttributeSet attrs, int defStyle)233 public CellLayout(Context context, AttributeSet attrs, int defStyle) { 234 super(context, attrs, defStyle); 235 TypedArray a = context.obtainStyledAttributes(attrs, R.styleable.CellLayout, defStyle, 0); 236 mContainerType = a.getInteger(R.styleable.CellLayout_containerType, WORKSPACE); 237 a.recycle(); 238 239 // A ViewGroup usually does not draw, but CellLayout needs to draw a rectangle to show 240 // the user where a dragged item will land when dropped. 241 setWillNotDraw(false); 242 setClipToPadding(false); 243 setClipChildren(false); 244 mActivity = ActivityContext.lookupContext(context); 245 DeviceProfile deviceProfile = mActivity.getDeviceProfile(); 246 247 resetCellSizeInternal(deviceProfile); 248 249 mCountX = deviceProfile.inv.numColumns; 250 mCountY = deviceProfile.inv.numRows; 251 mOccupied = new GridOccupancy(mCountX, mCountY); 252 mTmpOccupied = new GridOccupancy(mCountX, mCountY); 253 254 mFolderLeaveBehind.mDelegateCellX = -1; 255 mFolderLeaveBehind.mDelegateCellY = -1; 256 257 setAlwaysDrawnWithCacheEnabled(false); 258 259 Resources res = getResources(); 260 261 mBackground = getContext().getDrawable(R.drawable.bg_celllayout); 262 mBackground.setCallback(this); 263 mBackground.setAlpha(0); 264 265 mGridColor = Themes.getAttrColor(getContext(), R.attr.workspaceAccentColor); 266 mGridVisualizationRoundingRadius = 267 res.getDimensionPixelSize(R.dimen.grid_visualization_rounding_radius); 268 mReorderPreviewAnimationMagnitude = (REORDER_PREVIEW_MAGNITUDE * deviceProfile.iconSizePx); 269 270 // Initialize the data structures used for the drag visualization. 271 mEaseOutInterpolator = Interpolators.DECELERATE_QUINT; // Quint ease out 272 mDragCell[0] = mDragCell[1] = -1; 273 mDragCellSpan[0] = mDragCellSpan[1] = -1; 274 for (int i = 0; i < mDragOutlines.length; i++) { 275 mDragOutlines[i] = new CellLayoutLayoutParams(0, 0, 0, 0); 276 } 277 mDragOutlinePaint.setColor(Themes.getAttrColor(context, R.attr.workspaceTextColor)); 278 279 // When dragging things around the home screens, we show a green outline of 280 // where the item will land. The outlines gradually fade out, leaving a trail 281 // behind the drag path. 282 // Set up all the animations that are used to implement this fading. 283 final int duration = res.getInteger(R.integer.config_dragOutlineFadeTime); 284 final float fromAlphaValue = 0; 285 final float toAlphaValue = (float)res.getInteger(R.integer.config_dragOutlineMaxAlpha); 286 287 Arrays.fill(mDragOutlineAlphas, fromAlphaValue); 288 289 for (int i = 0; i < mDragOutlineAnims.length; i++) { 290 final InterruptibleInOutAnimator anim = 291 new InterruptibleInOutAnimator(duration, fromAlphaValue, toAlphaValue); 292 anim.getAnimator().setInterpolator(mEaseOutInterpolator); 293 final int thisIndex = i; 294 anim.getAnimator().addUpdateListener(new AnimatorUpdateListener() { 295 public void onAnimationUpdate(ValueAnimator animation) { 296 // If an animation is started and then stopped very quickly, we can still 297 // get spurious updates we've cleared the tag. Guard against this. 298 mDragOutlineAlphas[thisIndex] = (Float) animation.getAnimatedValue(); 299 CellLayout.this.invalidate(); 300 } 301 }); 302 // The animation holds a reference to the drag outline bitmap as long is it's 303 // running. This way the bitmap can be GCed when the animations are complete. 304 mDragOutlineAnims[i] = anim; 305 } 306 307 mShortcutsAndWidgets = new ShortcutAndWidgetContainer(context, mContainerType); 308 mShortcutsAndWidgets.setCellDimensions(mCellWidth, mCellHeight, mCountX, mCountY, 309 mBorderSpace); 310 addView(mShortcutsAndWidgets); 311 } 312 getCellLayoutContainer()313 public CellLayoutContainer getCellLayoutContainer() { 314 return mCellLayoutContainer; 315 } 316 setCellLayoutContainer(CellLayoutContainer cellLayoutContainer)317 public void setCellLayoutContainer(CellLayoutContainer cellLayoutContainer) { 318 mCellLayoutContainer = cellLayoutContainer; 319 } 320 321 /** 322 * Sets or clears a delegate used for accessible drag and drop 323 */ setDragAndDropAccessibilityDelegate(DragAndDropAccessibilityDelegate delegate)324 public void setDragAndDropAccessibilityDelegate(DragAndDropAccessibilityDelegate delegate) { 325 ViewCompat.setAccessibilityDelegate(this, delegate); 326 327 mTouchHelper = delegate; 328 int accessibilityFlag = mTouchHelper != null 329 ? IMPORTANT_FOR_ACCESSIBILITY_YES : IMPORTANT_FOR_ACCESSIBILITY_NO; 330 setImportantForAccessibility(accessibilityFlag); 331 getShortcutsAndWidgets().setImportantForAccessibility(accessibilityFlag); 332 // ExploreByTouchHelper sets focusability. Clear it when the delegate is cleared. 333 setFocusable(delegate != null); 334 // Invalidate the accessibility hierarchy 335 if (getParent() != null) { 336 getParent().notifySubtreeAccessibilityStateChanged( 337 this, this, AccessibilityEvent.CONTENT_CHANGE_TYPE_SUBTREE); 338 } 339 } 340 341 /** 342 * Returns the currently set accessibility delegate 343 */ getDragAndDropAccessibilityDelegate()344 public DragAndDropAccessibilityDelegate getDragAndDropAccessibilityDelegate() { 345 return mTouchHelper; 346 } 347 348 @Override dispatchHoverEvent(MotionEvent event)349 public boolean dispatchHoverEvent(MotionEvent event) { 350 // Always attempt to dispatch hover events to accessibility first. 351 if (mTouchHelper != null && mTouchHelper.dispatchHoverEvent(event)) { 352 return true; 353 } 354 return super.dispatchHoverEvent(event); 355 } 356 357 @Override onInterceptTouchEvent(MotionEvent ev)358 public boolean onInterceptTouchEvent(MotionEvent ev) { 359 return mTouchHelper != null 360 || (mInterceptTouchListener != null && mInterceptTouchListener.onTouch(this, ev)); 361 } 362 enableHardwareLayer(boolean hasLayer)363 public void enableHardwareLayer(boolean hasLayer) { 364 mShortcutsAndWidgets.setLayerType(hasLayer ? LAYER_TYPE_HARDWARE : LAYER_TYPE_NONE, sPaint); 365 } 366 isHardwareLayerEnabled()367 public boolean isHardwareLayerEnabled() { 368 return mShortcutsAndWidgets.getLayerType() == LAYER_TYPE_HARDWARE; 369 } 370 371 /** 372 * Change sizes of cells 373 * 374 * @param width the new width of the cells 375 * @param height the new height of the cells 376 */ setCellDimensions(int width, int height)377 public void setCellDimensions(int width, int height) { 378 mFixedCellWidth = mCellWidth = width; 379 mFixedCellHeight = mCellHeight = height; 380 mShortcutsAndWidgets.setCellDimensions(mCellWidth, mCellHeight, mCountX, mCountY, 381 mBorderSpace); 382 } 383 resetCellSizeInternal(DeviceProfile deviceProfile)384 private void resetCellSizeInternal(DeviceProfile deviceProfile) { 385 switch (mContainerType) { 386 case FOLDER: 387 mBorderSpace = new Point(deviceProfile.folderCellLayoutBorderSpacePx); 388 break; 389 case HOTSEAT: 390 mBorderSpace = new Point(deviceProfile.hotseatBorderSpace, 391 deviceProfile.hotseatBorderSpace); 392 break; 393 case WORKSPACE: 394 default: 395 mBorderSpace = new Point(deviceProfile.cellLayoutBorderSpacePx); 396 break; 397 } 398 399 mCellWidth = mCellHeight = -1; 400 mFixedCellWidth = mFixedCellHeight = -1; 401 } 402 403 /** 404 * Reset the cell sizes and border space 405 */ resetCellSize(DeviceProfile deviceProfile)406 public void resetCellSize(DeviceProfile deviceProfile) { 407 resetCellSizeInternal(deviceProfile); 408 requestLayout(); 409 } 410 setGridSize(int x, int y)411 public void setGridSize(int x, int y) { 412 mCountX = x; 413 mCountY = y; 414 mOccupied = new GridOccupancy(mCountX, mCountY); 415 mTmpOccupied = new GridOccupancy(mCountX, mCountY); 416 mShortcutsAndWidgets.setCellDimensions(mCellWidth, mCellHeight, mCountX, mCountY, 417 mBorderSpace); 418 requestLayout(); 419 } 420 421 // Set whether or not to invert the layout horizontally if the layout is in RTL mode. setInvertIfRtl(boolean invert)422 public void setInvertIfRtl(boolean invert) { 423 mShortcutsAndWidgets.setInvertIfRtl(invert); 424 } 425 setDropPending(boolean pending)426 public void setDropPending(boolean pending) { 427 mDropPending = pending; 428 } 429 isDropPending()430 public boolean isDropPending() { 431 return mDropPending; 432 } 433 setIsDragOverlapping(boolean isDragOverlapping)434 void setIsDragOverlapping(boolean isDragOverlapping) { 435 if (mIsDragOverlapping != isDragOverlapping) { 436 mIsDragOverlapping = isDragOverlapping; 437 mBackground.setState(mIsDragOverlapping 438 ? BACKGROUND_STATE_ACTIVE : BACKGROUND_STATE_DEFAULT); 439 invalidate(); 440 } 441 } 442 443 @Override dispatchSaveInstanceState(SparseArray<Parcelable> container)444 protected void dispatchSaveInstanceState(SparseArray<Parcelable> container) { 445 ParcelableSparseArray jail = getJailedArray(container); 446 super.dispatchSaveInstanceState(jail); 447 container.put(R.id.cell_layout_jail_id, jail); 448 } 449 450 @Override dispatchRestoreInstanceState(SparseArray<Parcelable> container)451 protected void dispatchRestoreInstanceState(SparseArray<Parcelable> container) { 452 super.dispatchRestoreInstanceState(getJailedArray(container)); 453 } 454 455 /** 456 * Wrap the SparseArray in another Parcelable so that the item ids do not conflict with our 457 * our internal resource ids 458 */ getJailedArray(SparseArray<Parcelable> container)459 private ParcelableSparseArray getJailedArray(SparseArray<Parcelable> container) { 460 final Parcelable parcelable = container.get(R.id.cell_layout_jail_id); 461 return parcelable instanceof ParcelableSparseArray ? 462 (ParcelableSparseArray) parcelable : new ParcelableSparseArray(); 463 } 464 getIsDragOverlapping()465 public boolean getIsDragOverlapping() { 466 return mIsDragOverlapping; 467 } 468 469 @Override onDraw(Canvas canvas)470 protected void onDraw(Canvas canvas) { 471 // When we're large, we are either drawn in a "hover" state (ie when dragging an item to 472 // a neighboring page) or with just a normal background (if backgroundAlpha > 0.0f) 473 // When we're small, we are either drawn normally or in the "accepts drops" state (during 474 // a drag). However, we also drag the mini hover background *over* one of those two 475 // backgrounds 476 if (mBackground.getAlpha() > 0) { 477 mBackground.draw(canvas); 478 } 479 480 if (DEBUG_VISUALIZE_OCCUPIED) { 481 Rect cellBounds = new Rect(); 482 // Will contain the bounds of the cell including spacing between cells. 483 Rect cellBoundsWithSpacing = new Rect(); 484 int[] targetCell = new int[2]; 485 int[] cellCenter = new int[2]; 486 Paint debugPaint = new Paint(); 487 debugPaint.setStrokeWidth(Utilities.dpToPx(1)); 488 for (int x = 0; x < mCountX; x++) { 489 for (int y = 0; y < mCountY; y++) { 490 if (!mOccupied.cells[x][y]) { 491 continue; 492 } 493 targetCell[0] = x; 494 targetCell[1] = y; 495 496 boolean canCreateFolder = canCreateFolder(getChildAt(x, y)); 497 cellToRect(x, y, 1, 1, cellBounds); 498 cellBoundsWithSpacing.set(cellBounds); 499 cellBoundsWithSpacing.inset(-mBorderSpace.x / 2, -mBorderSpace.y / 2); 500 getWorkspaceCellVisualCenter(x, y, cellCenter); 501 502 canvas.save(); 503 canvas.clipRect(cellBoundsWithSpacing); 504 505 // Draw reorder drag target. 506 debugPaint.setColor(Color.RED); 507 canvas.drawCircle(cellCenter[0], cellCenter[1], 508 getReorderRadius(targetCell, 1, 1), debugPaint); 509 510 // Draw folder creation drag target. 511 if (canCreateFolder) { 512 debugPaint.setColor(Color.GREEN); 513 canvas.drawCircle(cellCenter[0], cellCenter[1], 514 getFolderCreationRadius(targetCell), debugPaint); 515 } 516 517 canvas.restore(); 518 } 519 } 520 } 521 522 for (int i = 0; i < mDelegatedCellDrawings.size(); i++) { 523 DelegatedCellDrawing cellDrawing = mDelegatedCellDrawings.get(i); 524 cellToPoint(cellDrawing.mDelegateCellX, cellDrawing.mDelegateCellY, mTempLocation); 525 canvas.save(); 526 canvas.translate(mTempLocation[0], mTempLocation[1]); 527 cellDrawing.drawUnderItem(canvas); 528 canvas.restore(); 529 } 530 531 if (mFolderLeaveBehind.mDelegateCellX >= 0 && mFolderLeaveBehind.mDelegateCellY >= 0) { 532 cellToPoint(mFolderLeaveBehind.mDelegateCellX, 533 mFolderLeaveBehind.mDelegateCellY, mTempLocation); 534 canvas.save(); 535 canvas.translate(mTempLocation[0], mTempLocation[1]); 536 mFolderLeaveBehind.drawLeaveBehind(canvas, FOLDER_LEAVE_BEHIND_COLOR); 537 canvas.restore(); 538 } 539 540 if (mVisualizeCells || mVisualizeDropLocation) { 541 visualizeGrid(canvas); 542 } 543 } 544 545 /** 546 * Returns whether dropping an icon on the given View can create (or add to) a folder. 547 */ canCreateFolder(View child)548 private boolean canCreateFolder(View child) { 549 return child instanceof DraggableView 550 && ((DraggableView) child).getViewType() == DRAGGABLE_ICON; 551 } 552 553 /** 554 * Indicates the progress of the Workspace entering the SpringLoaded state; allows the 555 * CellLayout to update various visuals for this state. 556 * 557 * @param progress 558 */ setSpringLoadedProgress(float progress)559 public void setSpringLoadedProgress(float progress) { 560 if (Float.compare(progress, mSpringLoadedProgress) != 0) { 561 mSpringLoadedProgress = progress; 562 updateBgAlpha(); 563 setGridAlpha(progress); 564 } 565 } 566 567 /** 568 * See setSpringLoadedProgress 569 * @return progress 570 */ getSpringLoadedProgress()571 public float getSpringLoadedProgress() { 572 return mSpringLoadedProgress; 573 } 574 updateBgAlpha()575 protected void updateBgAlpha() { 576 mBackground.setAlpha((int) (mSpringLoadedProgress * 255)); 577 } 578 579 /** 580 * Set the progress of this page's scroll 581 * 582 * @param progress 0 if the screen is centered, +/-1 if it is to the right / left respectively 583 */ setScrollProgress(float progress)584 public void setScrollProgress(float progress) { 585 if (Float.compare(Math.abs(progress), mScrollProgress) != 0) { 586 mScrollProgress = Math.abs(progress); 587 updateBgAlpha(); 588 } 589 } 590 setGridAlpha(float gridAlpha)591 private void setGridAlpha(float gridAlpha) { 592 if (Float.compare(gridAlpha, mGridAlpha) != 0) { 593 mGridAlpha = gridAlpha; 594 invalidate(); 595 } 596 } 597 visualizeGrid(Canvas canvas)598 protected void visualizeGrid(Canvas canvas) { 599 DeviceProfile dp = mActivity.getDeviceProfile(); 600 int paddingX = Math.min((mCellWidth - dp.iconSizePx) / 2, dp.gridVisualizationPaddingX); 601 int paddingY = Math.min((mCellHeight - dp.iconSizePx) / 2, dp.gridVisualizationPaddingY); 602 603 mVisualizeGridPaint.setStrokeWidth(8); 604 605 // This is used for debugging purposes only 606 if (mVisualizeCells) { 607 int paintAlpha = (int) (120 * mGridAlpha); 608 mVisualizeGridPaint.setColor(ColorUtils.setAlphaComponent(mGridColor, paintAlpha)); 609 for (int i = 0; i < mCountX; i++) { 610 for (int j = 0; j < mCountY; j++) { 611 cellToRect(i, j, 1, 1, mTempOnDrawCellToRect); 612 mVisualizeGridRect.set(mTempOnDrawCellToRect); 613 mVisualizeGridRect.inset(paddingX, paddingY); 614 mVisualizeGridPaint.setStyle(Paint.Style.FILL); 615 canvas.drawRoundRect(mVisualizeGridRect, mGridVisualizationRoundingRadius, 616 mGridVisualizationRoundingRadius, mVisualizeGridPaint); 617 } 618 } 619 } 620 621 if (mVisualizeDropLocation) { 622 for (int i = 0; i < mDragOutlines.length; i++) { 623 final float alpha = mDragOutlineAlphas[i]; 624 if (alpha <= 0) continue; 625 CellLayoutLayoutParams params = mDragOutlines[i]; 626 cellToRect(params.getCellX(), params.getCellY(), params.cellHSpan, params.cellVSpan, 627 mTempOnDrawCellToRect); 628 mVisualizeGridRect.set(mTempOnDrawCellToRect); 629 mVisualizeGridRect.inset(paddingX, paddingY); 630 631 mVisualizeGridPaint.setAlpha(255); 632 mVisualizeGridPaint.setStyle(Paint.Style.STROKE); 633 mVisualizeGridPaint.setColor(Color.argb((int) (alpha), 634 Color.red(mGridColor), Color.green(mGridColor), Color.blue(mGridColor))); 635 636 canvas.save(); 637 canvas.translate(getMarginForGivenCellParams(params), 0); 638 canvas.drawRoundRect(mVisualizeGridRect, mGridVisualizationRoundingRadius, 639 mGridVisualizationRoundingRadius, mVisualizeGridPaint); 640 canvas.restore(); 641 } 642 } 643 } 644 getMarginForGivenCellParams(CellLayoutLayoutParams params)645 protected float getMarginForGivenCellParams(CellLayoutLayoutParams params) { 646 return 0; 647 } 648 649 @Override dispatchDraw(Canvas canvas)650 protected void dispatchDraw(Canvas canvas) { 651 super.dispatchDraw(canvas); 652 653 for (int i = 0; i < mDelegatedCellDrawings.size(); i++) { 654 DelegatedCellDrawing bg = mDelegatedCellDrawings.get(i); 655 cellToPoint(bg.mDelegateCellX, bg.mDelegateCellY, mTempLocation); 656 canvas.save(); 657 canvas.translate(mTempLocation[0], mTempLocation[1]); 658 bg.drawOverItem(canvas); 659 canvas.restore(); 660 } 661 } 662 663 /** 664 * Add Delegated cell drawing 665 */ addDelegatedCellDrawing(DelegatedCellDrawing bg)666 public void addDelegatedCellDrawing(DelegatedCellDrawing bg) { 667 mDelegatedCellDrawings.add(bg); 668 } 669 670 /** 671 * Remove item from DelegatedCellDrawings 672 */ removeDelegatedCellDrawing(DelegatedCellDrawing bg)673 public void removeDelegatedCellDrawing(DelegatedCellDrawing bg) { 674 mDelegatedCellDrawings.remove(bg); 675 } 676 setFolderLeaveBehindCell(int x, int y)677 public void setFolderLeaveBehindCell(int x, int y) { 678 View child = getChildAt(x, y); 679 mFolderLeaveBehind.setup(getContext(), mActivity, null, 680 child.getMeasuredWidth(), child.getPaddingTop()); 681 682 mFolderLeaveBehind.mDelegateCellX = x; 683 mFolderLeaveBehind.mDelegateCellY = y; 684 invalidate(); 685 } 686 clearFolderLeaveBehind()687 public void clearFolderLeaveBehind() { 688 mFolderLeaveBehind.mDelegateCellX = -1; 689 mFolderLeaveBehind.mDelegateCellY = -1; 690 invalidate(); 691 } 692 693 @Override shouldDelayChildPressedState()694 public boolean shouldDelayChildPressedState() { 695 return false; 696 } 697 restoreInstanceState(SparseArray<Parcelable> states)698 public void restoreInstanceState(SparseArray<Parcelable> states) { 699 try { 700 dispatchRestoreInstanceState(states); 701 } catch (IllegalArgumentException ex) { 702 if (FeatureFlags.IS_STUDIO_BUILD) { 703 throw ex; 704 } 705 // Mismatched viewId / viewType preventing restore. Skip restore on production builds. 706 Log.e(TAG, "Ignoring an error while restoring a view instance state", ex); 707 } 708 } 709 710 @Override cancelLongPress()711 public void cancelLongPress() { 712 super.cancelLongPress(); 713 714 // Cancel long press for all children 715 final int count = getChildCount(); 716 for (int i = 0; i < count; i++) { 717 final View child = getChildAt(i); 718 child.cancelLongPress(); 719 } 720 } 721 setOnInterceptTouchListener(View.OnTouchListener listener)722 public void setOnInterceptTouchListener(View.OnTouchListener listener) { 723 mInterceptTouchListener = listener; 724 } 725 getCountX()726 public int getCountX() { 727 return mCountX; 728 } 729 getCountY()730 public int getCountY() { 731 return mCountY; 732 } 733 acceptsWidget()734 public boolean acceptsWidget() { 735 return mContainerType == WORKSPACE; 736 } 737 738 /** 739 * Adds the given view to the CellLayout 740 * 741 * @param child view to add. 742 * @param index index of the CellLayout children where to add the view. 743 * @param childId id of the view. 744 * @param params represent the logic of the view on the CellLayout. 745 * @param markCells if the occupied cells should be marked or not 746 * @return if adding the view was successful 747 */ addViewToCellLayout(View child, int index, int childId, CellLayoutLayoutParams params, boolean markCells)748 public boolean addViewToCellLayout(View child, int index, int childId, 749 CellLayoutLayoutParams params, boolean markCells) { 750 final CellLayoutLayoutParams lp = params; 751 752 // Hotseat icons - remove text 753 if (child instanceof BubbleTextView) { 754 BubbleTextView bubbleChild = (BubbleTextView) child; 755 bubbleChild.setTextVisibility(mContainerType != HOTSEAT); 756 } 757 758 child.setScaleX(DEFAULT_SCALE); 759 child.setScaleY(DEFAULT_SCALE); 760 761 // Generate an id for each view, this assumes we have at most 256x256 cells 762 // per workspace screen 763 if (lp.getCellX() >= 0 && lp.getCellX() <= mCountX - 1 764 && lp.getCellY() >= 0 && lp.getCellY() <= mCountY - 1) { 765 // If the horizontal or vertical span is set to -1, it is taken to 766 // mean that it spans the extent of the CellLayout 767 if (lp.cellHSpan < 0) lp.cellHSpan = mCountX; 768 if (lp.cellVSpan < 0) lp.cellVSpan = mCountY; 769 770 child.setId(childId); 771 if (LOGD) { 772 Log.d(TAG, "Adding view to ShortcutsAndWidgetsContainer: " + child); 773 } 774 mShortcutsAndWidgets.addView(child, index, lp); 775 776 if (markCells) markCellsAsOccupiedForView(child); 777 778 return true; 779 } 780 return false; 781 } 782 783 @Override removeAllViews()784 public void removeAllViews() { 785 mOccupied.clear(); 786 mShortcutsAndWidgets.removeAllViews(); 787 } 788 789 @Override removeAllViewsInLayout()790 public void removeAllViewsInLayout() { 791 if (mShortcutsAndWidgets.getChildCount() > 0) { 792 mOccupied.clear(); 793 mShortcutsAndWidgets.removeAllViewsInLayout(); 794 } 795 } 796 797 @Override removeView(View view)798 public void removeView(View view) { 799 markCellsAsUnoccupiedForView(view); 800 mShortcutsAndWidgets.removeView(view); 801 } 802 803 @Override removeViewAt(int index)804 public void removeViewAt(int index) { 805 markCellsAsUnoccupiedForView(mShortcutsAndWidgets.getChildAt(index)); 806 mShortcutsAndWidgets.removeViewAt(index); 807 } 808 809 @Override removeViewInLayout(View view)810 public void removeViewInLayout(View view) { 811 markCellsAsUnoccupiedForView(view); 812 mShortcutsAndWidgets.removeViewInLayout(view); 813 } 814 815 @Override removeViews(int start, int count)816 public void removeViews(int start, int count) { 817 for (int i = start; i < start + count; i++) { 818 markCellsAsUnoccupiedForView(mShortcutsAndWidgets.getChildAt(i)); 819 } 820 mShortcutsAndWidgets.removeViews(start, count); 821 } 822 823 @Override removeViewsInLayout(int start, int count)824 public void removeViewsInLayout(int start, int count) { 825 for (int i = start; i < start + count; i++) { 826 markCellsAsUnoccupiedForView(mShortcutsAndWidgets.getChildAt(i)); 827 } 828 mShortcutsAndWidgets.removeViewsInLayout(start, count); 829 } 830 831 /** 832 * Given a point, return the cell that strictly encloses that point 833 * @param x X coordinate of the point 834 * @param y Y coordinate of the point 835 * @param result Array of 2 ints to hold the x and y coordinate of the cell 836 */ pointToCellExact(int x, int y, int[] result)837 public void pointToCellExact(int x, int y, int[] result) { 838 final int hStartPadding = getPaddingLeft(); 839 final int vStartPadding = getPaddingTop(); 840 841 result[0] = (x - hStartPadding) / (mCellWidth + mBorderSpace.x); 842 result[1] = (y - vStartPadding) / (mCellHeight + mBorderSpace.y); 843 844 final int xAxis = mCountX; 845 final int yAxis = mCountY; 846 847 if (result[0] < 0) result[0] = 0; 848 if (result[0] >= xAxis) result[0] = xAxis - 1; 849 if (result[1] < 0) result[1] = 0; 850 if (result[1] >= yAxis) result[1] = yAxis - 1; 851 } 852 853 /** 854 * Given a cell coordinate, return the point that represents the upper left corner of that cell 855 * 856 * @param cellX X coordinate of the cell 857 * @param cellY Y coordinate of the cell 858 * 859 * @param result Array of 2 ints to hold the x and y coordinate of the point 860 */ cellToPoint(int cellX, int cellY, int[] result)861 void cellToPoint(int cellX, int cellY, int[] result) { 862 cellToRect(cellX, cellY, 1, 1, mTempRect); 863 result[0] = mTempRect.left; 864 result[1] = mTempRect.top; 865 } 866 867 /** 868 * Given a cell coordinate, return the point that represents the center of the cell 869 * 870 * @param cellX X coordinate of the cell 871 * @param cellY Y coordinate of the cell 872 * 873 * @param result Array of 2 ints to hold the x and y coordinate of the point 874 */ cellToCenterPoint(int cellX, int cellY, int[] result)875 void cellToCenterPoint(int cellX, int cellY, int[] result) { 876 regionToCenterPoint(cellX, cellY, 1, 1, result); 877 } 878 879 /** 880 * Given a cell coordinate and span return the point that represents the center of the region 881 * 882 * @param cellX X coordinate of the cell 883 * @param cellY Y coordinate of the cell 884 * 885 * @param result Array of 2 ints to hold the x and y coordinate of the point 886 */ regionToCenterPoint(int cellX, int cellY, int spanX, int spanY, int[] result)887 public void regionToCenterPoint(int cellX, int cellY, int spanX, int spanY, int[] result) { 888 cellToRect(cellX, cellY, spanX, spanY, mTempRect); 889 result[0] = mTempRect.centerX(); 890 result[1] = mTempRect.centerY(); 891 } 892 893 /** 894 * Returns the distance between the given coordinate and the visual center of the given cell. 895 */ getDistanceFromWorkspaceCellVisualCenter(float x, float y, int[] cell)896 public float getDistanceFromWorkspaceCellVisualCenter(float x, float y, int[] cell) { 897 getWorkspaceCellVisualCenter(cell[0], cell[1], mTmpPoint); 898 return (float) Math.hypot(x - mTmpPoint[0], y - mTmpPoint[1]); 899 } 900 getWorkspaceCellVisualCenter(int cellX, int cellY, int[] outPoint)901 private void getWorkspaceCellVisualCenter(int cellX, int cellY, int[] outPoint) { 902 View child = getChildAt(cellX, cellY); 903 if (child instanceof DraggableView) { 904 DraggableView draggableChild = (DraggableView) child; 905 if (draggableChild.getViewType() == DRAGGABLE_ICON) { 906 cellToPoint(cellX, cellY, outPoint); 907 draggableChild.getWorkspaceVisualDragBounds(mTempRect); 908 mTempRect.offset(outPoint[0], outPoint[1]); 909 outPoint[0] = mTempRect.centerX(); 910 outPoint[1] = mTempRect.centerY(); 911 return; 912 } 913 } 914 cellToCenterPoint(cellX, cellY, outPoint); 915 } 916 917 /** 918 * Returns the max distance from the center of a cell that can accept a drop to create a folder. 919 */ getFolderCreationRadius(int[] targetCell)920 public float getFolderCreationRadius(int[] targetCell) { 921 DeviceProfile grid = mActivity.getDeviceProfile(); 922 float iconVisibleRadius = ICON_VISIBLE_AREA_FACTOR * grid.iconSizePx / 2; 923 // Halfway between reorder radius and icon. 924 return (getReorderRadius(targetCell, 1, 1) + iconVisibleRadius) / 2; 925 } 926 927 /** 928 * Returns the max distance from the center of a cell that will start to reorder on drag over. 929 */ getReorderRadius(int[] targetCell, int spanX, int spanY)930 public float getReorderRadius(int[] targetCell, int spanX, int spanY) { 931 int[] centerPoint = mTmpPoint; 932 getWorkspaceCellVisualCenter(targetCell[0], targetCell[1], centerPoint); 933 934 Rect cellBoundsWithSpacing = mTempRect; 935 cellToRect(targetCell[0], targetCell[1], spanX, spanY, cellBoundsWithSpacing); 936 cellBoundsWithSpacing.inset(-mBorderSpace.x / 2, -mBorderSpace.y / 2); 937 938 if (canCreateFolder(getChildAt(targetCell[0], targetCell[1])) && spanX == 1 && spanY == 1) { 939 // Take only the circle in the smaller dimension, to ensure we don't start reordering 940 // too soon before accepting a folder drop. 941 int minRadius = centerPoint[0] - cellBoundsWithSpacing.left; 942 minRadius = Math.min(minRadius, centerPoint[1] - cellBoundsWithSpacing.top); 943 minRadius = Math.min(minRadius, cellBoundsWithSpacing.right - centerPoint[0]); 944 minRadius = Math.min(minRadius, cellBoundsWithSpacing.bottom - centerPoint[1]); 945 return minRadius; 946 } 947 // Take up the entire cell, including space between this cell and the adjacent ones. 948 // Multiply by span to scale radius 949 return (float) Math.hypot(spanX * cellBoundsWithSpacing.width() / 2f, 950 spanY * cellBoundsWithSpacing.height() / 2f); 951 } 952 getCellWidth()953 public int getCellWidth() { 954 return mCellWidth; 955 } 956 getCellHeight()957 public int getCellHeight() { 958 return mCellHeight; 959 } 960 setFixedSize(int width, int height)961 public void setFixedSize(int width, int height) { 962 mFixedWidth = width; 963 mFixedHeight = height; 964 } 965 966 @Override onMeasure(int widthMeasureSpec, int heightMeasureSpec)967 protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { 968 int widthSpecMode = MeasureSpec.getMode(widthMeasureSpec); 969 int heightSpecMode = MeasureSpec.getMode(heightMeasureSpec); 970 int widthSize = MeasureSpec.getSize(widthMeasureSpec); 971 int heightSize = MeasureSpec.getSize(heightMeasureSpec); 972 int childWidthSize = widthSize - (getPaddingLeft() + getPaddingRight()); 973 int childHeightSize = heightSize - (getPaddingTop() + getPaddingBottom()); 974 975 if (mFixedCellWidth < 0 || mFixedCellHeight < 0) { 976 int cw = DeviceProfile.calculateCellWidth(childWidthSize, mBorderSpace.x, 977 mCountX); 978 int ch = DeviceProfile.calculateCellHeight(childHeightSize, mBorderSpace.y, 979 mCountY); 980 if (cw != mCellWidth || ch != mCellHeight) { 981 mCellWidth = cw; 982 mCellHeight = ch; 983 mShortcutsAndWidgets.setCellDimensions(mCellWidth, mCellHeight, mCountX, mCountY, 984 mBorderSpace); 985 } 986 } 987 988 int newWidth = childWidthSize; 989 int newHeight = childHeightSize; 990 if (mFixedWidth > 0 && mFixedHeight > 0) { 991 newWidth = mFixedWidth; 992 newHeight = mFixedHeight; 993 } else if (widthSpecMode == MeasureSpec.UNSPECIFIED || heightSpecMode == MeasureSpec.UNSPECIFIED) { 994 throw new RuntimeException("CellLayout cannot have UNSPECIFIED dimensions"); 995 } 996 997 mShortcutsAndWidgets.measure( 998 MeasureSpec.makeMeasureSpec(newWidth, MeasureSpec.EXACTLY), 999 MeasureSpec.makeMeasureSpec(newHeight, MeasureSpec.EXACTLY)); 1000 1001 int maxWidth = mShortcutsAndWidgets.getMeasuredWidth(); 1002 int maxHeight = mShortcutsAndWidgets.getMeasuredHeight(); 1003 if (mFixedWidth > 0 && mFixedHeight > 0) { 1004 setMeasuredDimension(maxWidth, maxHeight); 1005 } else { 1006 setMeasuredDimension(widthSize, heightSize); 1007 } 1008 } 1009 1010 @Override onLayout(boolean changed, int l, int t, int r, int b)1011 protected void onLayout(boolean changed, int l, int t, int r, int b) { 1012 int left = getPaddingLeft(); 1013 left += (int) Math.ceil(getUnusedHorizontalSpace() / 2f); 1014 int right = r - l - getPaddingRight(); 1015 right -= (int) Math.ceil(getUnusedHorizontalSpace() / 2f); 1016 1017 int top = getPaddingTop(); 1018 int bottom = b - t - getPaddingBottom(); 1019 1020 // Expand the background drawing bounds by the padding baked into the background drawable 1021 mBackground.getPadding(mTempRect); 1022 mBackground.setBounds( 1023 left - mTempRect.left - getPaddingLeft(), 1024 top - mTempRect.top - getPaddingTop(), 1025 right + mTempRect.right + getPaddingRight(), 1026 bottom + mTempRect.bottom + getPaddingBottom()); 1027 1028 mShortcutsAndWidgets.layout(left, top, right, bottom); 1029 } 1030 1031 /** 1032 * Returns the amount of space left over after subtracting padding and cells. This space will be 1033 * very small, a few pixels at most, and is a result of rounding down when calculating the cell 1034 * width in {@link DeviceProfile#calculateCellWidth(int, int, int)}. 1035 */ getUnusedHorizontalSpace()1036 public int getUnusedHorizontalSpace() { 1037 return getMeasuredWidth() - getPaddingLeft() - getPaddingRight() - (mCountX * mCellWidth) 1038 - ((mCountX - 1) * mBorderSpace.x); 1039 } 1040 1041 @Override verifyDrawable(Drawable who)1042 protected boolean verifyDrawable(Drawable who) { 1043 return super.verifyDrawable(who) || (who == mBackground); 1044 } 1045 getShortcutsAndWidgets()1046 public ShortcutAndWidgetContainer getShortcutsAndWidgets() { 1047 return mShortcutsAndWidgets; 1048 } 1049 getChildAt(int cellX, int cellY)1050 public View getChildAt(int cellX, int cellY) { 1051 return mShortcutsAndWidgets.getChildAt(cellX, cellY); 1052 } 1053 animateChildToPosition(final View child, int cellX, int cellY, int duration, int delay, boolean permanent, boolean adjustOccupied)1054 public boolean animateChildToPosition(final View child, int cellX, int cellY, int duration, 1055 int delay, boolean permanent, boolean adjustOccupied) { 1056 ShortcutAndWidgetContainer clc = getShortcutsAndWidgets(); 1057 1058 if (clc.indexOfChild(child) != -1 && (child instanceof Reorderable)) { 1059 final CellLayoutLayoutParams lp = (CellLayoutLayoutParams) child.getLayoutParams(); 1060 final ItemInfo info = (ItemInfo) child.getTag(); 1061 final Reorderable item = (Reorderable) child; 1062 1063 // We cancel any existing animations 1064 if (mReorderAnimators.containsKey(lp)) { 1065 mReorderAnimators.get(lp).cancel(); 1066 mReorderAnimators.remove(lp); 1067 } 1068 1069 1070 if (adjustOccupied) { 1071 GridOccupancy occupied = permanent ? mOccupied : mTmpOccupied; 1072 occupied.markCells(lp.getCellX(), lp.getCellY(), lp.cellHSpan, lp.cellVSpan, false); 1073 occupied.markCells(cellX, cellY, lp.cellHSpan, lp.cellVSpan, true); 1074 } 1075 1076 // Compute the new x and y position based on the new cellX and cellY 1077 // We leverage the actual layout logic in the layout params and hence need to modify 1078 // state and revert that state. 1079 final int oldX = lp.x; 1080 final int oldY = lp.y; 1081 lp.isLockedToGrid = true; 1082 if (permanent) { 1083 lp.setCellX(cellX); 1084 lp.setCellY(cellY); 1085 } else { 1086 lp.setTmpCellX(cellX); 1087 lp.setTmpCellY(cellY); 1088 } 1089 clc.setupLp(child); 1090 final int newX = lp.x; 1091 final int newY = lp.y; 1092 lp.x = oldX; 1093 lp.y = oldY; 1094 lp.isLockedToGrid = false; 1095 // End compute new x and y 1096 1097 MultiTranslateDelegate mtd = item.getTranslateDelegate(); 1098 float initPreviewOffsetX = mtd.getTranslationX(INDEX_REORDER_PREVIEW_OFFSET).getValue(); 1099 float initPreviewOffsetY = mtd.getTranslationY(INDEX_REORDER_PREVIEW_OFFSET).getValue(); 1100 final float finalPreviewOffsetX = newX - oldX; 1101 final float finalPreviewOffsetY = newY - oldY; 1102 1103 // Exit early if we're not actually moving the view 1104 if (finalPreviewOffsetX == 0 && finalPreviewOffsetY == 0 1105 && initPreviewOffsetX == 0 && initPreviewOffsetY == 0) { 1106 lp.isLockedToGrid = true; 1107 return true; 1108 } 1109 1110 ValueAnimator va = ValueAnimator.ofFloat(0f, 1f); 1111 va.setDuration(duration); 1112 mReorderAnimators.put(lp, va); 1113 1114 va.addUpdateListener(new AnimatorUpdateListener() { 1115 @Override 1116 public void onAnimationUpdate(ValueAnimator animation) { 1117 float r = (Float) animation.getAnimatedValue(); 1118 float x = (1 - r) * initPreviewOffsetX + r * finalPreviewOffsetX; 1119 float y = (1 - r) * initPreviewOffsetY + r * finalPreviewOffsetY; 1120 item.getTranslateDelegate().setTranslation(INDEX_REORDER_PREVIEW_OFFSET, x, y); 1121 } 1122 }); 1123 va.addListener(new AnimatorListenerAdapter() { 1124 boolean cancelled = false; 1125 public void onAnimationEnd(Animator animation) { 1126 // If the animation was cancelled, it means that another animation 1127 // has interrupted this one, and we don't want to lock the item into 1128 // place just yet. 1129 if (!cancelled) { 1130 lp.isLockedToGrid = true; 1131 item.getTranslateDelegate() 1132 .setTranslation(INDEX_REORDER_PREVIEW_OFFSET, 0, 0); 1133 child.requestLayout(); 1134 } 1135 if (mReorderAnimators.containsKey(lp)) { 1136 mReorderAnimators.remove(lp); 1137 } 1138 } 1139 public void onAnimationCancel(Animator animation) { 1140 cancelled = true; 1141 } 1142 }); 1143 va.setStartDelay(delay); 1144 va.start(); 1145 return true; 1146 } 1147 return false; 1148 } 1149 visualizeDropLocation(int cellX, int cellY, int spanX, int spanY, DropTarget.DragObject dragObject)1150 void visualizeDropLocation(int cellX, int cellY, int spanX, int spanY, 1151 DropTarget.DragObject dragObject) { 1152 if (mDragCell[0] != cellX || mDragCell[1] != cellY || mDragCellSpan[0] != spanX 1153 || mDragCellSpan[1] != spanY) { 1154 mDragCell[0] = cellX; 1155 mDragCell[1] = cellY; 1156 mDragCellSpan[0] = spanX; 1157 mDragCellSpan[1] = spanY; 1158 1159 final int oldIndex = mDragOutlineCurrent; 1160 mDragOutlineAnims[oldIndex].animateOut(); 1161 mDragOutlineCurrent = (oldIndex + 1) % mDragOutlines.length; 1162 1163 CellLayoutLayoutParams cell = mDragOutlines[mDragOutlineCurrent]; 1164 cell.setCellX(cellX); 1165 cell.setCellY(cellY); 1166 cell.cellHSpan = spanX; 1167 cell.cellVSpan = spanY; 1168 1169 mDragOutlineAnims[mDragOutlineCurrent].animateIn(); 1170 invalidate(); 1171 1172 if (dragObject.stateAnnouncer != null) { 1173 dragObject.stateAnnouncer.announce(getItemMoveDescription(cellX, cellY)); 1174 } 1175 1176 } 1177 } 1178 1179 @SuppressLint("StringFormatMatches") getItemMoveDescription(int cellX, int cellY)1180 public String getItemMoveDescription(int cellX, int cellY) { 1181 if (mContainerType == HOTSEAT) { 1182 return getContext().getString(R.string.move_to_hotseat_position, 1183 Math.max(cellX, cellY) + 1); 1184 } else { 1185 int row = cellY + 1; 1186 int col = Utilities.isRtl(getResources()) ? mCountX - cellX : cellX + 1; 1187 int panelCount = mCellLayoutContainer.getPanelCount(); 1188 int pageIndex = mCellLayoutContainer.getCellLayoutIndex(this); 1189 if (panelCount > 1) { 1190 // Increment the column if the target is on the right side of a two panel home 1191 col += (pageIndex % panelCount) * mCountX; 1192 } 1193 return getContext().getString(R.string.move_to_empty_cell_description, row, col, 1194 mCellLayoutContainer.getPageDescription(pageIndex)); 1195 } 1196 } 1197 clearDragOutlines()1198 public void clearDragOutlines() { 1199 final int oldIndex = mDragOutlineCurrent; 1200 mDragOutlineAnims[oldIndex].animateOut(); 1201 mDragCell[0] = mDragCell[1] = -1; 1202 } 1203 1204 /** 1205 * Find a vacant area that will fit the given bounds nearest the requested 1206 * cell location. Uses Euclidean distance to score multiple vacant areas. 1207 * 1208 * @param pixelX The X location at which you want to search for a vacant area. 1209 * @param pixelY The Y location at which you want to search for a vacant area. 1210 * @param minSpanX The minimum horizontal span required 1211 * @param minSpanY The minimum vertical span required 1212 * @param spanX Horizontal span of the object. 1213 * @param spanY Vertical span of the object. 1214 * @param result Array in which to place the result, or null (in which case a new array will 1215 * be allocated) 1216 * @return The X, Y cell of a vacant area that can contain this object, 1217 * nearest the requested location. 1218 */ findNearestVacantArea(int pixelX, int pixelY, int minSpanX, int minSpanY, int spanX, int spanY, int[] result, int[] resultSpan)1219 public int[] findNearestVacantArea(int pixelX, int pixelY, int minSpanX, int minSpanY, 1220 int spanX, int spanY, int[] result, int[] resultSpan) { 1221 return findNearestArea(pixelX, pixelY, minSpanX, minSpanY, spanX, spanY, false, 1222 result, resultSpan); 1223 } 1224 1225 /** 1226 * Find a vacant area that will fit the given bounds nearest the requested 1227 * cell location. Uses Euclidean distance to score multiple vacant areas. 1228 * @param relativeXPos The X location relative to the Cell layout at which you want to search 1229 * for a vacant area. 1230 * @param relativeYPos The Y location relative to the Cell layout at which you want to search 1231 * for a vacant area. 1232 * @param minSpanX The minimum horizontal span required 1233 * @param minSpanY The minimum vertical span required 1234 * @param spanX Horizontal span of the object. 1235 * @param spanY Vertical span of the object. 1236 * @param ignoreOccupied If true, the result can be an occupied cell 1237 * @param result Array in which to place the result, or null (in which case a new array will 1238 * be allocated) 1239 * @return The X, Y cell of a vacant area that can contain this object, 1240 * nearest the requested location. 1241 */ findNearestArea(int relativeXPos, int relativeYPos, int minSpanX, int minSpanY, int spanX, int spanY, boolean ignoreOccupied, int[] result, int[] resultSpan)1242 protected int[] findNearestArea(int relativeXPos, int relativeYPos, int minSpanX, int minSpanY, 1243 int spanX, int spanY, boolean ignoreOccupied, int[] result, int[] resultSpan) { 1244 // For items with a spanX / spanY > 1, the passed in point (relativeXPos, relativeYPos) 1245 // corresponds to the center of the item, but we are searching based on the top-left cell, 1246 // so we translate the point over to correspond to the top-left. 1247 relativeXPos = (int) (relativeXPos - (mCellWidth + mBorderSpace.x) * (spanX - 1) / 2f); 1248 relativeYPos = (int) (relativeYPos - (mCellHeight + mBorderSpace.y) * (spanY - 1) / 2f); 1249 1250 // Keep track of best-scoring drop area 1251 final int[] bestXY = result != null ? result : new int[2]; 1252 double bestDistance = Double.MAX_VALUE; 1253 final Rect bestRect = new Rect(-1, -1, -1, -1); 1254 final Stack<Rect> validRegions = new Stack<>(); 1255 1256 final int countX = mCountX; 1257 final int countY = mCountY; 1258 1259 if (minSpanX <= 0 || minSpanY <= 0 || spanX <= 0 || spanY <= 0 || 1260 spanX < minSpanX || spanY < minSpanY) { 1261 return bestXY; 1262 } 1263 1264 for (int y = 0; y < countY - (minSpanY - 1); y++) { 1265 inner: 1266 for (int x = 0; x < countX - (minSpanX - 1); x++) { 1267 int ySize = -1; 1268 int xSize = -1; 1269 if (!ignoreOccupied) { 1270 // First, let's see if this thing fits anywhere 1271 for (int i = 0; i < minSpanX; i++) { 1272 for (int j = 0; j < minSpanY; j++) { 1273 if (mOccupied.cells[x + i][y + j]) { 1274 continue inner; 1275 } 1276 } 1277 } 1278 xSize = minSpanX; 1279 ySize = minSpanY; 1280 1281 // We know that the item will fit at _some_ acceptable size, now let's see 1282 // how big we can make it. We'll alternate between incrementing x and y spans 1283 // until we hit a limit. 1284 boolean incX = true; 1285 boolean hitMaxX = xSize >= spanX; 1286 boolean hitMaxY = ySize >= spanY; 1287 while (!(hitMaxX && hitMaxY)) { 1288 if (incX && !hitMaxX) { 1289 for (int j = 0; j < ySize; j++) { 1290 if (x + xSize > countX -1 || mOccupied.cells[x + xSize][y + j]) { 1291 // We can't move out horizontally 1292 hitMaxX = true; 1293 } 1294 } 1295 if (!hitMaxX) { 1296 xSize++; 1297 } 1298 } else if (!hitMaxY) { 1299 for (int i = 0; i < xSize; i++) { 1300 if (y + ySize > countY - 1 || mOccupied.cells[x + i][y + ySize]) { 1301 // We can't move out vertically 1302 hitMaxY = true; 1303 } 1304 } 1305 if (!hitMaxY) { 1306 ySize++; 1307 } 1308 } 1309 hitMaxX |= xSize >= spanX; 1310 hitMaxY |= ySize >= spanY; 1311 incX = !incX; 1312 } 1313 } 1314 final int[] cellXY = mTmpPoint; 1315 cellToCenterPoint(x, y, cellXY); 1316 1317 // We verify that the current rect is not a sub-rect of any of our previous 1318 // candidates. In this case, the current rect is disqualified in favour of the 1319 // containing rect. 1320 Rect currentRect = new Rect(x, y, x + xSize, y + ySize); 1321 boolean contained = false; 1322 for (Rect r : validRegions) { 1323 if (r.contains(currentRect)) { 1324 contained = true; 1325 break; 1326 } 1327 } 1328 validRegions.push(currentRect); 1329 double distance = Math.hypot(cellXY[0] - relativeXPos, cellXY[1] - relativeYPos); 1330 1331 if ((distance <= bestDistance && !contained) || 1332 currentRect.contains(bestRect)) { 1333 bestDistance = distance; 1334 bestXY[0] = x; 1335 bestXY[1] = y; 1336 if (resultSpan != null) { 1337 resultSpan[0] = xSize; 1338 resultSpan[1] = ySize; 1339 } 1340 bestRect.set(currentRect); 1341 } 1342 } 1343 } 1344 1345 // Return -1, -1 if no suitable location found 1346 if (bestDistance == Double.MAX_VALUE) { 1347 bestXY[0] = -1; 1348 bestXY[1] = -1; 1349 } 1350 return bestXY; 1351 } 1352 getOccupied()1353 public GridOccupancy getOccupied() { 1354 return mOccupied; 1355 } 1356 copySolutionToTempState(ItemConfiguration solution, View dragView)1357 private void copySolutionToTempState(ItemConfiguration solution, View dragView) { 1358 mTmpOccupied.clear(); 1359 1360 int childCount = mShortcutsAndWidgets.getChildCount(); 1361 for (int i = 0; i < childCount; i++) { 1362 View child = mShortcutsAndWidgets.getChildAt(i); 1363 if (child == dragView) continue; 1364 CellLayoutLayoutParams lp = (CellLayoutLayoutParams) child.getLayoutParams(); 1365 CellAndSpan c = solution.map.get(child); 1366 if (c != null) { 1367 lp.setTmpCellX(c.cellX); 1368 lp.setTmpCellY(c.cellY); 1369 lp.cellHSpan = c.spanX; 1370 lp.cellVSpan = c.spanY; 1371 mTmpOccupied.markCells(c, true); 1372 } 1373 } 1374 mTmpOccupied.markCells(solution, true); 1375 } 1376 animateItemsToSolution(ItemConfiguration solution, View dragView, boolean commitDragView)1377 private void animateItemsToSolution(ItemConfiguration solution, View dragView, boolean 1378 commitDragView) { 1379 1380 GridOccupancy occupied = DESTRUCTIVE_REORDER ? mOccupied : mTmpOccupied; 1381 occupied.clear(); 1382 1383 int childCount = mShortcutsAndWidgets.getChildCount(); 1384 for (int i = 0; i < childCount; i++) { 1385 View child = mShortcutsAndWidgets.getChildAt(i); 1386 if (child == dragView) continue; 1387 CellAndSpan c = solution.map.get(child); 1388 if (c != null) { 1389 animateChildToPosition(child, c.cellX, c.cellY, REORDER_ANIMATION_DURATION, 0, 1390 DESTRUCTIVE_REORDER, false); 1391 occupied.markCells(c, true); 1392 } 1393 } 1394 if (commitDragView) { 1395 occupied.markCells(solution, true); 1396 } 1397 } 1398 1399 1400 // This method starts or changes the reorder preview animations beginOrAdjustReorderPreviewAnimations(ItemConfiguration solution, View dragView, int mode)1401 private void beginOrAdjustReorderPreviewAnimations(ItemConfiguration solution, 1402 View dragView, int mode) { 1403 int childCount = mShortcutsAndWidgets.getChildCount(); 1404 for (int i = 0; i < childCount; i++) { 1405 View child = mShortcutsAndWidgets.getChildAt(i); 1406 if (child == dragView) continue; 1407 CellAndSpan c = solution.map.get(child); 1408 boolean skip = mode == ReorderPreviewAnimation.MODE_HINT 1409 && !solution.intersectingViews.contains(child); 1410 1411 CellLayoutLayoutParams lp = (CellLayoutLayoutParams) child.getLayoutParams(); 1412 if (c != null && !skip && (child instanceof Reorderable)) { 1413 ReorderPreviewAnimation rha = new ReorderPreviewAnimation(child, mode, 1414 lp.getCellX(), lp.getCellY(), c.cellX, c.cellY, c.spanX, c.spanY, 1415 mReorderPreviewAnimationMagnitude, this, mShakeAnimators); 1416 rha.animate(); 1417 } 1418 } 1419 } 1420 completeAndClearReorderPreviewAnimations()1421 private void completeAndClearReorderPreviewAnimations() { 1422 for (ReorderPreviewAnimation a: mShakeAnimators.values()) { 1423 a.finishAnimation(); 1424 } 1425 mShakeAnimators.clear(); 1426 } 1427 commitTempPlacement(View dragView)1428 private void commitTempPlacement(View dragView) { 1429 mTmpOccupied.copyTo(mOccupied); 1430 1431 int screenId = mCellLayoutContainer.getCellLayoutId(this); 1432 int container = Favorites.CONTAINER_DESKTOP; 1433 1434 if (mContainerType == HOTSEAT) { 1435 screenId = -1; 1436 container = Favorites.CONTAINER_HOTSEAT; 1437 } 1438 1439 int childCount = mShortcutsAndWidgets.getChildCount(); 1440 for (int i = 0; i < childCount; i++) { 1441 View child = mShortcutsAndWidgets.getChildAt(i); 1442 CellLayoutLayoutParams lp = (CellLayoutLayoutParams) child.getLayoutParams(); 1443 ItemInfo info = (ItemInfo) child.getTag(); 1444 // We do a null check here because the item info can be null in the case of the 1445 // AllApps button in the hotseat. 1446 if (info != null && child != dragView) { 1447 CellPos presenterPos = mActivity.getCellPosMapper().mapModelToPresenter(info); 1448 final boolean requiresDbUpdate = (presenterPos.cellX != lp.getTmpCellX() 1449 || presenterPos.cellY != lp.getTmpCellY() || info.spanX != lp.cellHSpan 1450 || info.spanY != lp.cellVSpan || presenterPos.screenId != screenId); 1451 1452 lp.setCellX(lp.getTmpCellX()); 1453 lp.setCellY(lp.getTmpCellY()); 1454 if (requiresDbUpdate) { 1455 Launcher.cast(mActivity).getModelWriter().modifyItemInDatabase(info, container, 1456 screenId, lp.getCellX(), lp.getCellY(), lp.cellHSpan, lp.cellVSpan); 1457 } 1458 } 1459 } 1460 } 1461 setUseTempCoords(boolean useTempCoords)1462 private void setUseTempCoords(boolean useTempCoords) { 1463 int childCount = mShortcutsAndWidgets.getChildCount(); 1464 for (int i = 0; i < childCount; i++) { 1465 CellLayoutLayoutParams lp = (CellLayoutLayoutParams) mShortcutsAndWidgets.getChildAt( 1466 i).getLayoutParams(); 1467 lp.useTmpCoords = useTempCoords; 1468 } 1469 } 1470 1471 /** 1472 * For a given region, return the rectangle of the overlapping cell and span with the given 1473 * region including the region itself. If there is no overlap the rectangle will be 1474 * invalid i.e. -1, 0, -1, 0. 1475 */ 1476 @Nullable getIntersectingRectanglesInRegion(final Rect region, final View dragView)1477 public Rect getIntersectingRectanglesInRegion(final Rect region, final View dragView) { 1478 Rect boundingRect = new Rect(region); 1479 Rect r1 = new Rect(); 1480 boolean isOverlapping = false; 1481 final int count = mShortcutsAndWidgets.getChildCount(); 1482 for (int i = 0; i < count; i++) { 1483 View child = mShortcutsAndWidgets.getChildAt(i); 1484 if (child == dragView) continue; 1485 CellLayoutLayoutParams 1486 lp = (CellLayoutLayoutParams) child.getLayoutParams(); 1487 r1.set(lp.getCellX(), lp.getCellY(), lp.getCellX() + lp.cellHSpan, 1488 lp.getCellY() + lp.cellVSpan); 1489 if (Rect.intersects(region, r1)) { 1490 isOverlapping = true; 1491 boundingRect.union(r1); 1492 } 1493 } 1494 return isOverlapping ? boundingRect : null; 1495 } 1496 isNearestDropLocationOccupied(int pixelX, int pixelY, int spanX, int spanY, View dragView, int[] result)1497 public boolean isNearestDropLocationOccupied(int pixelX, int pixelY, int spanX, int spanY, 1498 View dragView, int[] result) { 1499 result = findNearestAreaIgnoreOccupied(pixelX, pixelY, spanX, spanY, result); 1500 return getIntersectingRectanglesInRegion( 1501 new Rect(result[0], result[1], result[0] + spanX, result[1] + spanY), 1502 dragView 1503 ) != null; 1504 } 1505 revertTempState()1506 void revertTempState() { 1507 completeAndClearReorderPreviewAnimations(); 1508 if (isItemPlacementDirty() && !DESTRUCTIVE_REORDER) { 1509 final int count = mShortcutsAndWidgets.getChildCount(); 1510 for (int i = 0; i < count; i++) { 1511 View child = mShortcutsAndWidgets.getChildAt(i); 1512 CellLayoutLayoutParams 1513 lp = (CellLayoutLayoutParams) child.getLayoutParams(); 1514 if (lp.getTmpCellX() != lp.getCellX() || lp.getTmpCellY() != lp.getCellY()) { 1515 lp.setTmpCellX(lp.getCellX()); 1516 lp.setTmpCellY(lp.getCellY()); 1517 animateChildToPosition(child, lp.getCellX(), lp.getCellY(), 1518 REORDER_ANIMATION_DURATION, 0, false, false); 1519 } 1520 } 1521 setItemPlacementDirty(false); 1522 } 1523 } 1524 createAreaForResize(int cellX, int cellY, int spanX, int spanY, View dragView, int[] direction, boolean commit)1525 boolean createAreaForResize(int cellX, int cellY, int spanX, int spanY, 1526 View dragView, int[] direction, boolean commit) { 1527 int[] pixelXY = new int[2]; 1528 regionToCenterPoint(cellX, cellY, spanX, spanY, pixelXY); 1529 1530 // First we determine if things have moved enough to cause a different layout 1531 ItemConfiguration swapSolution = findReorderSolution(pixelXY[0], pixelXY[1], spanX, spanY, 1532 spanX, spanY, direction, dragView, true); 1533 1534 setUseTempCoords(true); 1535 if (swapSolution != null && swapSolution.isSolution) { 1536 // If we're just testing for a possible location (MODE_ACCEPT_DROP), we don't bother 1537 // committing anything or animating anything as we just want to determine if a solution 1538 // exists 1539 copySolutionToTempState(swapSolution, dragView); 1540 setItemPlacementDirty(true); 1541 animateItemsToSolution(swapSolution, dragView, commit); 1542 1543 if (commit) { 1544 commitTempPlacement(null); 1545 completeAndClearReorderPreviewAnimations(); 1546 setItemPlacementDirty(false); 1547 } else { 1548 beginOrAdjustReorderPreviewAnimations(swapSolution, dragView, 1549 ReorderPreviewAnimation.MODE_PREVIEW); 1550 } 1551 mShortcutsAndWidgets.requestLayout(); 1552 } 1553 return swapSolution.isSolution; 1554 } 1555 createReorderAlgorithm()1556 public ReorderAlgorithm createReorderAlgorithm() { 1557 return new ReorderAlgorithm(this); 1558 } 1559 findReorderSolution(int pixelX, int pixelY, int minSpanX, int minSpanY, int spanX, int spanY, int[] direction, View dragView, boolean decX)1560 protected ItemConfiguration findReorderSolution(int pixelX, int pixelY, int minSpanX, 1561 int minSpanY, int spanX, int spanY, int[] direction, View dragView, boolean decX) { 1562 ItemConfiguration configuration = new ItemConfiguration(); 1563 copyCurrentStateToSolution(configuration); 1564 ReorderParameters parameters = new ReorderParameters(pixelX, pixelY, spanX, spanY, minSpanX, 1565 minSpanY, dragView, configuration); 1566 int[] directionVector = direction != null ? direction : mDirectionVector; 1567 return createReorderAlgorithm().findReorderSolution(parameters, directionVector, decX); 1568 } 1569 copyCurrentStateToSolution(ItemConfiguration solution)1570 public void copyCurrentStateToSolution(ItemConfiguration solution) { 1571 int childCount = mShortcutsAndWidgets.getChildCount(); 1572 for (int i = 0; i < childCount; i++) { 1573 View child = mShortcutsAndWidgets.getChildAt(i); 1574 CellLayoutLayoutParams lp = (CellLayoutLayoutParams) child.getLayoutParams(); 1575 solution.add(child, 1576 new CellAndSpan(lp.getCellX(), lp.getCellY(), lp.cellHSpan, lp.cellVSpan)); 1577 } 1578 } 1579 1580 /** 1581 * When the user drags an Item in the workspace sometimes we need to move the items already in 1582 * the workspace to make space for the new item, this function return a solution for that 1583 * reorder. 1584 * 1585 * @param pixelX X coordinate in the screen of the dragView in pixels 1586 * @param pixelY Y coordinate in the screen of the dragView in pixels 1587 * @param minSpanX minimum horizontal span the item can be shrunk to 1588 * @param minSpanY minimum vertical span the item can be shrunk to 1589 * @param spanX occupied horizontal span 1590 * @param spanY occupied vertical span 1591 * @param dragView the view of the item being draged 1592 * @return returns a solution for the given parameters, the solution contains all the icons and 1593 * the locations they should be in the given solution. 1594 */ calculateReorder(int pixelX, int pixelY, int minSpanX, int minSpanY, int spanX, int spanY, View dragView)1595 public ItemConfiguration calculateReorder(int pixelX, int pixelY, int minSpanX, int minSpanY, 1596 int spanX, int spanY, View dragView) { 1597 ItemConfiguration configuration = new ItemConfiguration(); 1598 copyCurrentStateToSolution(configuration); 1599 return createReorderAlgorithm().calculateReorder( 1600 new ReorderParameters(pixelX, pixelY, spanX, spanY, minSpanX, minSpanY, dragView, 1601 configuration) 1602 ); 1603 } 1604 performReorder(int pixelX, int pixelY, int minSpanX, int minSpanY, int spanX, int spanY, View dragView, int[] result, int[] resultSpan, int mode)1605 int[] performReorder(int pixelX, int pixelY, int minSpanX, int minSpanY, int spanX, int spanY, 1606 View dragView, int[] result, int[] resultSpan, int mode) { 1607 if (resultSpan == null) { 1608 resultSpan = new int[]{-1, -1}; 1609 } 1610 if (result == null) { 1611 result = new int[]{-1, -1}; 1612 } 1613 1614 ItemConfiguration finalSolution = null; 1615 // We want the solution to match the animation of the preview and to match the drop so we 1616 // only recalculate in mode MODE_SHOW_REORDER_HINT because that the first one to run in the 1617 // reorder cycle. 1618 if (mode == MODE_SHOW_REORDER_HINT || mPreviousSolution == null) { 1619 finalSolution = calculateReorder(pixelX, pixelY, minSpanX, minSpanY, spanX, spanY, 1620 dragView); 1621 mPreviousSolution = finalSolution; 1622 } else { 1623 finalSolution = mPreviousSolution; 1624 // We reset this vector after drop 1625 if (mode == MODE_ON_DROP || mode == MODE_ON_DROP_EXTERNAL) { 1626 mPreviousSolution = null; 1627 } 1628 } 1629 1630 if (finalSolution == null || !finalSolution.isSolution) { 1631 result[0] = result[1] = resultSpan[0] = resultSpan[1] = -1; 1632 } else { 1633 result[0] = finalSolution.cellX; 1634 result[1] = finalSolution.cellY; 1635 resultSpan[0] = finalSolution.spanX; 1636 resultSpan[1] = finalSolution.spanY; 1637 performReorder(finalSolution, dragView, mode); 1638 } 1639 return result; 1640 } 1641 1642 /** 1643 * Animates and submits in the DB the given ItemConfiguration depending of the mode. 1644 * 1645 * @param solution represents widgets on the screen which the Workspace will animate to and 1646 * would be submitted to the database. 1647 * @param dragView view which is being dragged over the workspace that trigger the reorder 1648 * @param mode depending on the mode different animations would be played and depending on the 1649 * mode the solution would be submitted or not the database. 1650 * The possible modes are {@link MODE_SHOW_REORDER_HINT}, {@link MODE_DRAG_OVER}, 1651 * {@link MODE_ON_DROP}, {@link MODE_ON_DROP_EXTERNAL}, {@link MODE_ACCEPT_DROP} 1652 * defined in {@link CellLayout}. 1653 */ performReorder(ItemConfiguration solution, View dragView, int mode)1654 public void performReorder(ItemConfiguration solution, View dragView, int mode) { 1655 if (mode == MODE_SHOW_REORDER_HINT) { 1656 beginOrAdjustReorderPreviewAnimations(solution, dragView, 1657 ReorderPreviewAnimation.MODE_HINT); 1658 return; 1659 } 1660 // If we're just testing for a possible location (MODE_ACCEPT_DROP), we don't bother 1661 // committing anything or animating anything as we just want to determine if a solution 1662 // exists 1663 if (mode == MODE_DRAG_OVER || mode == MODE_ON_DROP || mode == MODE_ON_DROP_EXTERNAL) { 1664 if (!DESTRUCTIVE_REORDER) { 1665 setUseTempCoords(true); 1666 } 1667 1668 if (!DESTRUCTIVE_REORDER) { 1669 copySolutionToTempState(solution, dragView); 1670 } 1671 setItemPlacementDirty(true); 1672 animateItemsToSolution(solution, dragView, mode == MODE_ON_DROP); 1673 1674 if (!DESTRUCTIVE_REORDER 1675 && (mode == MODE_ON_DROP || mode == MODE_ON_DROP_EXTERNAL)) { 1676 // Since the temp solution didn't update dragView, don't commit it either 1677 commitTempPlacement(dragView); 1678 completeAndClearReorderPreviewAnimations(); 1679 setItemPlacementDirty(false); 1680 } else { 1681 beginOrAdjustReorderPreviewAnimations(solution, dragView, 1682 ReorderPreviewAnimation.MODE_PREVIEW); 1683 } 1684 } 1685 1686 if (mode == MODE_ON_DROP && !DESTRUCTIVE_REORDER) { 1687 setUseTempCoords(false); 1688 } 1689 1690 mShortcutsAndWidgets.requestLayout(); 1691 } 1692 setItemPlacementDirty(boolean dirty)1693 void setItemPlacementDirty(boolean dirty) { 1694 mItemPlacementDirty = dirty; 1695 } isItemPlacementDirty()1696 boolean isItemPlacementDirty() { 1697 return mItemPlacementDirty; 1698 } 1699 1700 /** 1701 * Find a starting cell position that will fit the given bounds nearest the requested 1702 * cell location. Uses Euclidean distance to score multiple vacant areas. 1703 * 1704 * @param pixelX The X location at which you want to search for a vacant area. 1705 * @param pixelY The Y location at which you want to search for a vacant area. 1706 * @param spanX Horizontal span of the object. 1707 * @param spanY Vertical span of the object. 1708 * @param result Previously returned value to possibly recycle. 1709 * @return The X, Y cell of a vacant area that can contain this object, 1710 * nearest the requested location. 1711 */ findNearestAreaIgnoreOccupied(int pixelX, int pixelY, int spanX, int spanY, int[] result)1712 public int[] findNearestAreaIgnoreOccupied(int pixelX, int pixelY, int spanX, int spanY, 1713 int[] result) { 1714 return findNearestArea(pixelX, pixelY, spanX, spanY, spanX, spanY, true, result, null); 1715 } 1716 existsEmptyCell()1717 boolean existsEmptyCell() { 1718 return findCellForSpan(null, 1, 1); 1719 } 1720 1721 /** 1722 * Finds the upper-left coordinate of the first rectangle in the grid that can 1723 * hold a cell of the specified dimensions. If intersectX and intersectY are not -1, 1724 * then this method will only return coordinates for rectangles that contain the cell 1725 * (intersectX, intersectY) 1726 * 1727 * @param cellXY The array that will contain the position of a vacant cell if such a cell 1728 * can be found. 1729 * @param spanX The horizontal span of the cell we want to find. 1730 * @param spanY The vertical span of the cell we want to find. 1731 * 1732 * @return True if a vacant cell of the specified dimension was found, false otherwise. 1733 */ findCellForSpan(int[] cellXY, int spanX, int spanY)1734 public boolean findCellForSpan(int[] cellXY, int spanX, int spanY) { 1735 if (cellXY == null) { 1736 cellXY = new int[2]; 1737 } 1738 return mOccupied.findVacantCell(cellXY, spanX, spanY); 1739 } 1740 1741 /** 1742 * A drag event has begun over this layout. 1743 * It may have begun over this layout (in which case onDragChild is called first), 1744 * or it may have begun on another layout. 1745 */ onDragEnter()1746 void onDragEnter() { 1747 mDragging = true; 1748 mPreviousSolution = null; 1749 } 1750 1751 /** 1752 * Called when drag has left this CellLayout or has been completed (successfully or not) 1753 */ onDragExit()1754 void onDragExit() { 1755 // This can actually be called when we aren't in a drag, e.g. when adding a new 1756 // item to this layout via the customize drawer. 1757 // Guard against that case. 1758 if (mDragging) { 1759 mDragging = false; 1760 } 1761 1762 // Invalidate the drag data 1763 mPreviousSolution = null; 1764 mDragCell[0] = mDragCell[1] = -1; 1765 mDragCellSpan[0] = mDragCellSpan[1] = -1; 1766 mDragOutlineAnims[mDragOutlineCurrent].animateOut(); 1767 mDragOutlineCurrent = (mDragOutlineCurrent + 1) % mDragOutlineAnims.length; 1768 revertTempState(); 1769 setIsDragOverlapping(false); 1770 } 1771 1772 /** 1773 * Mark a child as having been dropped. 1774 * At the beginning of the drag operation, the child may have been on another 1775 * screen, but it is re-parented before this method is called. 1776 * 1777 * @param child The child that is being dropped 1778 */ onDropChild(View child)1779 void onDropChild(View child) { 1780 if (child != null) { 1781 CellLayoutLayoutParams 1782 lp = (CellLayoutLayoutParams) child.getLayoutParams(); 1783 lp.dropped = true; 1784 child.requestLayout(); 1785 markCellsAsOccupiedForView(child); 1786 } 1787 } 1788 1789 /** 1790 * Computes a bounding rectangle for a range of cells 1791 * 1792 * @param cellX X coordinate of upper left corner expressed as a cell position 1793 * @param cellY Y coordinate of upper left corner expressed as a cell position 1794 * @param cellHSpan Width in cells 1795 * @param cellVSpan Height in cells 1796 * @param resultRect Rect into which to put the results 1797 */ cellToRect(int cellX, int cellY, int cellHSpan, int cellVSpan, Rect resultRect)1798 public void cellToRect(int cellX, int cellY, int cellHSpan, int cellVSpan, Rect resultRect) { 1799 final int cellWidth = mCellWidth; 1800 final int cellHeight = mCellHeight; 1801 1802 // We observe a shift of 1 pixel on the x coordinate compared to the actual cell coordinates 1803 final int hStartPadding = getPaddingLeft() 1804 + (int) Math.ceil(getUnusedHorizontalSpace() / 2f); 1805 final int vStartPadding = getPaddingTop(); 1806 1807 int x = hStartPadding + (cellX * mBorderSpace.x) + (cellX * cellWidth); 1808 int y = vStartPadding + (cellY * mBorderSpace.y) + (cellY * cellHeight); 1809 1810 int width = cellHSpan * cellWidth + ((cellHSpan - 1) * mBorderSpace.x); 1811 int height = cellVSpan * cellHeight + ((cellVSpan - 1) * mBorderSpace.y); 1812 1813 resultRect.set(x, y, x + width, y + height); 1814 } 1815 markCellsAsOccupiedForView(View view)1816 public void markCellsAsOccupiedForView(View view) { 1817 if (view instanceof LauncherAppWidgetHostView 1818 && view.getTag() instanceof LauncherAppWidgetInfo) { 1819 LauncherAppWidgetInfo info = (LauncherAppWidgetInfo) view.getTag(); 1820 CellPos pos = mActivity.getCellPosMapper().mapModelToPresenter(info); 1821 mOccupied.markCells(pos.cellX, pos.cellY, info.spanX, info.spanY, true); 1822 return; 1823 } 1824 if (view == null || view.getParent() != mShortcutsAndWidgets) return; 1825 CellLayoutLayoutParams 1826 lp = (CellLayoutLayoutParams) view.getLayoutParams(); 1827 mOccupied.markCells(lp.getCellX(), lp.getCellY(), lp.cellHSpan, lp.cellVSpan, true); 1828 } 1829 markCellsAsUnoccupiedForView(View view)1830 public void markCellsAsUnoccupiedForView(View view) { 1831 if (view instanceof LauncherAppWidgetHostView 1832 && view.getTag() instanceof LauncherAppWidgetInfo) { 1833 LauncherAppWidgetInfo info = (LauncherAppWidgetInfo) view.getTag(); 1834 CellPos pos = mActivity.getCellPosMapper().mapModelToPresenter(info); 1835 mOccupied.markCells(pos.cellX, pos.cellY, info.spanX, info.spanY, false); 1836 return; 1837 } 1838 if (view == null || view.getParent() != mShortcutsAndWidgets) return; 1839 CellLayoutLayoutParams 1840 lp = (CellLayoutLayoutParams) view.getLayoutParams(); 1841 mOccupied.markCells(lp.getCellX(), lp.getCellY(), lp.cellHSpan, lp.cellVSpan, false); 1842 } 1843 getDesiredWidth()1844 public int getDesiredWidth() { 1845 return getPaddingLeft() + getPaddingRight() + (mCountX * mCellWidth) 1846 + ((mCountX - 1) * mBorderSpace.x); 1847 } 1848 getDesiredHeight()1849 public int getDesiredHeight() { 1850 return getPaddingTop() + getPaddingBottom() + (mCountY * mCellHeight) 1851 + ((mCountY - 1) * mBorderSpace.y); 1852 } 1853 isOccupied(int x, int y)1854 public boolean isOccupied(int x, int y) { 1855 if (x >= 0 && x < mCountX && y >= 0 && y < mCountY) { 1856 return mOccupied.cells[x][y]; 1857 } 1858 if (BuildConfig.IS_STUDIO_BUILD) { 1859 throw new RuntimeException("Position exceeds the bound of this CellLayout"); 1860 } 1861 return true; 1862 } 1863 1864 @Override generateLayoutParams(AttributeSet attrs)1865 public ViewGroup.LayoutParams generateLayoutParams(AttributeSet attrs) { 1866 return new CellLayoutLayoutParams(getContext(), attrs); 1867 } 1868 1869 @Override checkLayoutParams(ViewGroup.LayoutParams p)1870 protected boolean checkLayoutParams(ViewGroup.LayoutParams p) { 1871 return p instanceof CellLayoutLayoutParams; 1872 } 1873 1874 @Override generateLayoutParams(ViewGroup.LayoutParams p)1875 protected ViewGroup.LayoutParams generateLayoutParams(ViewGroup.LayoutParams p) { 1876 return new CellLayoutLayoutParams(p); 1877 } 1878 1879 /** 1880 * Returns whether an item can be placed in this CellLayout (after rearranging and/or resizing 1881 * if necessary). 1882 */ hasReorderSolution(ItemInfo itemInfo)1883 public boolean hasReorderSolution(ItemInfo itemInfo) { 1884 int[] cellPoint = new int[2]; 1885 // Check for a solution starting at every cell. 1886 for (int cellX = 0; cellX < getCountX(); cellX++) { 1887 for (int cellY = 0; cellY < getCountY(); cellY++) { 1888 cellToPoint(cellX, cellY, cellPoint); 1889 if (findReorderSolution(cellPoint[0], cellPoint[1], itemInfo.minSpanX, 1890 itemInfo.minSpanY, itemInfo.spanX, itemInfo.spanY, mDirectionVector, null, 1891 true).isSolution) { 1892 return true; 1893 } 1894 } 1895 } 1896 return false; 1897 } 1898 1899 /** 1900 * Finds solution to accept hotseat migration to cell layout. commits solution if commitConfig 1901 */ makeSpaceForHotseatMigration(boolean commitConfig)1902 public boolean makeSpaceForHotseatMigration(boolean commitConfig) { 1903 int[] cellPoint = new int[2]; 1904 int[] directionVector = new int[]{0, -1}; 1905 cellToPoint(0, mCountY, cellPoint); 1906 ItemConfiguration configuration = findReorderSolution( 1907 cellPoint[0] /* pixelX */, 1908 cellPoint[1] /* pixelY */, 1909 mCountX /* minSpanX */, 1910 1 /* minSpanY */, 1911 mCountX /* spanX */, 1912 1 /* spanY */, 1913 directionVector /* direction */, 1914 null /* dragView */, 1915 false /* decX */ 1916 ); 1917 if (configuration.isSolution) { 1918 if (commitConfig) { 1919 copySolutionToTempState(configuration, null); 1920 commitTempPlacement(null); 1921 // undo marking cells occupied since there is actually nothing being placed yet. 1922 mOccupied.markCells(0, mCountY - 1, mCountX, 1, false); 1923 } 1924 return true; 1925 } 1926 return false; 1927 } 1928 1929 /** 1930 * returns a copy of cell layout's grid occupancy 1931 */ cloneGridOccupancy()1932 public GridOccupancy cloneGridOccupancy() { 1933 GridOccupancy occupancy = new GridOccupancy(mCountX, mCountY); 1934 mOccupied.copyTo(occupancy); 1935 return occupancy; 1936 } 1937 isRegionVacant(int x, int y, int spanX, int spanY)1938 public boolean isRegionVacant(int x, int y, int spanX, int spanY) { 1939 return mOccupied.isRegionVacant(x, y, spanX, spanY); 1940 } 1941 setSpaceBetweenCellLayoutsPx(@x int spaceBetweenCellLayoutsPx)1942 public void setSpaceBetweenCellLayoutsPx(@Px int spaceBetweenCellLayoutsPx) { 1943 mSpaceBetweenCellLayoutsPx = spaceBetweenCellLayoutsPx; 1944 } 1945 } 1946