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