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