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