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