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