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