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