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