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