1 /* 2 * Copyright (C) 2012 The Android Open Source Project 3 * 4 * Licensed under the Apache License, Version 2.0 (the "License"); 5 * you may not use this file except in compliance with the License. 6 * You may obtain a copy of the License at 7 * 8 * http://www.apache.org/licenses/LICENSE-2.0 9 * 10 * Unless required by applicable law or agreed to in writing, software 11 * distributed under the License is distributed on an "AS IS" BASIS, 12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 * See the License for the specific language governing permissions and 14 * limitations under the License. 15 */ 16 17 package com.android.deskclock.widget.sgv; 18 19 import android.animation.Animator; 20 import android.animation.AnimatorListenerAdapter; 21 import android.animation.AnimatorSet; 22 import android.animation.ValueAnimator; 23 import android.annotation.SuppressLint; 24 import android.content.Context; 25 import android.content.res.TypedArray; 26 import android.database.DataSetObserver; 27 import android.graphics.Bitmap; 28 import android.graphics.Canvas; 29 import android.graphics.PixelFormat; 30 import android.graphics.Point; 31 import android.graphics.Rect; 32 import android.os.Handler; 33 import android.os.Parcel; 34 import android.os.Parcelable; 35 import android.support.v4.util.SparseArrayCompat; 36 import android.support.v4.view.MotionEventCompat; 37 import android.support.v4.view.VelocityTrackerCompat; 38 import android.support.v4.view.ViewCompat; 39 import android.support.v4.widget.EdgeEffectCompat; 40 import android.util.AttributeSet; 41 import android.util.Log; 42 import android.util.SparseArray; 43 import android.view.DragEvent; 44 import android.view.Gravity; 45 import android.view.MotionEvent; 46 import android.view.VelocityTracker; 47 import android.view.View; 48 import android.view.ViewConfiguration; 49 import android.view.ViewGroup; 50 import android.view.WindowManager; 51 import android.widget.GridView; 52 import android.widget.ImageView; 53 import android.widget.ScrollView; 54 55 import com.android.deskclock.widget.sgv.SgvAnimationHelper.AnimationIn; 56 import com.android.deskclock.widget.sgv.SgvAnimationHelper.AnimationOut; 57 58 import java.util.ArrayList; 59 import java.util.Arrays; 60 import java.util.HashMap; 61 import java.util.List; 62 import java.util.Map; 63 64 /** 65 * Temporarily copied from support v4 library so that StaggeredGridView can access 66 * animation APIs on the current SDK version. 67 */ 68 /** 69 * ListView and GridView just not complex enough? Try StaggeredGridView! 70 * 71 * <p>StaggeredGridView presents a multi-column grid with consistent column sizes 72 * but varying row sizes between the columns. Each successive item from a 73 * {@link android.widget.ListAdapter ListAdapter} will be arranged from top to bottom, 74 * left to right. The largest vertical gap is always filled first.</p> 75 * 76 * <p>Item views may span multiple columns as specified by their {@link LayoutParams}. 77 * The attribute <code>android:layout_span</code> may be used when inflating 78 * item views from xml.</p> 79 */ 80 public class StaggeredGridView extends ViewGroup { 81 82 private static final String TAG = "Clock-" + StaggeredGridView.class.getSimpleName(); 83 84 /* 85 * There are a few things you should know if you're going to make modifications 86 * to StaggeredGridView. 87 * 88 * Like ListView, SGV populates from an adapter and recycles views that fall out 89 * of the visible boundaries of the grid. A few invariants always hold: 90 * 91 * - mFirstPosition is the adapter position of the View returned by getChildAt(0). 92 * - Any child index can be translated to an adapter position by adding mFirstPosition. 93 * - Any adapter position can be translated to a child index by subtracting mFirstPosition. 94 * - Views for items in the range [mFirstPosition, mFirstPosition + getChildCount()) are 95 * currently attached to the grid as children. All other adapter positions do not have 96 * active views. 97 * 98 * This means a few things thanks to the staggered grid's nature. Some views may stay attached 99 * long after they have scrolled offscreen if removing and recycling them would result in 100 * breaking one of the invariants above. 101 * 102 * LayoutRecords are used to track data about a particular item's layout after the associated 103 * view has been removed. These let positioning and the choice of column for an item 104 * remain consistent even though the rules for filling content up vs. filling down vary. 105 * 106 * Whenever layout parameters for a known LayoutRecord change, other LayoutRecords before 107 * or after it may need to be invalidated. e.g. if the item's height or the number 108 * of columns it spans changes, all bets for other items in the same direction are off 109 * since the cached information no longer applies. 110 */ 111 112 private GridAdapter mAdapter; 113 114 public static final int COLUMN_COUNT_AUTO = -1; 115 116 /** 117 * The window size to search for a specific item when restoring scroll position. 118 */ 119 private final int SCROLL_RESTORE_WINDOW_SIZE = 10; 120 121 private static final int CHILD_TO_REORDER_AREA_RATIO = 4; 122 123 private static final int SINGLE_COL_REORDERING_AREA_SIZE = 30; 124 125 // Time delay in milliseconds between posting each scroll runnables. 126 private static final int SCROLL_HANDLER_DELAY = 5; 127 128 // The default rate of pixels to scroll by when a child view is dragged towards the 129 // upper and lower bound of this view. 130 private static final int DRAG_SCROLL_RATE = 10; 131 132 public static final int ANIMATION_DELAY_IN_MS = 50; 133 134 private AnimationIn mAnimationInMode = AnimationIn.NONE; 135 private AnimationOut mAnimationOutMode = AnimationOut.NONE; 136 137 private AnimatorSet mCurrentRunningAnimatorSet = null; 138 139 /** 140 * Flag to indicate whether the current running animator set was canceled before it reaching 141 * the end of the animations. This flag is used to help indicate whether the next set of 142 * animators should resume from where the last animator set left off. 143 */ 144 boolean mIsCurrentAnimationCanceled = false; 145 146 private int mColCountSetting = 2; 147 private int mColCount = 2; 148 private int mMinColWidth = 0; 149 private int mItemMargin = 0; 150 151 private int[] mItemTops; 152 private int[] mItemBottoms; 153 154 private final Rect mTempRect = new Rect(); 155 156 private boolean mFastChildLayout; 157 private boolean mPopulating; 158 private boolean mInLayout; 159 160 private boolean mIsRtlLayout; 161 162 private final RecycleBin mRecycler = new RecycleBin(); 163 164 private final AdapterDataSetObserver mObserver = new AdapterDataSetObserver(); 165 166 private boolean mDataChanged; 167 private int mItemCount; 168 169 /** 170 * After data set change, we ask adapter the first view that changed. 171 * Any view from 0 to mFirstChangedPosition - 1 is not changed. 172 */ 173 private int mFirstChangedPosition; 174 175 /** 176 * If set to true, then we guard against jagged edges in the grid by doing expensive 177 * computation. Otherwise if this is false, we skip the computation. 178 */ 179 private boolean mGuardAgainstJaggedEdges; 180 181 private boolean mHasStableIds; 182 183 /** 184 * List of all views to animate out. This is used when we need to animate out stale views. 185 */ 186 private final List<View> mViewsToAnimateOut = new ArrayList<View>(); 187 188 private int mFirstPosition; 189 190 private long mFocusedChildIdToScrollIntoView; 191 private ScrollState mCurrentScrollState; 192 193 private final int mTouchSlop; 194 private final int mMaximumVelocity; 195 private final int mFlingVelocity; 196 private float mLastTouchY = 0; 197 private float mTouchRemainderY; 198 private int mActivePointerId; 199 200 private static final int TOUCH_MODE_IDLE = 0; 201 private static final int TOUCH_MODE_DRAGGING = 1; 202 private static final int TOUCH_MODE_FLINGING = 2; 203 private static final int TOUCH_MODE_OVERFLING = 3; 204 205 // Value used to estimate the range of scroll and scroll position 206 final static int SCROLLING_ESTIMATED_ITEM_HEIGHT = 100; 207 208 private int mTouchMode; 209 private final VelocityTracker mVelocityTracker = VelocityTracker.obtain(); 210 private final OverScrollerSGV mScroller; 211 212 private final EdgeEffectCompat mTopEdge; 213 private final EdgeEffectCompat mBottomEdge; 214 215 private boolean mIsDragReorderingEnabled; 216 217 private ScrollListener mScrollListener; 218 private OnSizeChangedListener mOnSizeChangedListener; 219 220 // The view to show when the adapter is empty. 221 private View mEmptyView; 222 223 // The size of the region at location relative to the child's edges where reordering 224 // can happen if another child view is dragged and dropped over it. 225 private int mHorizontalReorderingAreaSize; 226 227 // TODO: Put these states into a ReorderingParam object for maintainability. 228 private ImageView mDragView; 229 230 // X and Y positions of the touch down event that started the drag 231 private int mTouchDownForDragStartX; 232 private int mTouchDownForDragStartY; 233 234 // X and Y offsets inside the item from where the user grabbed to the 235 // child's left coordinate. 236 // This is used to aid in the drawing of the drag shadow. 237 private int mTouchOffsetToChildLeft; 238 private int mTouchOffsetToChildTop; 239 240 // Difference between screen coordinates and coordinates in this view. 241 private int mOffsetToAbsoluteX; 242 private int mOffsetToAbsoluteY; 243 244 // the cached positions of the drag view when released. 245 private Rect mCachedDragViewRect; 246 247 // the current drag state 248 private int mDragState; 249 250 // the height of this view 251 private int mHeight; 252 253 // The bounds of the screen that should initiate scrolling when a view 254 // is dragged past these positions. 255 private int mUpperScrollBound; 256 private int mLowerScrollBound; 257 258 // The Bitmap that contains the drag shadow. 259 private Bitmap mDragBitmap; 260 private final int mOverscrollDistance; 261 262 private final WindowManager mWindowManager; 263 private WindowManager.LayoutParams mWindowParams; 264 private static final int mWindowManagerLayoutFlags = 265 WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE | 266 WindowManager.LayoutParams.FLAG_NOT_TOUCHABLE | 267 WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON | 268 WindowManager.LayoutParams.FLAG_LAYOUT_IN_SCREEN | 269 WindowManager.LayoutParams.FLAG_LAYOUT_NO_LIMITS; 270 271 private ReorderHelper mReorderHelper; 272 273 /** 274 * Indicates whether to use pixels-based or position-based scrollbar 275 * properties. 276 * This property is borrow from AbsListView 277 */ 278 private boolean mSmoothScrollbarEnabled = false; 279 280 private static final class LayoutRecord { 281 public int column; 282 public long id = -1; 283 public int height; 284 public int span; 285 private int[] mMargins; 286 ensureMargins()287 private final void ensureMargins() { 288 if (mMargins == null) { 289 // Don't need to confirm length; 290 // all layoutrecords are purged when column count changes. 291 mMargins = new int[span * 2]; 292 } 293 } 294 getMarginAbove(int col)295 public final int getMarginAbove(int col) { 296 if (mMargins == null) { 297 return 0; 298 } 299 return mMargins[col * 2]; 300 } 301 getMarginBelow(int col)302 public final int getMarginBelow(int col) { 303 if (mMargins == null) { 304 return 0; 305 } 306 return mMargins[col * 2 + 1]; 307 } 308 setMarginAbove(int col, int margin)309 public final void setMarginAbove(int col, int margin) { 310 if (mMargins == null && margin == 0) { 311 return; 312 } 313 ensureMargins(); 314 mMargins[col * 2] = margin; 315 } 316 setMarginBelow(int col, int margin)317 public final void setMarginBelow(int col, int margin) { 318 if (mMargins == null && margin == 0) { 319 return; 320 } 321 ensureMargins(); 322 mMargins[col * 2 + 1] = margin; 323 } 324 325 @Override toString()326 public String toString() { 327 String result = "LayoutRecord{c=" + column + ", id=" + id + " h=" + height + 328 " s=" + span; 329 if (mMargins != null) { 330 result += " margins[above, below]("; 331 for (int i = 0; i < mMargins.length; i += 2) { 332 result += "[" + mMargins[i] + ", " + mMargins[i+1] + "]"; 333 } 334 result += ")"; 335 } 336 return result + "}"; 337 } 338 } 339 340 private final Map<Long, ViewRectPair> mChildRectsForAnimation = 341 new HashMap<Long, ViewRectPair>(); 342 343 private final SparseArrayCompat<LayoutRecord> mLayoutRecords = 344 new SparseArrayCompat<LayoutRecord>(); 345 346 // Handler for executing the scroll runnable 347 private Handler mScrollHandler; 348 349 // Boolean is true when the {@link #mDragScroller} scroll runanbled has been kicked off. 350 // This is set back to false when it is removed from the handler. 351 private boolean mIsDragScrollerRunning; 352 353 /** 354 * Scroller runnable to invoke scrolling when user is holding a dragged view over the upper 355 * or lower bounds of the screen. 356 */ 357 private final Runnable mDragScroller = new Runnable() { 358 @Override 359 public void run() { 360 if (mDragState == ReorderUtils.DRAG_STATE_NONE) { 361 return; 362 } 363 364 boolean enableUpdate = true; 365 if (mLastTouchY >= mLowerScrollBound) { 366 // scroll the list up a bit if we're past the lower bound, and the direction 367 // of the movement is towards the bottom of the view. 368 if (trackMotionScroll(-DRAG_SCROLL_RATE, false)) { 369 // Disable reordering if the view is scrolling 370 enableUpdate = false; 371 } 372 } else if (mLastTouchY <= mUpperScrollBound) { 373 // scroll the list down a bit if we're past the upper bound, and the direction 374 // of the movement is towards the top of the view. 375 if (trackMotionScroll(DRAG_SCROLL_RATE, false)) { 376 // Disable reordering if the view is scrolling 377 enableUpdate = false; 378 } 379 } 380 381 mReorderHelper.enableUpdatesOnDrag(enableUpdate); 382 383 mScrollHandler.postDelayed(this, SCROLL_HANDLER_DELAY); 384 } 385 }; 386 StaggeredGridView(Context context)387 public StaggeredGridView(Context context) { 388 this(context, null); 389 } 390 StaggeredGridView(Context context, AttributeSet attrs)391 public StaggeredGridView(Context context, AttributeSet attrs) { 392 this(context, attrs, 0); 393 } 394 StaggeredGridView(Context context, AttributeSet attrs, int defStyle)395 public StaggeredGridView(Context context, AttributeSet attrs, int defStyle) { 396 super(context, attrs, defStyle); 397 398 final ViewConfiguration vc = ViewConfiguration.get(context); 399 mTouchSlop = vc.getScaledTouchSlop(); 400 mMaximumVelocity = vc.getScaledMaximumFlingVelocity(); 401 mFlingVelocity = vc.getScaledMinimumFlingVelocity(); 402 mScroller = new OverScrollerSGV(context); 403 404 mTopEdge = new EdgeEffectCompat(context); 405 mBottomEdge = new EdgeEffectCompat(context); 406 setWillNotDraw(false); 407 setClipToPadding(false); 408 409 SgvAnimationHelper.initialize(context); 410 411 mDragState = ReorderUtils.DRAG_STATE_NONE; 412 mIsDragReorderingEnabled = true; 413 mWindowManager = (WindowManager) context.getSystemService(Context.WINDOW_SERVICE); 414 final ViewConfiguration configuration = ViewConfiguration.get(context); 415 mOverscrollDistance = configuration.getScaledOverflingDistance(); 416 // Disable splitting event. Only one of the children can handle motion event. 417 setMotionEventSplittingEnabled(false); 418 } 419 420 /** 421 * Check to see if the current layout is Right-to-Left. This check is only supported for 422 * API 17+. For earlier versions, this method will just return false. 423 * 424 * NOTE: This is based on the private API method in {@link View} class. 425 * 426 * @return boolean Boolean indicating whether the currently locale is RTL. 427 */ 428 @SuppressLint("NewApi") isLayoutRtl()429 private boolean isLayoutRtl() { 430 if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.JELLY_BEAN_MR1) { 431 return View.LAYOUT_DIRECTION_RTL == getLayoutDirection(); 432 } else { 433 return false; 434 } 435 } 436 437 /** 438 * Set a fixed number of columns for this grid. Space will be divided evenly 439 * among all columns, respecting the item margin between columns. 440 * The default is 2. (If it were 1, perhaps you should be using a 441 * {@link android.widget.ListView ListView}.) 442 * 443 * @param colCount Number of columns to display. 444 * @see #setMinColumnWidth(int) 445 */ setColumnCount(int colCount)446 public void setColumnCount(int colCount) { 447 if (colCount < 1 && colCount != COLUMN_COUNT_AUTO) { 448 throw new IllegalArgumentException("Column count must be at least 1 - received " + 449 colCount); 450 } 451 final boolean needsPopulate = colCount != mColCount; 452 mColCount = mColCountSetting = colCount; 453 if (needsPopulate) { 454 // When switching column count, for now, don't restore scroll position, and just 455 // start layout fresh again. 456 clearAllState(); 457 458 mHorizontalReorderingAreaSize = 0; 459 populate(); 460 } 461 } 462 getColumnCount()463 public int getColumnCount() { 464 return mColCount; 465 } 466 467 /** 468 * Set whether or not to explicitly guard against "jagged edges" in the grid 469 * (meaning that the top edge of the children views in the first row of the grid can be 470 * horizontally misaligned). 471 * 472 * If guardAgainstJaggedEdges is true, then we prevent jagged edges by computing the heights of 473 * all views starting at the 0th position of the adapter to figure out the proper offset of the 474 * views currently on screen. This is an expensive operation and should be avoided if possible. 475 * 476 * If guardAgainstJaggedEdges is false, then we can skip the expensive computation that 477 * guards against jagged edges and just layout views on the screen starting from mFirstPosition 478 * (ignoring what came before it). 479 */ setGuardAgainstJaggedEdges(boolean guardAgainstJaggedEdges)480 public void setGuardAgainstJaggedEdges(boolean guardAgainstJaggedEdges) { 481 mGuardAgainstJaggedEdges = guardAgainstJaggedEdges; 482 } 483 484 /** 485 * Set a minimum column width for 486 * @param minColWidth 487 */ setMinColumnWidth(int minColWidth)488 public void setMinColumnWidth(int minColWidth) { 489 mMinColWidth = minColWidth; 490 setColumnCount(COLUMN_COUNT_AUTO); 491 } 492 493 /** 494 * Set the margin between items in pixels. This margin is applied 495 * both vertically and horizontally. 496 * 497 * @param marginPixels Spacing between items in pixels 498 */ setItemMargin(int marginPixels)499 public void setItemMargin(int marginPixels) { 500 // We only need to {@link #populate()} if the margin has been changed. 501 if (marginPixels != mItemMargin) { 502 mItemMargin = marginPixels; 503 populate(); 504 } 505 } 506 getItemMargin()507 public int getItemMargin() { 508 return mItemMargin; 509 } 510 511 /** 512 * When smooth scrollbar is enabled, the position and size of the scrollbar thumb 513 * is computed based on the number of visible pixels in the visible items. This 514 * however assumes that all list items have the same height. If you use a list in 515 * which items have different heights, the scrollbar will change appearance as the 516 * user scrolls through the list. To avoid this issue, you need to disable this 517 * property. 518 * 519 * When smooth scrollbar is disabled, the position and size of the scrollbar thumb 520 * is based solely on the number of items in the adapter and the position of the 521 * visible items inside the adapter. This provides a stable scrollbar as the user 522 * navigates through a list of items with varying heights. 523 * 524 * @param enabled Whether or not to enable smooth scrollbar. 525 * 526 * @see #setSmoothScrollbarEnabled(boolean) 527 * @attr ref android.R.styleable#AbsListView_smoothScrollbar 528 */ setSmoothScrollbarEnabled(boolean enabled)529 public void setSmoothScrollbarEnabled(boolean enabled) { 530 mSmoothScrollbarEnabled = enabled; 531 } 532 533 /** 534 * Returns the current state of the fast scroll feature. 535 * 536 * @return True if smooth scrollbar is enabled is enabled, false otherwise. 537 * 538 * @see #setSmoothScrollbarEnabled(boolean) 539 */ isSmoothScrollbarEnabled()540 public boolean isSmoothScrollbarEnabled() { 541 return mSmoothScrollbarEnabled; 542 } 543 544 545 /** 546 * Return the child view specified by the coordinates if 547 * there exists a child there. 548 * 549 * @return the child in this StaggeredGridView at the coordinates, null otherwise. 550 */ getChildAtCoordinate(int x, int y)551 private View getChildAtCoordinate(int x, int y) { 552 if (y < 0) { 553 // TODO: If we've dragged off the screen, return null for now until we know what 554 // we'd like the experience to be like. 555 return null; 556 } 557 558 final Rect frame = new Rect(); 559 final int count = getChildCount(); 560 for (int i = 0; i < count; i++) { 561 562 final View childView = getChildAt(i); 563 childView.getHitRect(frame); 564 if (frame.contains(x, y)) { 565 return getChildAt(i); 566 } 567 } 568 569 // No child view at this coordinate. 570 return null; 571 } 572 573 /** 574 * Get the last Y coordinate on this grid where the last touch was made 575 */ getLastTouchY()576 public float getLastTouchY() { 577 return mLastTouchY; 578 } 579 580 /** 581 * Enable drag reordering of child items. 582 */ enableDragReordering()583 public void enableDragReordering() { 584 mIsDragReorderingEnabled = true; 585 } 586 587 /** 588 * Disable drag reordering of child items. 589 */ disableDragReordering()590 public void disableDragReordering() { 591 mIsDragReorderingEnabled = false; 592 } 593 594 /** 595 * Check to see if drag reordering is supported. The switch must be flipped to true, and there 596 * must be a {@link ReorderListener} registered to listen for reordering events. 597 * 598 * @return boolean indicating whether drag reordering is currently supported. 599 */ isDragReorderingSupported()600 private boolean isDragReorderingSupported() { 601 return mIsDragReorderingEnabled && mReorderHelper != null && 602 mReorderHelper.hasReorderListener(); 603 } 604 605 /** 606 * Calculate bounds to assist in scrolling during a drag 607 * @param y The y coordinate of the current drag. 608 */ initializeDragScrollParameters(int y)609 private void initializeDragScrollParameters(int y) { 610 // Calculate the upper and lower bound of the screen to support drag scrolling 611 mHeight = getHeight(); 612 mUpperScrollBound = Math.min(y - mTouchSlop, mHeight / 5); 613 mLowerScrollBound = Math.max(y + mTouchSlop, mHeight * 4 / 5); 614 } 615 616 /** 617 * Initiate the dragging process. Create a bitmap that is displayed as the dragging event 618 * happens and is moved around across the screen. This function is called once for each time 619 * that a dragging event is initiated. 620 * 621 * The logic to this method was borrowed from the TouchInterceptor.java class from the 622 * music app. 623 * 624 * @param draggedChild The child view being dragged 625 * @param x The x coordinate of this view where dragging began 626 * @param y The y coordinate of this view where dragging began 627 */ startDragging(final View draggedChild, final int x, final int y)628 private void startDragging(final View draggedChild, final int x, final int y) { 629 if (!isDragReorderingSupported()) { 630 return; 631 } 632 633 mDragBitmap = createDraggedChildBitmap(draggedChild); 634 if (mDragBitmap == null) { 635 // It appears that creating bitmaps for large views fail. For now, don't allow 636 // dragging in this scenario. When using the framework's drag and drop implementation, 637 // drag shadow also fails with a OutofResourceException when trying to draw the drag 638 // shadow onto a Surface. 639 mReorderHelper.handleDragCancelled(draggedChild); 640 return; 641 } 642 mTouchOffsetToChildLeft = x - draggedChild.getLeft(); 643 mTouchOffsetToChildTop = y - draggedChild.getTop(); 644 updateReorderStates(ReorderUtils.DRAG_STATE_DRAGGING); 645 646 initializeDragScrollParameters(y); 647 648 final LayoutParams params = (LayoutParams) draggedChild.getLayoutParams(); 649 mReorderHelper.handleDragStart(draggedChild, params.position, params.id, 650 new Point(mTouchDownForDragStartX, mTouchDownForDragStartY)); 651 652 // TODO: Reconsider using the framework's DragShadow support for dragging, 653 // and only draw the bitmap in onDrop for animation. 654 final Context context = getContext(); 655 mDragView = new ImageView(context); 656 mDragView.setImageBitmap(mDragBitmap); 657 mDragView.setAlpha(160); 658 659 mWindowParams = new WindowManager.LayoutParams(); 660 mWindowParams.gravity = Gravity.TOP | Gravity.START; 661 662 mWindowParams.height = WindowManager.LayoutParams.WRAP_CONTENT; 663 mWindowParams.width = WindowManager.LayoutParams.WRAP_CONTENT; 664 mWindowParams.flags = mWindowManagerLayoutFlags; 665 mWindowParams.format = PixelFormat.TRANSLUCENT; 666 // Use WindowManager to overlay a transparent image on drag 667 mWindowManager.addView(mDragView, mWindowParams); 668 updateDraggedBitmapLocation(x, y); 669 } 670 createDraggedChildBitmap(View view)671 private Bitmap createDraggedChildBitmap(View view) { 672 view.setDrawingCacheEnabled(true); 673 final Bitmap cache = view.getDrawingCache(); 674 675 Bitmap bitmap = null; 676 if (cache != null) { 677 try { 678 bitmap = cache.copy(Bitmap.Config.ARGB_8888, false); 679 } catch (final OutOfMemoryError e) { 680 Log.w(TAG, "Failed to copy bitmap from Drawing cache", e); 681 bitmap = null; 682 } 683 } 684 685 view.destroyDrawingCache(); 686 view.setDrawingCacheEnabled(false); 687 688 return bitmap; 689 } 690 691 /** 692 * Updates the current drag state and the UI appropriately. 693 * @param state the new drag state to update to. 694 */ updateReorderStates(int state)695 private void updateReorderStates(int state) throws IllegalStateException { 696 boolean resetDraggedChildView = false; 697 boolean resetDragProperties = false; 698 699 mDragState = state; 700 701 switch (state) { 702 case ReorderUtils.DRAG_STATE_NONE: 703 case ReorderUtils.DRAG_STATE_DRAGGING: 704 // reset all states when a drag is complete or when we're starting a new drag. 705 resetDraggedChildView = true; 706 resetDragProperties = true; 707 break; 708 709 case ReorderUtils.DRAG_STATE_RELEASED_REORDER: 710 // In a release over a valid reordering zone, don't reset any UI. Let 711 // LayoutChildren() take care of doing the appropriate animation 712 // based on the result 713 break; 714 715 case ReorderUtils.DRAG_STATE_RELEASED_HOVER: 716 // When a dragged child is released over another child, the dragged child will 717 // remain hidden. It is up to the ReorderListener to refresh the UI state 718 // of the child if it does not handle the drop. 719 resetDragProperties = true; 720 break; 721 722 default: 723 throw new IllegalStateException("Illegal drag state: " + mDragState); 724 } 725 726 if (resetDraggedChildView && mReorderHelper.getDraggedChild() != null) { 727 // DraggedChildId and mCachedDragViewRect need to stay around longer than 728 // the other properties because on the next data change, as we lay out, we'll need 729 // mCachedDragViewRect to position the view's animation start position, and 730 // draggedChildId to check if the current was the dragged view. 731 // For the other properties - DraggedOverChildView, DraggedChildView, etc., 732 // as soon as drag is released, we can reset them because they have no impact on the 733 // next layout pass. 734 mReorderHelper.clearDraggedChildId(); 735 mCachedDragViewRect = null; 736 } 737 738 if (resetDragProperties) { 739 if (mDragView != null) { 740 mDragView.setVisibility(INVISIBLE); 741 mWindowManager.removeView(mDragView); 742 mDragView.setImageDrawable(null); 743 mDragView = null; 744 745 if (mDragBitmap != null) { 746 mDragBitmap.recycle(); 747 mDragBitmap = null; 748 } 749 } 750 751 // We don't reset DraggedChildId here because it may still be in used. 752 // Let LayoutChildren reset it when it's done with it. 753 mReorderHelper.clearDraggedChild(); 754 mReorderHelper.clearDraggedOverChild(); 755 } 756 } 757 758 /** 759 * Redraw the dragged child's bitmap based on the new coordinates. If the reordering direction 760 * is {@link ReorderUtils#REORDER_DIRECTION_VERTICAL}, then ignore the x coordinate, as 761 * only vertical movement is allowed. Similarly, if reordering direction is 762 * {@link ReorderUtils#REORDER_DIRECTION_HORIZONTAL}. Even though this class does not manage 763 * drag shadow directly, we need to make sure we position the dragged bitmap at where the 764 * drag shadow is so that when drag ends, we can swap the shadow and the bitmap to animate 765 * the view into place. 766 * @param x The updated x coordinate of the drag shadow. 767 * @param y THe updated y coordinate of the drag shadow. 768 */ updateDraggedBitmapLocation(int x, int y)769 private void updateDraggedBitmapLocation(int x, int y) { 770 final int direction = mAdapter.getReorderingDirection(); 771 if ((direction & ReorderUtils.REORDER_DIRECTION_HORIZONTAL) == 772 ReorderUtils.REORDER_DIRECTION_HORIZONTAL) { 773 if (mDragBitmap != null && mDragBitmap.getWidth() > getWidth()) { 774 // If the bitmap is wider than the width of the screen, then some parts of the view 775 // are off screen. In this case, just set the drag shadow to start at x = 0 776 // (adjusted to the absolute position on screen) so that at least the beginning of 777 // the drag shadow is guaranteed to be within view. 778 mWindowParams.x = mOffsetToAbsoluteX; 779 } else { 780 // WindowParams is RTL agnostic and operates on raw coordinates. So in an RTL 781 // layout, we would still want to find the view's left coordinate for the 782 // drag shadow, rather than the view's start. 783 mWindowParams.x = x - mTouchOffsetToChildLeft + mOffsetToAbsoluteX; 784 } 785 } else { 786 mWindowParams.x = mOffsetToAbsoluteX; 787 } 788 789 if ((direction & ReorderUtils.REORDER_DIRECTION_VERTICAL) == 790 ReorderUtils.REORDER_DIRECTION_VERTICAL) { 791 mWindowParams.y = y - mTouchOffsetToChildTop + mOffsetToAbsoluteY; 792 } else { 793 mWindowParams.y = mOffsetToAbsoluteY; 794 } 795 796 mWindowManager.updateViewLayout(mDragView, mWindowParams); 797 } 798 799 /** 800 * Update the visual state of the drag event based on the current drag location. If the user 801 * has attempted to re-order by dragging a child over another child's drop zone, call the 802 * appropriate {@link ReorderListener} callback. 803 * 804 * @param x The current x coordinate of the drag event 805 * @param y The current y coordinate of the drag event 806 */ handleDrag(int x, int y)807 private void handleDrag(int x, int y) { 808 if (mDragState != ReorderUtils.DRAG_STATE_DRAGGING) { 809 return; 810 } 811 812 // TODO: Consider moving drag shadow management logic into mReorderHelper as well, or 813 // scrap the custom logic and use the framework's drag-and-drop support now that we're not 814 // doing anything special to the drag shadow. 815 updateDraggedBitmapLocation(x, y); 816 817 if (mCurrentRunningAnimatorSet == null) { 818 // If the current animator set is not null, then animation is running, in which case, 819 // we shouldn't do any reordering processing, as views will be moving around, and 820 // interfering with drag target calculations. 821 mReorderHelper.handleDrag(new Point(x, y)); 822 } 823 } 824 825 /** 826 * Check if a view is reorderable. 827 * @param i the child index in view group 828 */ isChildReorderable(int i)829 public boolean isChildReorderable(int i) { 830 return mAdapter.isDraggable(mFirstPosition + i); 831 } 832 833 /** 834 * Handle the the release of a dragged view. 835 * @param x The current x coordinate where the drag was released. 836 * @param y The current y coordinate where the drag was released. 837 */ handleDrop(int x, int y)838 private void handleDrop(int x, int y) { 839 if (!mReorderHelper.hasReorderListener()) { 840 updateReorderStates(ReorderUtils.DRAG_STATE_NONE); 841 return; 842 } 843 844 if (mReorderHelper.isOverReorderingArea()) { 845 // Store the location of the drag shadow at where dragging stopped 846 // for animation if a reordering has just happened. Since the drag 847 // shadow is drawn as a WindowManager view, its coordinates are 848 // absolute. However, for views inside the grid, we need to operate 849 // with coordinate values that's relative to this grid, so we need 850 // to subtract the offset to absolute screen coordinates that have 851 // been added to mWindowParams. 852 final int left = mWindowParams.x - mOffsetToAbsoluteX; 853 final int top = mWindowParams.y - mOffsetToAbsoluteY; 854 855 mCachedDragViewRect = new Rect( 856 left, top, left + mDragView.getWidth(), top + mDragView.getHeight()); 857 if (getChildCount() > 0) { 858 final View view = getChildAt(0); 859 final LayoutParams lp = (LayoutParams) view.getLayoutParams(); 860 if (lp.position > mReorderHelper.getDraggedChildPosition()) { 861 // If the adapter position of the first child in view is 862 // greater than the position of the original dragged child, 863 // this means that the user has scrolled the child out of 864 // view. Those off screen views would have been recycled. If 865 // mFirstPosition is currently x, after the reordering 866 // operation, the child[mFirstPosition] will be 867 // at mFirstPosition-1. We want to adjust mFirstPosition so 868 // that we render the view in the correct location after 869 // reordering completes. 870 // 871 // If the user has not scrolled the original dragged child 872 // out of view, then the view has not been recycled and is 873 // still in view. 874 // When onLayout() gets called, we'll automatically fill in 875 // the empty space that the child leaves behind from the 876 // reordering operation. 877 mFirstPosition--; 878 } 879 } 880 881 // Get the current scroll position so that after reordering 882 // completes, we can restore the scroll position of mFirstPosition. 883 mCurrentScrollState = getScrollState(); 884 } 885 886 final boolean reordered = mReorderHelper.handleDrop(new Point(x, y)); 887 if (reordered) { 888 updateReorderStates(ReorderUtils.DRAG_STATE_RELEASED_REORDER); 889 } else { 890 updateReorderStates(ReorderUtils.DRAG_STATE_NONE); 891 } 892 } 893 894 @Override onInterceptTouchEvent(MotionEvent ev)895 public boolean onInterceptTouchEvent(MotionEvent ev) { 896 mVelocityTracker.addMovement(ev); 897 final int action = ev.getAction() & MotionEventCompat.ACTION_MASK; 898 switch (action) { 899 case MotionEvent.ACTION_DOWN: { 900 mOffsetToAbsoluteX = (int)(ev.getRawX() - ev.getX()); 901 mOffsetToAbsoluteY = (int)(ev.getRawY() - ev.getY()); 902 903 // Per bug 7377413, event.getX() and getY() returns rawX and rawY when accessed in 904 // dispatchDragEvent, so since an action down is required before a drag can be 905 // initiated, initialize mTouchDownForDragStartX/Y here for the most accurate value. 906 mTouchDownForDragStartX = (int) ev.getX(); 907 mTouchDownForDragStartY = (int) ev.getY(); 908 909 mVelocityTracker.clear(); 910 mScroller.abortAnimation(); 911 mLastTouchY = ev.getY(); 912 mActivePointerId = MotionEventCompat.getPointerId(ev, 0); 913 mTouchRemainderY = 0; 914 if (mTouchMode == TOUCH_MODE_FLINGING) { 915 // Catch! 916 mTouchMode = TOUCH_MODE_DRAGGING; 917 return true; 918 } 919 break; 920 } 921 case MotionEvent.ACTION_MOVE: { 922 final int index = MotionEventCompat.findPointerIndex(ev, mActivePointerId); 923 if (index < 0) { 924 Log.e(TAG, "onInterceptTouchEvent could not find pointer with id " + 925 mActivePointerId + " - did StaggeredGridView receive an inconsistent " + 926 "event stream?"); 927 return false; 928 } 929 final float y = MotionEventCompat.getY(ev, index); 930 final float dy = y - mLastTouchY + mTouchRemainderY; 931 final int deltaY = (int) dy; 932 mTouchRemainderY = dy - deltaY; 933 934 if (Math.abs(dy) > mTouchSlop) { 935 mTouchMode = TOUCH_MODE_DRAGGING; 936 return true; 937 } 938 } 939 } 940 941 return false; 942 } 943 944 @Override onTouchEvent(MotionEvent ev)945 public boolean onTouchEvent(MotionEvent ev) { 946 mVelocityTracker.addMovement(ev); 947 final int action = ev.getAction() & MotionEventCompat.ACTION_MASK; 948 switch (action) { 949 case MotionEvent.ACTION_DOWN: 950 resetScroller(); 951 mVelocityTracker.clear(); 952 mScroller.abortAnimation(); 953 mLastTouchY = ev.getY(); 954 mActivePointerId = MotionEventCompat.getPointerId(ev, 0); 955 mTouchRemainderY = 0; 956 break; 957 958 case MotionEvent.ACTION_MOVE: { 959 final int index = MotionEventCompat.findPointerIndex(ev, mActivePointerId); 960 if (index < 0) { 961 Log.e(TAG, "onInterceptTouchEvent could not find pointer with id " + 962 mActivePointerId + " - did StaggeredGridView receive an inconsistent " + 963 "event stream?"); 964 return false; 965 } 966 967 final float y = MotionEventCompat.getY(ev, index); 968 final float dy = y - mLastTouchY + mTouchRemainderY; 969 final int deltaY = (int) dy; 970 mTouchRemainderY = dy - deltaY; 971 972 if (Math.abs(dy) > mTouchSlop) { 973 mTouchMode = TOUCH_MODE_DRAGGING; 974 } 975 976 if (mTouchMode == TOUCH_MODE_DRAGGING) { 977 mLastTouchY = y; 978 if (!trackMotionScroll(deltaY, true)) { 979 // Break fling velocity if we impacted an edge. 980 mVelocityTracker.clear(); 981 } 982 } 983 break; 984 } 985 986 case MotionEvent.ACTION_CANCEL: { 987 mTouchMode = TOUCH_MODE_IDLE; 988 break; 989 } 990 991 case MotionEvent.ACTION_UP: { 992 mVelocityTracker.computeCurrentVelocity(1000, mMaximumVelocity); 993 final float velocity = VelocityTrackerCompat.getYVelocity(mVelocityTracker, 994 mActivePointerId); 995 if (Math.abs(velocity) > mFlingVelocity) { 996 mTouchMode = TOUCH_MODE_FLINGING; 997 resetScroller(); 998 mScroller.fling(0, 0, 0, (int) velocity, 0, 0, 999 Integer.MIN_VALUE, Integer.MAX_VALUE); 1000 mLastTouchY = 0; 1001 ViewCompat.postInvalidateOnAnimation(this); 1002 } else { 1003 mTouchMode = TOUCH_MODE_IDLE; 1004 } 1005 } 1006 break; 1007 } 1008 1009 return true; 1010 } 1011 resetScroller()1012 private void resetScroller() { 1013 mTouchMode = TOUCH_MODE_IDLE; 1014 mTopEdge.finish(); 1015 mBottomEdge.finish(); 1016 mScroller.abortAnimation(); 1017 } 1018 1019 @Override dispatchDragEvent(DragEvent event)1020 public boolean dispatchDragEvent(DragEvent event) { 1021 if (!isDragReorderingSupported()) { 1022 // If the consumer of this StaggeredGridView has not registered a ReorderListener, 1023 // don't bother handling drag events. 1024 return super.dispatchDragEvent(event); 1025 } 1026 1027 switch(event.getAction()) { 1028 case DragEvent.ACTION_DRAG_STARTED: 1029 // Per bug 7071594, we won't be able to catch this event in onDragEvent, 1030 // so we'll handle the event as it is being dispatched on the way down. 1031 if (mReorderHelper.hasReorderListener() && mIsDragReorderingEnabled) { 1032 final View child = getChildAtCoordinate( 1033 mTouchDownForDragStartX, mTouchDownForDragStartY); 1034 if (child != null) { 1035 // Child can be null if the touch point is not on a child view, but is 1036 // still within the bounds of this StaggeredGridView (i.e., margins 1037 // between cells). 1038 startDragging(child, mTouchDownForDragStartX, mTouchDownForDragStartY); 1039 // We must return true in order to continue getting future 1040 // {@link DragEvent}s. 1041 return true; 1042 } 1043 } 1044 // Be sure to return a value here instead of calling super.dispatchDragEvent() 1045 // which will unnecessarily dispatch to all the children (since the 1046 // {@link StaggeredGridView} handles all drag events for our purposes) 1047 return false; 1048 1049 case DragEvent.ACTION_DROP: 1050 case DragEvent.ACTION_DRAG_ENDED: 1051 if (mDragState == ReorderUtils.DRAG_STATE_DRAGGING) { 1052 handleDrop((int)event.getX(), (int)event.getY()); 1053 } 1054 1055 // Return early here to avoid calling super.dispatchDragEvent() which dispatches to 1056 // children (since this view already can handle all drag events). The super call 1057 // can also cause a NPE if the view hierarchy changed in the middle of a drag 1058 // and the {@link DragEvent} gets nulled out. This is a workaround for 1059 // a framework bug: 8298439. 1060 // Since the {@link StaggeredGridView} handles all drag events for our purposes, 1061 // just manually fire the drag event to ourselves. 1062 return onDragEvent(event); 1063 } 1064 1065 // In all other cases, default to the superclass implementation. We need this so that 1066 // the drag/drop framework will fire off {@link #onDragEvent(DragEvent ev)} calls to us. 1067 return super.dispatchDragEvent(event); 1068 } 1069 1070 @Override onDragEvent(DragEvent ev)1071 public boolean onDragEvent(DragEvent ev) { 1072 if (!isDragReorderingSupported()) { 1073 // If the consumer of this StaggeredGridView has not registered a ReorderListener, 1074 // don't bother handling drag events. 1075 return false; 1076 } 1077 1078 final int x = (int)ev.getX(); 1079 final int y = (int)ev.getY(); 1080 1081 switch(ev.getAction()) { 1082 case DragEvent.ACTION_DRAG_LOCATION: 1083 if (mDragState == ReorderUtils.DRAG_STATE_DRAGGING) { 1084 handleDrag(x, y); 1085 mLastTouchY = y; 1086 } 1087 1088 // Kick off the scroll handler on the first drag location event, 1089 // if it's not already running 1090 if (!mIsDragScrollerRunning && 1091 // And if the distance traveled while dragging exceeds the touch slop 1092 ((Math.abs(x - mTouchDownForDragStartX) >= 4 * mTouchSlop) || 1093 (Math.abs(y - mTouchDownForDragStartY) >= 4 * mTouchSlop))) { 1094 // Set true because that the scroller is running now 1095 mIsDragScrollerRunning = true; 1096 1097 if (mScrollHandler == null) { 1098 mScrollHandler = getHandler(); 1099 } 1100 mScrollHandler.postDelayed(mDragScroller, SCROLL_HANDLER_DELAY); 1101 } 1102 1103 return true; 1104 1105 case DragEvent.ACTION_DROP: 1106 case DragEvent.ACTION_DRAG_ENDED: 1107 // We can either expect to receive: 1108 // 1. Both {@link DragEvent#ACTION_DROP} and then 1109 // {@link DragEvent#ACTION_DRAG_ENDED} if the drop is over this view. 1110 // 2. Only {@link DragEvent#ACTION_DRAG_ENDED} if the drop happened over a 1111 // different view. 1112 // For this reason, we should always handle the drop. In case #1, if this code path 1113 // gets executed again then nothing will happen because we will have already 1114 // updated {@link #mDragState} to not be {@link ReorderUtils#DRAG_STATE_DRAGGING}. 1115 if (mScrollHandler != null) { 1116 mScrollHandler.removeCallbacks(mDragScroller); 1117 // Scroller is no longer running 1118 mIsDragScrollerRunning = false; 1119 } 1120 1121 return true; 1122 } 1123 1124 return false; 1125 } 1126 1127 /** 1128 * 1129 * @param deltaY Pixels that content should move by 1130 * @return true if the movement completed, false if it was stopped prematurely. 1131 */ trackMotionScroll(int deltaY, boolean allowOverScroll)1132 private boolean trackMotionScroll(int deltaY, boolean allowOverScroll) { 1133 final boolean contentFits = contentFits(); 1134 final int allowOverhang = Math.abs(deltaY); 1135 final int overScrolledBy; 1136 final int movedBy; 1137 if (!contentFits) { 1138 int overhang; 1139 final boolean up; 1140 mPopulating = true; 1141 if (deltaY > 0) { 1142 overhang = fillUp(mFirstPosition - 1, allowOverhang); 1143 up = true; 1144 } else { 1145 overhang = fillDown(mFirstPosition + getChildCount(), allowOverhang); 1146 1147 if (overhang < 0) { 1148 // Overhang when filling down indicates how many pixels past the bottom of the 1149 // screen has been filled in. If this value is negative, it should be set to 1150 // 0 so that we don't allow over scrolling. 1151 overhang = 0; 1152 } 1153 1154 up = false; 1155 } 1156 1157 movedBy = Math.min(overhang, allowOverhang); 1158 offsetChildren(up ? movedBy : -movedBy); 1159 recycleOffscreenViews(); 1160 mPopulating = false; 1161 overScrolledBy = allowOverhang - overhang; 1162 } else { 1163 overScrolledBy = allowOverhang; 1164 movedBy = 0; 1165 } 1166 1167 if (allowOverScroll) { 1168 final int overScrollMode = ViewCompat.getOverScrollMode(this); 1169 1170 if (overScrollMode == ViewCompat.OVER_SCROLL_ALWAYS || 1171 (overScrollMode == ViewCompat.OVER_SCROLL_IF_CONTENT_SCROLLS && !contentFits)) { 1172 1173 if (overScrolledBy > 0) { 1174 final EdgeEffectCompat edge = deltaY > 0 ? mTopEdge : mBottomEdge; 1175 edge.onPull((float) Math.abs(deltaY) / getHeight()); 1176 ViewCompat.postInvalidateOnAnimation(this); 1177 } 1178 } 1179 } 1180 1181 awakenScrollBars(0 /* show immediately */, true /* invalidate */); 1182 return deltaY == 0 || movedBy != 0; 1183 } 1184 contentFits()1185 public final boolean contentFits() { 1186 if (mFirstPosition != 0 || getChildCount() != mItemCount) { 1187 return false; 1188 } 1189 1190 int topmost = Integer.MAX_VALUE; 1191 int bottommost = Integer.MIN_VALUE; 1192 for (int i = 0; i < mColCount; i++) { 1193 if (mItemTops[i] < topmost) { 1194 topmost = mItemTops[i]; 1195 } 1196 if (mItemBottoms[i] > bottommost) { 1197 bottommost = mItemBottoms[i]; 1198 } 1199 } 1200 1201 return topmost >= getPaddingTop() && bottommost <= getHeight() - getPaddingBottom(); 1202 } 1203 1204 /** 1205 * Recycle views within the range starting from startIndex (inclusive) until the last 1206 * attached child view. 1207 */ recycleViewsInRange(int startIndex, int endIndex)1208 private void recycleViewsInRange(int startIndex, int endIndex) { 1209 for (int i = endIndex; i >= startIndex; i--) { 1210 final View child = getChildAt(i); 1211 1212 if (mInLayout) { 1213 removeViewsInLayout(i, 1); 1214 } else { 1215 removeViewAt(i); 1216 } 1217 1218 mRecycler.addScrap(child); 1219 } 1220 } 1221 1222 // TODO: Have other overloaded recycle methods call into this one so we would just have one 1223 // code path. recycleView(View view)1224 private void recycleView(View view) { 1225 if (view == null) { 1226 return; 1227 } 1228 1229 if (mInLayout) { 1230 removeViewInLayout(view); 1231 invalidate(); 1232 } else { 1233 removeView(view); 1234 } 1235 1236 mRecycler.addScrap(view); 1237 } 1238 1239 /** 1240 * Important: this method will leave offscreen views attached if they 1241 * are required to maintain the invariant that child view with index i 1242 * is always the view corresponding to position mFirstPosition + i. 1243 */ recycleOffscreenViews()1244 private void recycleOffscreenViews() { 1245 if (getChildCount() == 0) { 1246 return; 1247 } 1248 1249 final int height = getHeight(); 1250 final int clearAbove = -mItemMargin; 1251 final int clearBelow = height + mItemMargin; 1252 for (int i = getChildCount() - 1; i >= 0; i--) { 1253 final View child = getChildAt(i); 1254 if (child.getTop() <= clearBelow) { 1255 // There may be other offscreen views, but we need to maintain 1256 // the invariant documented above. 1257 break; 1258 } 1259 1260 child.clearFocus(); 1261 if (mInLayout) { 1262 removeViewsInLayout(i, 1); 1263 } else { 1264 removeViewAt(i); 1265 } 1266 1267 mRecycler.addScrap(child); 1268 } 1269 1270 while (getChildCount() > 0) { 1271 final View child = getChildAt(0); 1272 if (child.getBottom() >= clearAbove) { 1273 // There may be other offscreen views, but we need to maintain 1274 // the invariant documented above. 1275 break; 1276 } 1277 1278 child.clearFocus(); 1279 if (mInLayout) { 1280 removeViewsInLayout(0, 1); 1281 } else { 1282 removeViewAt(0); 1283 } 1284 1285 mRecycler.addScrap(child); 1286 mFirstPosition++; 1287 } 1288 1289 final int childCount = getChildCount(); 1290 if (childCount > 0) { 1291 // Repair the top and bottom column boundaries from the views we still have 1292 Arrays.fill(mItemTops, Integer.MAX_VALUE); 1293 Arrays.fill(mItemBottoms, Integer.MIN_VALUE); 1294 for (int i = 0; i < childCount; i++){ 1295 final View child = getChildAt(i); 1296 final LayoutParams lp = (LayoutParams) child.getLayoutParams(); 1297 final int top = child.getTop() - mItemMargin; 1298 final int bottom = child.getBottom(); 1299 LayoutRecord rec = mLayoutRecords.get(mFirstPosition + i); 1300 1301 // It's possible the layout record could be null for visible views because 1302 // they are cleared between adapter data set changes, but the views are left 1303 // attached for the purpose of animations. Hence, populate the layout record again. 1304 if (rec == null) { 1305 rec = recreateLayoutRecord(mFirstPosition + i, child, lp); 1306 } 1307 1308 // In LTR layout, iterate across each column that this child is laid out in, 1309 // starting from the child's first column (lp.column). For each column, update 1310 // mItemTops and mItemBottoms appropriately to take into account this child's 1311 // dimension. In RTL layout, iterate in reverse, where the child's starting 1312 // column would start from the right-most. 1313 final int span = Math.min(mColCount, lp.span); 1314 for (int spanIndex = 0; spanIndex < span; spanIndex++) { 1315 final int col = mIsRtlLayout ? lp.column - spanIndex : 1316 lp.column + spanIndex; 1317 final int colTop = top - rec.getMarginAbove(spanIndex); 1318 final int colBottom = bottom + rec.getMarginBelow(spanIndex); 1319 if (colTop < mItemTops[col]) { 1320 mItemTops[col] = colTop; 1321 } 1322 if (colBottom > mItemBottoms[col]) { 1323 mItemBottoms[col] = colBottom; 1324 } 1325 } 1326 } 1327 1328 for (int col = 0; col < mColCount; col++) { 1329 if (mItemTops[col] == Integer.MAX_VALUE) { 1330 // If one was untouched, both were. 1331 final int top = getPaddingTop(); 1332 mItemTops[col] = top; 1333 mItemBottoms[col] = top; 1334 } 1335 } 1336 } 1337 1338 mCurrentScrollState = getScrollState(); 1339 } 1340 recreateLayoutRecord(int position, View child, LayoutParams lp)1341 private LayoutRecord recreateLayoutRecord(int position, View child, LayoutParams lp) { 1342 final LayoutRecord rec = new LayoutRecord(); 1343 mLayoutRecords.put(position, rec); 1344 rec.column = lp.column; 1345 rec.height = child.getHeight(); 1346 rec.id = lp.id; 1347 rec.span = Math.min(mColCount, lp.span); 1348 return rec; 1349 } 1350 1351 @Override computeScroll()1352 public void computeScroll() { 1353 if (mTouchMode == TOUCH_MODE_OVERFLING) { 1354 handleOverfling(); 1355 } else if (mScroller.computeScrollOffset()) { 1356 final int overScrollMode = ViewCompat.getOverScrollMode(this); 1357 final boolean supportsOverscroll = overScrollMode != ViewCompat.OVER_SCROLL_NEVER; 1358 final int y = mScroller.getCurrY(); 1359 final int dy = (int) (y - mLastTouchY); 1360 // TODO: Figure out why mLastTouchY is being updated here. Consider using a new class 1361 // variable since this value does not represent the last place on the screen where a 1362 // touch occurred. 1363 mLastTouchY = y; 1364 // Check if the top of the motion view is where it is 1365 // supposed to be 1366 final View motionView = supportsOverscroll && 1367 getChildCount() > 0 ? getChildAt(0) : null; 1368 final int motionViewPrevTop = motionView != null ? motionView.getTop() : 0; 1369 final boolean stopped = !trackMotionScroll(dy, false); 1370 if (!stopped && !mScroller.isFinished()) { 1371 mTouchMode = TOUCH_MODE_IDLE; 1372 ViewCompat.postInvalidateOnAnimation(this); 1373 } else if (stopped && dy != 0 && supportsOverscroll) { 1374 // Check to see if we have bumped into the scroll limit 1375 if (motionView != null) { 1376 final int motionViewRealTop = motionView.getTop(); 1377 // Apply overscroll 1378 final int overscroll = -dy - (motionViewRealTop - motionViewPrevTop); 1379 overScrollBy(0, overscroll, 0, getScrollY(), 0, 0, 0, mOverscrollDistance, 1380 true); 1381 } 1382 final EdgeEffectCompat edge; 1383 if (dy > 0) { 1384 edge = mTopEdge; 1385 mBottomEdge.finish(); 1386 } else { 1387 edge = mBottomEdge; 1388 mTopEdge.finish(); 1389 } 1390 edge.onAbsorb(Math.abs((int) mScroller.getCurrVelocity())); 1391 if (mScroller.computeScrollOffset()) { 1392 mScroller.notifyVerticalEdgeReached(getScrollY(), 0, mOverscrollDistance); 1393 } 1394 mTouchMode = TOUCH_MODE_OVERFLING; 1395 ViewCompat.postInvalidateOnAnimation(this); 1396 } else { 1397 mTouchMode = TOUCH_MODE_IDLE; 1398 } 1399 } 1400 } 1401 handleOverfling()1402 private void handleOverfling() { 1403 // If the animation is not finished yet, determine next steps. 1404 if (mScroller.computeScrollOffset()) { 1405 final int scrollY = getScrollY(); 1406 final int currY = mScroller.getCurrY(); 1407 final int deltaY = currY - scrollY; 1408 if (overScrollBy(0, deltaY, 0, scrollY, 0, 0, 0, mOverscrollDistance, false)) { 1409 final boolean crossDown = scrollY <= 0 && currY > 0; 1410 final boolean crossUp = scrollY >= 0 && currY < 0; 1411 if (crossDown || crossUp) { 1412 int velocity = (int) mScroller.getCurrVelocity(); 1413 if (crossUp) { 1414 velocity = -velocity; 1415 } 1416 1417 // Don't flywheel from this; we're just continuing 1418 // things. 1419 mTouchMode = TOUCH_MODE_IDLE; 1420 mScroller.abortAnimation(); 1421 } else { 1422 // Spring back! We are done overscrolling. 1423 if (mScroller.springBack(0, scrollY, 0, 0, 0, 0)) { 1424 mTouchMode = TOUCH_MODE_OVERFLING; 1425 ViewCompat.postInvalidateOnAnimation(this); 1426 } else { 1427 // If already valid, we are done. Exit overfling mode. 1428 mTouchMode = TOUCH_MODE_IDLE; 1429 } 1430 } 1431 } else { 1432 // Still over-flinging; just post the next frame of the animation. 1433 ViewCompat.postInvalidateOnAnimation(this); 1434 } 1435 } else { 1436 // Otherwise, exit overfling mode. 1437 mTouchMode = TOUCH_MODE_IDLE; 1438 mScroller.abortAnimation(); 1439 } 1440 } 1441 1442 @Override 1443 protected void onOverScrolled(int scrollX, int scrollY, boolean clampedX, boolean clampedY) { 1444 if (getScrollY() != scrollY) { 1445 scrollTo(0, scrollY); 1446 } 1447 } 1448 1449 @Override 1450 public void draw(Canvas canvas) { 1451 super.draw(canvas); 1452 1453 if (mTopEdge != null) { 1454 boolean needsInvalidate = false; 1455 if (!mTopEdge.isFinished()) { 1456 final int restoreCount = canvas.save(); 1457 canvas.translate(0, 0); 1458 mTopEdge.draw(canvas); 1459 canvas.restoreToCount(restoreCount); 1460 needsInvalidate = true; 1461 } 1462 if (!mBottomEdge.isFinished()) { 1463 final int restoreCount = canvas.save(); 1464 final int width = getWidth(); 1465 canvas.translate(-width, getHeight()); 1466 canvas.rotate(180, width, 0); 1467 mBottomEdge.draw(canvas); 1468 canvas.restoreToCount(restoreCount); 1469 needsInvalidate = true; 1470 } 1471 1472 if (needsInvalidate) { 1473 ViewCompat.postInvalidateOnAnimation(this); 1474 } 1475 } 1476 } 1477 1478 public void beginFastChildLayout() { 1479 mFastChildLayout = true; 1480 } 1481 1482 public void endFastChildLayout() { 1483 mFastChildLayout = false; 1484 populate(); 1485 } 1486 1487 @Override 1488 public void requestLayout() { 1489 if (!mPopulating && !mFastChildLayout) { 1490 super.requestLayout(); 1491 } 1492 } 1493 1494 /** 1495 * Sets the view to show if the adapter is empty 1496 */ 1497 public void setEmptyView(View emptyView) { 1498 mEmptyView = emptyView; 1499 1500 updateEmptyStatus(); 1501 } 1502 1503 public View getEmptyView() { 1504 return mEmptyView; 1505 } 1506 1507 /** 1508 * Update the status of the list based on the whether the adapter is empty. If is it empty and 1509 * we have an empty view, display it. In all the other cases, make sure that the 1510 * StaggeredGridView is VISIBLE and that the empty view is GONE (if it's not null). 1511 */ 1512 private void updateEmptyStatus() { 1513 if (mAdapter == null || mAdapter.isEmpty()) { 1514 if (mEmptyView != null) { 1515 mEmptyView.setVisibility(View.VISIBLE); 1516 setVisibility(View.GONE); 1517 } else { 1518 setVisibility(View.VISIBLE); 1519 } 1520 } else { 1521 if (mEmptyView != null) { 1522 mEmptyView.setVisibility(View.GONE); 1523 } 1524 setVisibility(View.VISIBLE); 1525 } 1526 } 1527 1528 @Override 1529 protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { 1530 int widthMode = MeasureSpec.getMode(widthMeasureSpec); 1531 int heightMode = MeasureSpec.getMode(heightMeasureSpec); 1532 final int widthSize = MeasureSpec.getSize(widthMeasureSpec); 1533 final int heightSize = MeasureSpec.getSize(heightMeasureSpec); 1534 1535 if (widthMode != MeasureSpec.EXACTLY) { 1536 Log.d(TAG, "onMeasure: must have an exact width or match_parent! " + 1537 "Using fallback spec of EXACTLY " + widthSize); 1538 widthMode = MeasureSpec.EXACTLY; 1539 } 1540 if (heightMode != MeasureSpec.EXACTLY) { 1541 Log.d(TAG, "onMeasure: must have an exact height or match_parent! " + 1542 "Using fallback spec of EXACTLY " + heightSize); 1543 heightMode = MeasureSpec.EXACTLY; 1544 } 1545 1546 setMeasuredDimension(widthSize, heightSize); 1547 1548 if (mColCountSetting == COLUMN_COUNT_AUTO) { 1549 final int colCount = widthSize / mMinColWidth; 1550 if (colCount != mColCount) { 1551 mColCount = colCount; 1552 } 1553 } 1554 1555 if (mHorizontalReorderingAreaSize == 0) { 1556 if (mColCount > 1) { 1557 final int totalMarginWidth = mItemMargin * (mColCount + 1); 1558 final int singleViewWidth = (widthSize - totalMarginWidth) / mColCount; 1559 mHorizontalReorderingAreaSize = singleViewWidth / CHILD_TO_REORDER_AREA_RATIO; 1560 } else { 1561 mHorizontalReorderingAreaSize = SINGLE_COL_REORDERING_AREA_SIZE; 1562 } 1563 } 1564 } 1565 1566 @Override 1567 protected void onLayout(boolean changed, int l, int t, int r, int b) { 1568 mIsRtlLayout = isLayoutRtl(); 1569 1570 mInLayout = true; 1571 populate(); 1572 mInLayout = false; 1573 final int width = r - l; 1574 final int height = b - t; 1575 mTopEdge.setSize(width, height); 1576 mBottomEdge.setSize(width, height); 1577 } 1578 1579 private void populate() { 1580 if (getWidth() == 0 || getHeight() == 0 || mAdapter == null) { 1581 return; 1582 } 1583 1584 if (mColCount == COLUMN_COUNT_AUTO) { 1585 final int colCount = getWidth() / mMinColWidth; 1586 if (colCount != mColCount) { 1587 mColCount = colCount; 1588 } 1589 } 1590 1591 final int colCount = mColCount; 1592 if (mItemTops == null || mItemBottoms == null || mItemTops.length != colCount || 1593 mItemBottoms.length != colCount) { 1594 mItemTops = new int[colCount]; 1595 mItemBottoms = new int[colCount]; 1596 1597 mLayoutRecords.clear(); 1598 if (mInLayout) { 1599 removeAllViewsInLayout(); 1600 } else { 1601 removeAllViews(); 1602 } 1603 } 1604 1605 // Before we do layout, if there are any pending animations and data has changed, 1606 // cancel the animation, as layout on new data will likely trigger another animation 1607 // set to be run. 1608 if (mDataChanged && mCurrentRunningAnimatorSet != null) { 1609 mCurrentRunningAnimatorSet.cancel(); 1610 mCurrentRunningAnimatorSet = null; 1611 } 1612 1613 if (isSelectionAtTop()) { 1614 mCurrentScrollState = null; 1615 } 1616 1617 if (mCurrentScrollState != null) { 1618 restoreScrollPosition(mCurrentScrollState); 1619 } else { 1620 calculateLayoutStartOffsets(getPaddingTop() /* layout start offset */); 1621 } 1622 1623 mPopulating = true; 1624 1625 mFocusedChildIdToScrollIntoView = -1; 1626 final View focusedChild = getFocusedChild(); 1627 if (focusedChild != null) { 1628 final LayoutParams lp = (LayoutParams) focusedChild.getLayoutParams(); 1629 mFocusedChildIdToScrollIntoView = lp.id; 1630 } 1631 1632 layoutChildren(mDataChanged); 1633 fillDown(mFirstPosition + getChildCount(), 0); 1634 fillUp(mFirstPosition - 1, 0); 1635 1636 if (isDragReorderingSupported() && 1637 mDragState == ReorderUtils.DRAG_STATE_RELEASED_REORDER || 1638 mDragState == ReorderUtils.DRAG_STATE_RELEASED_HOVER) { 1639 // This child was dragged and dropped with the UI likely 1640 // still showing. Call updateReorderStates, to update 1641 // all UI appropriately. 1642 mReorderHelper.clearDraggedChildId(); 1643 updateReorderStates(ReorderUtils.DRAG_STATE_NONE); 1644 } 1645 1646 if (mDataChanged) { 1647 // Animation should only play if data has changed since populate() can be called 1648 // multiple times with the same data set (e.g., screen size changed). 1649 handleLayoutAnimation(); 1650 } 1651 1652 recycleOffscreenViews(); 1653 1654 mPopulating = false; 1655 mDataChanged = false; 1656 } 1657 1658 @Override 1659 public void scrollBy(int x, int y) { 1660 if (y != 0) { 1661 // TODO: Implement smooth scrolling for this so that scrolling does more than just 1662 // jumping by y pixels. 1663 trackMotionScroll(y, false /* over scroll */); 1664 } 1665 } 1666 1667 private void offsetChildren(int offset) { 1668 final int childCount = getChildCount(); 1669 for (int i = 0; i < childCount; i++) { 1670 final View child = getChildAt(i); 1671 1672 child.offsetTopAndBottom(offset); 1673 1674 // As we're scrolling, we need to make sure the children that are coming into view 1675 // have their reordering area set. 1676 final LayoutParams lp = (LayoutParams) child.getLayoutParams(); 1677 setReorderingArea(lp); 1678 } 1679 1680 final int colCount = mColCount; 1681 for (int i = 0; i < colCount; i++) { 1682 mItemTops[i] += offset; 1683 mItemBottoms[i] += offset; 1684 } 1685 1686 if (mScrollListener != null) { 1687 mScrollListener.onScrollChanged(offset, computeVerticalScrollOffset(), 1688 computeVerticalScrollRange()); 1689 } 1690 } 1691 1692 /** 1693 * Performs layout animation of child views. 1694 * @throws IllegalStateException Exception is thrown of currently set animation mode is 1695 * not recognized. 1696 */ 1697 private void handleLayoutAnimation() throws IllegalStateException { 1698 final List<Animator> animators = new ArrayList<Animator>(); 1699 1700 // b/8422632 - Without this dummy first animator, startDelays of subsequent animators won't 1701 // be honored correctly; all animators will block regardless of startDelay until the first 1702 // animator in the AnimatorSet truly starts playing. 1703 final ValueAnimator anim = ValueAnimator.ofFloat(0f, 1f); 1704 anim.setDuration(0); 1705 animators.add(anim); 1706 1707 addOutAnimatorsForStaleViews(animators, mAnimationOutMode); 1708 1709 // Play the In animators at a slight delay after all Out animators have started. 1710 final int animationInStartDelay = animators.size() > 0 ? 1711 (SgvAnimationHelper.getDefaultAnimationDuration() / 2) : 0; 1712 addInAnimators(animators, mAnimationInMode, animationInStartDelay); 1713 1714 if (animators != null && animators.size() > 0) { 1715 final AnimatorSet animatorSet = new AnimatorSet(); 1716 animatorSet.playTogether(animators); 1717 animatorSet.addListener(new AnimatorListenerAdapter() { 1718 @Override 1719 public void onAnimationStart(Animator animation) { 1720 mIsCurrentAnimationCanceled = false; 1721 mCurrentRunningAnimatorSet = animatorSet; 1722 } 1723 1724 @Override 1725 public void onAnimationCancel(Animator animation) { 1726 mIsCurrentAnimationCanceled = true; 1727 } 1728 1729 @Override 1730 public void onAnimationEnd(Animator animation) { 1731 if (!mIsCurrentAnimationCanceled) { 1732 // If this animation ended naturally, not because it was canceled, then 1733 // reset the animation mode back to ANIMATION_MODE_NONE. However, if 1734 // the animation was canceled by a data change, then keep the mode as is, 1735 // so that on a re-layout, we can resume animation from the views' current 1736 // positions. 1737 resetAnimationMode(); 1738 } 1739 mCurrentRunningAnimatorSet = null; 1740 } 1741 }); 1742 1743 Log.v(TAG, "starting"); 1744 animatorSet.start(); 1745 } else { 1746 resetAnimationMode(); 1747 } 1748 1749 mViewsToAnimateOut.clear(); 1750 mChildRectsForAnimation.clear(); 1751 } 1752 1753 /** 1754 * Reset the current animation mode. 1755 */ 1756 private void resetAnimationMode() { 1757 mAnimationInMode = AnimationIn.NONE; 1758 mAnimationOutMode = AnimationOut.NONE; 1759 } 1760 1761 /** 1762 * Add animators for animating in new views as well as updating positions of views that 1763 * should remain on screen. 1764 */ 1765 private void addInAnimators(List<Animator> animators, AnimationIn animationInMode, 1766 int startDelay) { 1767 if (animationInMode == AnimationIn.NONE) { 1768 return; 1769 } 1770 1771 switch (animationInMode) { 1772 case FLY_UP_ALL_VIEWS: 1773 addFlyInAllViewsAnimators(animators); 1774 break; 1775 1776 case EXPAND_NEW_VIEWS: 1777 addUpdateViewPositionsAnimators(animators, true /* cascade animation */, 1778 AnimationIn.EXPAND_NEW_VIEWS, startDelay); 1779 break; 1780 1781 case EXPAND_NEW_VIEWS_NO_CASCADE: 1782 addUpdateViewPositionsAnimators(animators, false /* cascade animation */, 1783 AnimationIn.EXPAND_NEW_VIEWS_NO_CASCADE, startDelay); 1784 break; 1785 1786 case SLIDE_IN_NEW_VIEWS: 1787 addUpdateViewPositionsAnimators(animators, true /* cascade animation */, 1788 AnimationIn.SLIDE_IN_NEW_VIEWS, startDelay); 1789 break; 1790 1791 case FLY_IN_NEW_VIEWS: 1792 addUpdateViewPositionsAnimators(animators, true /* cascade animation */, 1793 AnimationIn.FLY_IN_NEW_VIEWS, startDelay); 1794 break; 1795 1796 case FADE: 1797 addUpdateViewPositionsAnimators(animators, true /* cascade animation */, 1798 AnimationIn.FADE, startDelay); 1799 break; 1800 1801 default: 1802 throw new IllegalStateException("Unknown animationInMode: " + mAnimationInMode); 1803 } 1804 } 1805 1806 /** 1807 * Add animators for animating out stale views 1808 * @param animationOutMode The animation mode to play for stale views 1809 */ 1810 private void addOutAnimatorsForStaleViews(List<Animator> animators, 1811 AnimationOut animationOutMode) { 1812 if (animationOutMode == AnimationOut.NONE) { 1813 return; 1814 } 1815 1816 for (final View v : mViewsToAnimateOut) { 1817 // For each stale view to animate out, retrieve the animators for the view, then attach 1818 // the StaleViewAnimationEndListener which checks to see if the view should be recycled 1819 // at the end of the animation. 1820 final List<Animator> viewAnimators = new ArrayList<Animator>(); 1821 1822 switch (animationOutMode) { 1823 case SLIDE: 1824 final LayoutParams lp = (LayoutParams) v.getLayoutParams(); 1825 // Bias towards sliding right, but depending on the column that this view 1826 // is laid out in, slide towards the nearest side edge. 1827 int endTranslation = (int)(v.getWidth() * 1.5); 1828 if (lp.column < (mColCount / 2)) { 1829 endTranslation = -endTranslation; 1830 } 1831 SgvAnimationHelper.addSlideOutAnimators(viewAnimators, v, 1832 (int) v.getTranslationX(), endTranslation); 1833 break; 1834 1835 case COLLAPSE: 1836 SgvAnimationHelper.addCollapseOutAnimators(viewAnimators, v); 1837 break; 1838 1839 case FLY_DOWN: 1840 SgvAnimationHelper.addFlyOutAnimators(viewAnimators, v, 1841 (int) v.getTranslationY(), getHeight()); 1842 break; 1843 1844 case FADE: 1845 SgvAnimationHelper.addFadeAnimators(viewAnimators, v, v.getAlpha(), 1846 0 /* end alpha */); 1847 break; 1848 1849 default: 1850 throw new IllegalStateException("Unknown animationOutMode: " + 1851 animationOutMode); 1852 } 1853 1854 if (viewAnimators.size() > 0) { 1855 addStaleViewAnimationEndListener(v, viewAnimators); 1856 animators.addAll(viewAnimators); 1857 } 1858 } 1859 } 1860 1861 /** 1862 * Handle setting up the animators of child views when the animation is invoked by a change 1863 * in the adapter. This method has a side effect of translating view positions in preparation 1864 * for the animations. 1865 */ 1866 private List<Animator> addFlyInAllViewsAnimators(List<Animator> animators) { 1867 final int childCount = getChildCount(); 1868 if (childCount == 0) { 1869 return null; 1870 } 1871 1872 if (animators == null) { 1873 animators = new ArrayList<Animator>(); 1874 } 1875 1876 for (int i = 0; i < childCount; i++) { 1877 final int animationDelay = i * ANIMATION_DELAY_IN_MS; 1878 final View childToAnimate = getChildAt(i); 1879 1880 // Start all views from below the bottom of this grid and animate them upwards. This 1881 // is done simply by translating the current view's vertical position by the height 1882 // of the entire grid. 1883 float yTranslation = getHeight(); 1884 float rotation = SgvAnimationHelper.ANIMATION_ROTATION_DEGREES; 1885 if (mIsCurrentAnimationCanceled) { 1886 // If mIsAnimationCanceled is true, then this is not the first time that this 1887 // animation is running. For this particular case, we should resume from where 1888 // the previous animation left off, rather than resetting translation and rotation. 1889 yTranslation = childToAnimate.getTranslationY(); 1890 rotation = childToAnimate.getRotation(); 1891 } 1892 1893 SgvAnimationHelper.addTranslationRotationAnimators(animators, childToAnimate, 1894 0 /* xTranslation */, (int) yTranslation, rotation, animationDelay); 1895 } 1896 1897 return animators; 1898 } 1899 1900 /** 1901 * Animations to update the views on screen to their new positions. For new views that aren't 1902 * currently on screen, animate them in using the specified animationInMode. 1903 */ 1904 private List<Animator> addUpdateViewPositionsAnimators(List<Animator> animators, 1905 boolean cascadeAnimation, AnimationIn animationInMode, int startDelay) { 1906 final int childCount = getChildCount(); 1907 if (childCount == 0) { 1908 return null; 1909 } 1910 1911 if (animators == null) { 1912 animators = new ArrayList<Animator>(); 1913 } 1914 1915 int viewsAnimated = 0; 1916 for (int i = 0; i < childCount; i++) { 1917 final View childToAnimate = getChildAt(i); 1918 1919 if (mViewsToAnimateOut.contains(childToAnimate)) { 1920 // If the stale views are still animating, then they are still laid out, so 1921 // getChildCount() would've accounted for them. Since they have their own set 1922 // of animations to play, we'll skip over them in this loop. 1923 continue; 1924 } 1925 1926 // Use progressive animation delay to create the staggered effect of animating 1927 // views. This is done by having each view delay their animation by 1928 // ANIMATION_DELAY_IN_MS after the animation of the previous view. 1929 int animationDelay = startDelay + 1930 (cascadeAnimation ? viewsAnimated * ANIMATION_DELAY_IN_MS : 0); 1931 1932 // Figure out whether a view with this item ID existed before 1933 final LayoutParams lp = (LayoutParams) childToAnimate.getLayoutParams(); 1934 1935 final ViewRectPair viewRectPair = mChildRectsForAnimation.get(lp.id); 1936 1937 final int xTranslation; 1938 final int yTranslation; 1939 1940 // If there is a valid {@link Rect} for the view with this newId, then 1941 // setup an animation. 1942 if (viewRectPair != null && viewRectPair.rect != null) { 1943 // In the special case where the items are explicitly fading, we don't want to do 1944 // any of the translations. 1945 if (animationInMode == AnimationIn.FADE) { 1946 SgvAnimationHelper.addFadeAnimators(animators, childToAnimate, 1947 0 /* start alpha */, 1.0f /* end alpha */, animationDelay); 1948 continue; 1949 } 1950 1951 final Rect oldRect = viewRectPair.rect; 1952 // Since the view already exists, translate it to its new position. 1953 // Reset the child back to its previous position given by oldRect if the child 1954 // has not already been translated. If the child has been translated, use the 1955 // current translated values, as this child may be in the middle of a previous 1956 // animation, so we don't want to simply force it to new location. 1957 1958 xTranslation = oldRect.left - childToAnimate.getLeft(); 1959 yTranslation = oldRect.top - childToAnimate.getTop(); 1960 final float rotation = childToAnimate.getRotation(); 1961 1962 // First set the translation X and Y. The current translation might be out of date. 1963 childToAnimate.setTranslationX(xTranslation); 1964 childToAnimate.setTranslationY(yTranslation); 1965 1966 if (xTranslation == 0 && yTranslation == 0 && rotation == 0) { 1967 // Bail early if this view doesn't need to be translated. 1968 continue; 1969 } 1970 1971 SgvAnimationHelper.addTranslationRotationAnimators(animators, childToAnimate, 1972 xTranslation, yTranslation, rotation, animationDelay); 1973 } else { 1974 // If this view was not present before the data updated, rather than just flashing 1975 // the view into its designated position, fly it up from the bottom. 1976 xTranslation = 0; 1977 yTranslation = (animationInMode == AnimationIn.FLY_IN_NEW_VIEWS) ? getHeight() : 0; 1978 1979 // Since this is a new view coming in, add additional delays so that these IN 1980 // animations start after all the OUT animations have been played. 1981 animationDelay += SgvAnimationHelper.getDefaultAnimationDuration(); 1982 1983 childToAnimate.setTranslationX(xTranslation); 1984 childToAnimate.setTranslationY(yTranslation); 1985 1986 switch (animationInMode) { 1987 case FLY_IN_NEW_VIEWS: 1988 SgvAnimationHelper.addTranslationRotationAnimators(animators, 1989 childToAnimate, xTranslation, yTranslation, 1990 SgvAnimationHelper.ANIMATION_ROTATION_DEGREES, animationDelay); 1991 break; 1992 1993 case SLIDE_IN_NEW_VIEWS: 1994 // Bias towards sliding right, but depending on the column that this view 1995 // is laid out in, slide towards the nearest side edge. 1996 int startTranslation = (int)(childToAnimate.getWidth() * 1.5); 1997 if (lp.column < (mColCount / 2)) { 1998 startTranslation = -startTranslation; 1999 } 2000 2001 SgvAnimationHelper.addSlideInFromRightAnimators(animators, 2002 childToAnimate, startTranslation, 2003 animationDelay); 2004 break; 2005 2006 case EXPAND_NEW_VIEWS: 2007 case EXPAND_NEW_VIEWS_NO_CASCADE: 2008 if (i == 0) { 2009 // Initially set the alpha of this view to be invisible, then fade in. 2010 childToAnimate.setAlpha(0); 2011 2012 // Create animators that translate the view back to translation = 0 2013 // which would be its new layout position 2014 final int offset = -1 * childToAnimate.getHeight(); 2015 SgvAnimationHelper.addXYTranslationAnimators(animators, 2016 childToAnimate, 0 /* xTranslation */, offset, animationDelay); 2017 2018 SgvAnimationHelper.addFadeAnimators(animators, childToAnimate, 2019 0 /* start alpha */, 1.0f /* end alpha */, animationDelay); 2020 } else { 2021 SgvAnimationHelper.addExpandInAnimators(animators, 2022 childToAnimate, animationDelay); 2023 } 2024 break; 2025 case FADE: 2026 SgvAnimationHelper.addFadeAnimators(animators, childToAnimate, 2027 0 /* start alpha */, 1.0f /* end alpha */, animationDelay); 2028 break; 2029 2030 default: 2031 continue; 2032 } 2033 } 2034 2035 viewsAnimated++; 2036 } 2037 2038 return animators; 2039 } 2040 2041 private void addStaleViewAnimationEndListener(final View view, List<Animator> viewAnimators) { 2042 if (viewAnimators == null) { 2043 return; 2044 } 2045 2046 for (final Animator animator : viewAnimators) { 2047 animator.addListener(new AnimatorListenerAdapter() { 2048 @Override 2049 public void onAnimationEnd(Animator animation) { 2050 // In the event that onChanged is called before this animation finishes, 2051 // we would have mistakenly cached a view that would be recycled. So 2052 // check if it's there, and remove it so that obtainView() doesn't 2053 // accidentally use the cached view later when it's already been 2054 // moved to the recycler. 2055 final LayoutParams lp = (LayoutParams) view.getLayoutParams(); 2056 if (mChildRectsForAnimation.containsKey(lp.id)) { 2057 mChildRectsForAnimation.remove(lp.id); 2058 } 2059 2060 recycleView(view); 2061 } 2062 }); 2063 } 2064 } 2065 2066 /** 2067 * Calculate and cache the {@link LayoutRecord}s for all positions up to mFirstPosition. 2068 * mFirstPosition is the position that layout will start from, but we need to know where all 2069 * views preceding it will be laid out so that mFirstPosition will be laid out at the correct 2070 * position. If this is not done, mFirstPosition will be laid out at the first empty space 2071 * possible (i.e., top left), and this may not be the correct position in the overall layout. 2072 * 2073 * This can be optimized if we don't need to guard against jagged edges in the grid or if 2074 * mFirstChangedPosition is set to a non-zero value (so we can skip calculating some views). 2075 */ 2076 private void calculateLayoutStartOffsets(int offset) { 2077 // Bail early if we don't guard against jagged edges or if nothing has changed before 2078 // mFirstPosition. 2079 // Also check that we're not at the top of the list because sometimes grid padding isn't set 2080 // until after mItemTops and mItemBottoms arrays have been initialized, so we should 2081 // go through and compute the right layout start offset for mFirstPosition = 0. 2082 if (mFirstPosition != 0 && 2083 (!mGuardAgainstJaggedEdges || mFirstPosition < mFirstChangedPosition)) { 2084 // At this time, we know that mItemTops should be the same, because 2085 // nothing has changed before view at mFirstPosition. The only thing 2086 // we need to do is to reset mItemBottoms. The result should be the 2087 // same, if we don't bail early and execute the following code 2088 // again. Notice that mItemBottoms always equal to mItemTops after 2089 // this method. 2090 System.arraycopy(mItemTops, 0, mItemBottoms, 0, mColCount); 2091 return; 2092 } 2093 2094 final int colWidth = (getWidth() - getPaddingLeft() - getPaddingRight() - 2095 mItemMargin * (mColCount - 1)) / mColCount; 2096 2097 Arrays.fill(mItemTops, getPaddingTop()); 2098 Arrays.fill(mItemBottoms, getPaddingTop()); 2099 2100 // Since we will be doing a pass to calculate all views up to mFirstPosition, it is likely 2101 // that all existing {@link LayoutRecord}s will be stale, so clear it out to avoid 2102 // accidentally the re-use of stale values. 2103 // 2104 // Note: We cannot just invalidate all layout records after mFirstPosition because it is 2105 // possible that this layout pass is caused by a down sync from the server that may affect 2106 // the layout of views from position 0 to mFirstPosition - 1. 2107 if (mDataChanged) { 2108 mLayoutRecords.clear(); 2109 } 2110 2111 for (int i = 0; i < mFirstPosition; i++) { 2112 LayoutRecord rec = mLayoutRecords.get(i); 2113 2114 if (mDataChanged || rec == null) { 2115 final View view = obtainView(i, null); 2116 final LayoutParams lp = (LayoutParams) view.getLayoutParams(); 2117 2118 final int heightSpec; 2119 if (lp.height == LayoutParams.WRAP_CONTENT) { 2120 heightSpec = MeasureSpec.makeMeasureSpec(0, MeasureSpec.UNSPECIFIED); 2121 } else { 2122 heightSpec = MeasureSpec.makeMeasureSpec(lp.height, MeasureSpec.EXACTLY); 2123 } 2124 2125 final int span = Math.min(mColCount, lp.span); 2126 final int widthSize = colWidth * span + mItemMargin * (span - 1); 2127 final int widthSpec = MeasureSpec.makeMeasureSpec(widthSize, MeasureSpec.EXACTLY); 2128 2129 view.measure(widthSpec, heightSpec); 2130 final int height = view.getMeasuredHeight(); 2131 2132 if (rec == null) { 2133 rec = new LayoutRecord(); 2134 mLayoutRecords.put(i, rec); 2135 } 2136 2137 rec.height = height; 2138 rec.id = lp.id; 2139 rec.span = span; 2140 2141 // We're not actually using this view, so add this back to the recycler. 2142 mRecycler.addScrap(view); 2143 } 2144 2145 int nextColumn = getNextColumnDown(); 2146 2147 // Given the span, check if there's enough space to put this view at this column. 2148 // IMPORTANT Use the same logic in {@link #layoutChildren}. 2149 if (rec.span > 1) { 2150 if (mIsRtlLayout) { 2151 if (nextColumn + 1 < rec.span) { 2152 nextColumn = mColCount - 1; 2153 } 2154 } else { 2155 if (mColCount - nextColumn < rec.span) { 2156 nextColumn = 0; 2157 } 2158 } 2159 } 2160 rec.column = nextColumn; 2161 2162 // Place the top of this child beneath the last by finding the lowest coordinate across 2163 // the columns that this child will span. For LTR layout, we scan across from left to 2164 // right, and for RTL layout, we scan from right to left. 2165 // TODO: Consolidate this logic with getNextRecordDown() in the future, as that method 2166 // already calculates the margins for us. This will keep the implementation consistent 2167 // with layoutChildren(), fillUp() and fillDown(). 2168 int lowest = mItemBottoms[nextColumn] + mItemMargin; 2169 if (rec.span > 1) { 2170 for (int spanIndex = 0; spanIndex < rec.span; spanIndex++) { 2171 final int index = mIsRtlLayout ? nextColumn - spanIndex : 2172 nextColumn + spanIndex; 2173 final int bottom = mItemBottoms[index] + mItemMargin; 2174 if (bottom > lowest) { 2175 lowest = bottom; 2176 } 2177 } 2178 } 2179 2180 for (int spanIndex = 0; spanIndex < rec.span; spanIndex++) { 2181 final int col = mIsRtlLayout ? nextColumn - spanIndex : nextColumn + spanIndex; 2182 mItemBottoms[col] = lowest + rec.height; 2183 2184 if (Log.isLoggable(TAG, Log.VERBOSE)) { 2185 Log.v(TAG, " position: " + i + " bottoms: "); 2186 for (int j = 0; j < mColCount; j++) { 2187 Log.v(TAG, " mItemBottoms["+j+"]: " + mItemBottoms[j]); 2188 } 2189 } 2190 } 2191 } 2192 2193 // mItemBottoms[] at this point contains the values of all views up to mFirstPosition. To 2194 // figure out where view at mFirstPosition will be laid out, we'll need to find the column 2195 // that is the highest (i.e., i where mItemBottoms[i] <= mItemBottoms[j] for all j 2196 // from 0 to mColCount.) 2197 int highestValue = Integer.MAX_VALUE; 2198 for (int k = 0; k < mColCount; k++) { 2199 if (mItemBottoms[k] < highestValue) { 2200 highestValue = mItemBottoms[k]; 2201 } 2202 } 2203 2204 // Adjust the offsets in each column so that values in mItemTops[] and mItemBottoms[] 2205 // reflect coordinates on screen. These offsets will be the actual values where layout 2206 // will start from, otherwise, we'd naively start at (leftPadding, topPadding) for 2207 // mFirstPosition. 2208 for (int k = 0; k < mColCount; k++) { 2209 mItemBottoms[k] = mItemBottoms[k] - highestValue + offset; 2210 mItemTops[k] = mItemBottoms[k]; 2211 2212 // Log.v(TAG, "Adjusting to offset = mItemBottoms[" + k + "]: " + mItemBottoms[k]); 2213 } 2214 } 2215 2216 /** 2217 * Measure and layout all currently visible children. 2218 * 2219 * @param queryAdapter true to requery the adapter for view data 2220 */ 2221 final void layoutChildren(boolean queryAdapter) { 2222 final int paddingLeft = getPaddingLeft(); 2223 final int paddingRight = getPaddingRight(); 2224 final int itemMargin = mItemMargin; 2225 final int availableWidth = (getWidth() - paddingLeft - paddingRight - itemMargin 2226 * (mColCount - 1)); 2227 final int colWidth = availableWidth / mColCount; 2228 // The availableWidth may not be divisible by mColCount. Keep the 2229 // remainder. It will be added to the width of the last view in the row. 2230 final int remainder = availableWidth % mColCount; 2231 2232 boolean viewsRemovedInLayout = false; 2233 2234 // If we're animating out stale views, then we want to defer recycling of views. 2235 final boolean deferRecyclingForAnimation = mAnimationOutMode != AnimationOut.NONE; 2236 2237 if (!deferRecyclingForAnimation) { 2238 final int childCount = getChildCount(); 2239 // If the latest data set has fewer data items than mFirstPosition, don't keep any 2240 // views on screen, and just let the layout logic below retrieve appropriate views 2241 // from the recycler. 2242 final int viewsToKeepOnScreen = (mItemCount <= mFirstPosition) ? 0 : 2243 mItemCount - mFirstPosition; 2244 2245 if (childCount > viewsToKeepOnScreen) { 2246 // If there are more views laid out than the number of data items remaining to be 2247 // laid out, recycle the extraneous views. 2248 recycleViewsInRange(viewsToKeepOnScreen, childCount - 1); 2249 viewsRemovedInLayout = true; 2250 } 2251 } else { 2252 mViewsToAnimateOut.clear(); 2253 } 2254 2255 for (int i = 0; i < getChildCount(); i++) { 2256 final int position = mFirstPosition + i; 2257 View child = getChildAt(i); 2258 2259 final int highestAvailableLayoutPosition = mItemBottoms[getNextColumnDown()]; 2260 if (deferRecyclingForAnimation && 2261 (position >= mItemCount || highestAvailableLayoutPosition >= getHeight())) { 2262 // For the remainder of views on screen, they should not be on screen, so we can 2263 // skip layout. Add them to the list of views to animate out. 2264 // We should only get in this position if deferRecyclingForAnimation = true, 2265 // otherwise, we should've recycled all views before getting into this layout loop. 2266 mViewsToAnimateOut.add(child); 2267 continue; 2268 } 2269 2270 LayoutParams lp = null; 2271 int col = -1; 2272 2273 if (child != null) { 2274 lp = (LayoutParams) child.getLayoutParams(); 2275 col = lp.column; 2276 } 2277 2278 final boolean needsLayout = queryAdapter || child == null || child.isLayoutRequested(); 2279 if (queryAdapter) { 2280 View newView = null; 2281 if (deferRecyclingForAnimation) { 2282 // If we are deferring recycling for animation, then we don't want to pass the 2283 // current child in to obtainView for re-use. obtainView() in this case should 2284 // try to find the view belonging to this item on screen, or populate a fresh 2285 // one from the recycler. 2286 newView = obtainView(position); 2287 } else { 2288 newView = obtainView(position, child); 2289 } 2290 2291 // Update layout params since they may have changed 2292 lp = (LayoutParams) newView.getLayoutParams(); 2293 2294 if (newView != child) { 2295 if (child != null && !deferRecyclingForAnimation) { 2296 mRecycler.addScrap(child); 2297 removeViewInLayout(child); 2298 viewsRemovedInLayout = true; 2299 } 2300 2301 // If this view is already in the layout hierarchy, we can just detach it 2302 // from the parent and re-attach it at the correct index. If the view has 2303 // already been removed from the layout hierarchy, getParent() == null. 2304 if (newView.getParent() == this) { 2305 detachViewFromParent(newView); 2306 attachViewToParent(newView, i, lp); 2307 } else { 2308 addViewInLayout(newView, i, lp); 2309 } 2310 } 2311 2312 child = newView; 2313 2314 // Since the data has changed, we need to make sure the next child is in the 2315 // right column. We choose the next column down (vs. next column up) because we 2316 // are filling from the top of the screen downwards as we iterate through 2317 // visible children. (We take span into account below.) 2318 lp.column = getNextColumnDown(); 2319 col = lp.column; 2320 } 2321 2322 setReorderingArea(lp); 2323 2324 final int span = Math.min(mColCount, lp.span); 2325 2326 // Given the span, check if there's enough space to put this view at this column. 2327 // IMPORTANT Propagate the same logic to {@link #calculateLayoutStartOffsets}. 2328 if (span > 1) { 2329 if (mIsRtlLayout) { 2330 // For RTL layout, if the current column index is less than the span of the 2331 // child, then we know that there is not enough room remaining to lay this 2332 // child out (e.g., if col == 0, but span == 2, then laying this child down 2333 // at column = col would put us out of bound into a negative column index.). 2334 // For this scenario, reset the index back to the right-most column, and lay 2335 // out the child at this position where we can ensure that we can display as 2336 // much of the child as possible. 2337 if (col + 1 < span) { 2338 col = mColCount - 1; 2339 } 2340 } else { 2341 if (mColCount - col < span) { 2342 // If not, reset the col to 0. 2343 col = 0; 2344 } 2345 } 2346 2347 lp.column = col; 2348 } 2349 2350 int widthSize = (colWidth * span + itemMargin * (span - 1)); 2351 // If it is rtl, we layout the view from col to col - span + 2352 // 1. If it reaches the most left column, i.e. we added the 2353 // additional width. So the check it span == col +1 2354 if ((mIsRtlLayout && span == col + 1) 2355 || (!mIsRtlLayout && span + col == mColCount)) { 2356 widthSize += remainder; 2357 } 2358 if (needsLayout) { 2359 final int widthSpec = MeasureSpec.makeMeasureSpec(widthSize, MeasureSpec.EXACTLY); 2360 2361 final int heightSpec; 2362 if (lp.height == LayoutParams.WRAP_CONTENT) { 2363 heightSpec = MeasureSpec.makeMeasureSpec(0, MeasureSpec.UNSPECIFIED); 2364 } else { 2365 heightSpec = MeasureSpec.makeMeasureSpec(lp.height, MeasureSpec.EXACTLY); 2366 } 2367 2368 child.measure(widthSpec, heightSpec); 2369 } 2370 2371 // Place the top of this child beneath the last by finding the lowest coordinate across 2372 // the columns that this child will span. For LTR layout, we scan across from left to 2373 // right, and for RTL layout, we scan from right to left. 2374 // TODO: Consolidate this logic with getNextRecordDown() in the future, as that method 2375 // already calculates the margins for us. This will keep the implementation consistent 2376 // with fillUp() and fillDown(). 2377 int childTop = mItemBottoms[col] + mItemMargin; 2378 if (span > 1) { 2379 int lowest = childTop; 2380 for (int spanIndex = 0; spanIndex < span; spanIndex++) { 2381 final int index = mIsRtlLayout ? col - spanIndex : col + spanIndex; 2382 final int bottom = mItemBottoms[index] + mItemMargin; 2383 if (bottom > lowest) { 2384 lowest = bottom; 2385 } 2386 } 2387 2388 childTop = lowest; 2389 } 2390 2391 final int childHeight = child.getMeasuredHeight(); 2392 final int childBottom = childTop + childHeight; 2393 int childLeft = 0; 2394 int childRight = 0; 2395 if (mIsRtlLayout) { 2396 childRight = (getWidth() - paddingRight) - 2397 (mColCount - col - 1) * (colWidth + itemMargin); 2398 childLeft = childRight - child.getMeasuredWidth(); 2399 } else { 2400 childLeft = paddingLeft + col * (colWidth + itemMargin); 2401 childRight = childLeft + child.getMeasuredWidth(); 2402 } 2403 2404 /* Log.v(TAG, "[layoutChildren] height: " + childHeight 2405 + " top: " + childTop + " bottom: " + childBottom 2406 + " left: " + childLeft 2407 + " column: " + col 2408 + " position: " + position 2409 + " id: " + lp.id); 2410 */ 2411 child.layout(childLeft, childTop, childRight, childBottom); 2412 if (lp.id == mFocusedChildIdToScrollIntoView) { 2413 child.requestFocus(); 2414 } 2415 2416 for (int spanIndex = 0; spanIndex < span; spanIndex++) { 2417 final int index = mIsRtlLayout ? col - spanIndex : col + spanIndex; 2418 mItemBottoms[index] = childBottom; 2419 } 2420 2421 // Whether or not LayoutRecords may have already existed for the view at this position 2422 // on screen, we'll update it after we lay out to ensure that the LayoutRecord 2423 // has the most updated information about the view at this position. We can be assured 2424 // that all views before those on screen (views with adapter position < mFirstPosition) 2425 // have the correct LayoutRecords because calculateLayoutStartOffsets() would have 2426 // set them appropriately. 2427 LayoutRecord rec = mLayoutRecords.get(position); 2428 if (rec == null) { 2429 rec = new LayoutRecord(); 2430 mLayoutRecords.put(position, rec); 2431 } 2432 2433 rec.column = lp.column; 2434 rec.height = childHeight; 2435 rec.id = lp.id; 2436 rec.span = span; 2437 } 2438 2439 // It appears that removeViewInLayout() does not invalidate. So if we make use of this 2440 // method during layout, we should invalidate explicitly. 2441 if (viewsRemovedInLayout || deferRecyclingForAnimation) { 2442 invalidate(); 2443 } 2444 } 2445 2446 /** 2447 * Set the reordering area for the child layout specified 2448 */ 2449 private void setReorderingArea(LayoutParams childLayoutParams) { 2450 final boolean isLastColumn = childLayoutParams.column == (mColCount - 1); 2451 childLayoutParams.reorderingArea = 2452 mAdapter.getReorderingArea(childLayoutParams.position, isLastColumn); 2453 } 2454 2455 final void invalidateLayoutRecordsBeforePosition(int position) { 2456 int endAt = 0; 2457 while (endAt < mLayoutRecords.size() && mLayoutRecords.keyAt(endAt) < position) { 2458 endAt++; 2459 } 2460 mLayoutRecords.removeAtRange(0, endAt); 2461 } 2462 2463 final void invalidateLayoutRecordsAfterPosition(int position) { 2464 int beginAt = mLayoutRecords.size() - 1; 2465 while (beginAt >= 0 && mLayoutRecords.keyAt(beginAt) > position) { 2466 beginAt--; 2467 } 2468 beginAt++; 2469 mLayoutRecords.removeAtRange(beginAt + 1, mLayoutRecords.size() - beginAt); 2470 } 2471 2472 /** 2473 * Before doing an animation, map the item IDs for the currently visible children to the 2474 * {@link Rect} that defines their position on the screen so a translation animation 2475 * can be applied to their new layout positions. 2476 */ 2477 private void cacheChildRects() { 2478 final int childCount = getChildCount(); 2479 mChildRectsForAnimation.clear(); 2480 2481 long originalDraggedChildId = -1; 2482 if (isDragReorderingSupported()) { 2483 originalDraggedChildId = mReorderHelper.getDraggedChildId(); 2484 if (mCachedDragViewRect != null && originalDraggedChildId != -1) { 2485 // This child was dragged in a reordering operation. Use the cached position 2486 // of where the drag event was released as the cached location. 2487 mChildRectsForAnimation.put(originalDraggedChildId, 2488 new ViewRectPair(mDragView, mCachedDragViewRect)); 2489 mCachedDragViewRect = null; 2490 } 2491 } 2492 2493 for (int i = 0; i < childCount; i++) { 2494 final View child = getChildAt(i); 2495 final LayoutParams lp = (LayoutParams) child.getLayoutParams(); 2496 2497 Rect rect; 2498 if (lp.id != originalDraggedChildId) { 2499 final int childTop = (int) child.getY(); 2500 final int childBottom = childTop + child.getHeight(); 2501 final int childLeft = (int) child.getX(); 2502 final int childRight = childLeft + child.getWidth(); 2503 rect = new Rect(childLeft, childTop, childRight, childBottom); 2504 mChildRectsForAnimation.put(lp.id /* item id */, new ViewRectPair(child, rect)); 2505 } 2506 } 2507 } 2508 2509 /** 2510 * Should be called with mPopulating set to true 2511 * 2512 * @param fromPosition Position to start filling from 2513 * @param overhang the number of extra pixels to fill beyond the current top edge 2514 * @return the max overhang beyond the beginning of the view of any added items at the top 2515 */ 2516 final int fillUp(int fromPosition, int overhang) { 2517 final int paddingLeft = getPaddingLeft(); 2518 final int paddingRight = getPaddingRight(); 2519 final int itemMargin = mItemMargin; 2520 final int availableWidth = (getWidth() - paddingLeft - paddingRight - itemMargin 2521 * (mColCount - 1)); 2522 final int colWidth = availableWidth / mColCount; 2523 // The availableWidth may not be divisible by mColCount. Keep the 2524 // remainder. It will be added to the width of the last view in the row. 2525 final int remainder = availableWidth % mColCount; 2526 final int gridTop = getPaddingTop(); 2527 final int fillTo = -overhang; 2528 int nextCol = getNextColumnUp(); 2529 int position = fromPosition; 2530 2531 while (nextCol >= 0 && mItemTops[nextCol] > fillTo && position >= 0) { 2532 final View child = obtainView(position, null); 2533 final LayoutParams lp = (LayoutParams) child.getLayoutParams(); 2534 2535 if (child.getParent() != this) { 2536 if (mInLayout) { 2537 addViewInLayout(child, 0, lp); 2538 } else { 2539 addView(child, 0); 2540 } 2541 } 2542 2543 final int span = Math.min(mColCount, lp.span); 2544 2545 LayoutRecord rec; 2546 if (span > 1) { 2547 rec = getNextRecordUp(position, span); 2548 nextCol = rec.column; 2549 } else { 2550 rec = mLayoutRecords.get(position); 2551 } 2552 2553 boolean invalidateBefore = false; 2554 if (rec == null) { 2555 rec = new LayoutRecord(); 2556 mLayoutRecords.put(position, rec); 2557 rec.column = nextCol; 2558 rec.span = span; 2559 } else if (span != rec.span) { 2560 rec.span = span; 2561 rec.column = nextCol; 2562 invalidateBefore = true; 2563 } else { 2564 nextCol = rec.column; 2565 } 2566 2567 if (mHasStableIds) { 2568 rec.id = lp.id; 2569 } 2570 2571 lp.column = nextCol; 2572 setReorderingArea(lp); 2573 2574 int widthSize = colWidth * span + itemMargin * (span - 1); 2575 // If it is rtl, we layout the view from nextCol to nextCol - span + 2576 // 1. If it reaches the most left column, i.e. we added the 2577 // additional width. So the check it span == nextCol + 1 2578 if ((mIsRtlLayout && span == nextCol + 1) 2579 || (!mIsRtlLayout && span + nextCol == mColCount)) { 2580 widthSize += remainder; 2581 } 2582 final int widthSpec = MeasureSpec.makeMeasureSpec(widthSize, MeasureSpec.EXACTLY); 2583 final int heightSpec; 2584 if (lp.height == LayoutParams.WRAP_CONTENT) { 2585 heightSpec = MeasureSpec.makeMeasureSpec(0, MeasureSpec.UNSPECIFIED); 2586 } else { 2587 heightSpec = MeasureSpec.makeMeasureSpec(lp.height, MeasureSpec.EXACTLY); 2588 } 2589 child.measure(widthSpec, heightSpec); 2590 2591 final int childHeight = child.getMeasuredHeight(); 2592 if (invalidateBefore || (childHeight != rec.height && rec.height > 0)) { 2593 invalidateLayoutRecordsBeforePosition(position); 2594 } 2595 rec.height = childHeight; 2596 2597 // Iterate across each column that this child spans and add the margin calculated 2598 // for that column to mItemTops. getMarginBelow() is expected to give us the correct 2599 // margin values at each column such that mItemTops ends up with a smooth edge across 2600 // the column spans. We need to do this before actually laying down the child, 2601 // otherwise we risk overlapping one child over another. mItemTops stores the top 2602 // index for where the next child should be laid out. For RTL, we do the update 2603 // in reverse order. 2604 for (int i = 0; i < span; i++) { 2605 final int index = mIsRtlLayout ? nextCol - i : nextCol + i; 2606 mItemTops[index] += rec.getMarginBelow(i); 2607 } 2608 2609 final int startFrom = mItemTops[nextCol]; 2610 final int childBottom = startFrom; 2611 final int childTop = childBottom - childHeight; 2612 2613 int childLeft = 0; 2614 int childRight = 0; 2615 // For LTR layout, the child's left is calculated as the 2616 // (column index from left) * (columnWidth plus item margins). 2617 // For RTL layout, the child's left is relative to its right, and its right coordinate 2618 // is calculated as the difference between the width of this grid and 2619 // (column index from right) * (columnWidth plus item margins). 2620 if (mIsRtlLayout) { 2621 childRight = (getWidth() - paddingRight) - 2622 (mColCount - nextCol - 1) * (colWidth + itemMargin); 2623 childLeft = childRight - child.getMeasuredWidth(); 2624 } else { 2625 childLeft = paddingLeft + nextCol * (colWidth + itemMargin); 2626 childRight = childLeft + child.getMeasuredWidth(); 2627 } 2628 child.layout(childLeft, childTop, childRight, childBottom); 2629 2630 Log.v(TAG, "[fillUp] position: " + position + " id: " + lp.id 2631 + " childLeft: " + childLeft + " childTop: " + childTop 2632 + " column: " + rec.column + " childHeight:" + childHeight); 2633 2634 // Since we're filling up, once the child is laid out, update mItemTops again 2635 // to reflect the next available top value at this column. This is simply the child's 2636 // top coordinates, minus any available margins set. For LTR, we start at the column 2637 // that this child is laid out from (nextCol) and move right for span amount. For RTL 2638 // layout, we start at the column that this child is laid out from and move left. 2639 for (int i = 0; i < span; i++) { 2640 final int index = mIsRtlLayout ? nextCol - i : nextCol + i; 2641 mItemTops[index] = childTop - rec.getMarginAbove(i) - itemMargin; 2642 } 2643 2644 if (lp.id == mFocusedChildIdToScrollIntoView) { 2645 child.requestFocus(); 2646 } 2647 2648 nextCol = getNextColumnUp(); 2649 mFirstPosition = position--; 2650 } 2651 2652 int highestView = getHeight(); 2653 for (int i = 0; i < mColCount; i++) { 2654 if (mItemTops[i] < highestView) { 2655 highestView = mItemTops[i]; 2656 } 2657 } 2658 return gridTop - highestView; 2659 } 2660 2661 /** 2662 * Should be called with mPopulating set to true 2663 * 2664 * @param fromPosition Position to start filling from 2665 * @param overhang the number of extra pixels to fill beyond the current bottom edge 2666 * @return the max overhang beyond the end of the view of any added items at the bottom 2667 */ 2668 final int fillDown(int fromPosition, int overhang) { 2669 final int paddingLeft = getPaddingLeft(); 2670 final int paddingRight = getPaddingRight(); 2671 final int itemMargin = mItemMargin; 2672 final int availableWidth = (getWidth() - paddingLeft - paddingRight - itemMargin 2673 * (mColCount - 1)); 2674 final int colWidth = availableWidth / mColCount; 2675 // The availableWidth may not be divisible by mColCount. Keep the 2676 // remainder. It will be added to the width of the last view in the row. 2677 final int remainder = availableWidth % mColCount; 2678 final int gridBottom = getHeight() - getPaddingBottom(); 2679 final int fillTo = gridBottom + overhang; 2680 int nextCol = getNextColumnDown(); 2681 int position = fromPosition; 2682 2683 while (nextCol >= 0 && mItemBottoms[nextCol] < fillTo && position < mItemCount) { 2684 final View child = obtainView(position, null); 2685 final LayoutParams lp = (LayoutParams) child.getLayoutParams(); 2686 if (child.getParent() != this) { 2687 if (mInLayout) { 2688 addViewInLayout(child, -1, lp); 2689 } else { 2690 addView(child); 2691 } 2692 } 2693 2694 final int span = Math.min(mColCount, lp.span); 2695 2696 LayoutRecord rec; 2697 if (span > 1) { 2698 rec = getNextRecordDown(position, span); 2699 nextCol = rec.column; 2700 } else { 2701 rec = mLayoutRecords.get(position); 2702 } 2703 2704 boolean invalidateAfter = false; 2705 if (rec == null) { 2706 rec = new LayoutRecord(); 2707 mLayoutRecords.put(position, rec); 2708 rec.column = nextCol; 2709 rec.span = span; 2710 } else if (span != rec.span) { 2711 rec.span = span; 2712 rec.column = nextCol; 2713 invalidateAfter = true; 2714 } else { 2715 nextCol = rec.column; 2716 } 2717 2718 if (mHasStableIds) { 2719 rec.id = lp.id; 2720 } 2721 2722 lp.column = nextCol; 2723 setReorderingArea(lp); 2724 2725 2726 int widthSize = colWidth * span + itemMargin * (span - 1); 2727 // If it is rtl, we layout the view from nextCol to nextCol - span + 2728 // 1. If it reaches the most left column, i.e. we added the 2729 // additional width. So the check it span == nextCol +1 2730 if ((mIsRtlLayout && span == nextCol + 1) 2731 || (!mIsRtlLayout && span + nextCol == mColCount)) { 2732 widthSize += remainder; 2733 } 2734 final int widthSpec = MeasureSpec.makeMeasureSpec(widthSize, MeasureSpec.EXACTLY); 2735 final int heightSpec; 2736 if (lp.height == LayoutParams.WRAP_CONTENT) { 2737 heightSpec = MeasureSpec.makeMeasureSpec(0, MeasureSpec.UNSPECIFIED); 2738 } else { 2739 heightSpec = MeasureSpec.makeMeasureSpec(lp.height, MeasureSpec.EXACTLY); 2740 } 2741 child.measure(widthSpec, heightSpec); 2742 2743 final int childHeight = child.getMeasuredHeight(); 2744 if (invalidateAfter || (childHeight != rec.height && rec.height > 0)) { 2745 invalidateLayoutRecordsAfterPosition(position); 2746 } 2747 2748 rec.height = childHeight; 2749 2750 // Before laying out the child, we need to make sure mItemBottoms is updated with the 2751 // correct values such that there is a smooth edge across the child's span. 2752 // getMarginAbove() is expected to give us these values. For LTR layout, we start at 2753 // nextCol, and update forward for the number of columns this child spans. For RTL 2754 // layout, we start at nextCol and update backwards for the same number of columns. 2755 for (int i = 0; i < span; i++) { 2756 final int index = mIsRtlLayout ? nextCol - i : nextCol + i; 2757 mItemBottoms[index] += rec.getMarginAbove(i); 2758 } 2759 2760 final int startFrom = mItemBottoms[nextCol]; 2761 final int childTop = startFrom + itemMargin; 2762 final int childBottom = childTop + childHeight; 2763 int childLeft = 0; 2764 int childRight = 0; 2765 if (mIsRtlLayout) { 2766 childRight = (getWidth() - paddingRight) - 2767 (mColCount - nextCol - 1) * (colWidth + itemMargin); 2768 childLeft = childRight - child.getMeasuredWidth(); 2769 } else { 2770 childLeft = paddingLeft + nextCol * (colWidth + itemMargin); 2771 childRight = childLeft + child.getMeasuredWidth(); 2772 } 2773 2774 Log.v(TAG, "[fillDown] position: " + position + " id: " + lp.id 2775 + " childLeft: " + childLeft + " childTop: " + childTop 2776 + " column: " + rec.column + " childHeight:" + childHeight); 2777 2778 child.layout(childLeft, childTop, childRight, childBottom); 2779 2780 // Once we've laid down the child, update mItemBottoms again to reflect the next 2781 // available set of bottom values for the next child. 2782 for (int i = 0; i < span; i++) { 2783 final int index = mIsRtlLayout ? nextCol - i : nextCol + i; 2784 mItemBottoms[index] = childBottom + rec.getMarginBelow(i); 2785 } 2786 2787 if (lp.id == mFocusedChildIdToScrollIntoView) { 2788 child.requestFocus(); 2789 } 2790 2791 nextCol = getNextColumnDown(); 2792 position++; 2793 } 2794 2795 int lowestView = 0; 2796 for (int i = 0; i < mColCount; i++) { 2797 final int index = mIsRtlLayout ? mColCount - (i + 1) : i; 2798 if (mItemBottoms[index] > lowestView) { 2799 lowestView = mItemBottoms[index]; 2800 } 2801 } 2802 2803 return lowestView - gridBottom; 2804 } 2805 2806 /** 2807 * @return column that the next view filling upwards should occupy. This is the bottom-most 2808 * position available for a single-column item. 2809 */ 2810 final int getNextColumnUp() { 2811 int result = -1; 2812 int bottomMost = Integer.MIN_VALUE; 2813 2814 final int colCount = mColCount; 2815 for (int i = colCount - 1; i >= 0; i--) { 2816 final int index = mIsRtlLayout ? colCount - (i + 1) : i; 2817 final int top = mItemTops[index]; 2818 if (top > bottomMost) { 2819 bottomMost = top; 2820 result = index; 2821 } 2822 } 2823 2824 return result; 2825 } 2826 2827 /** 2828 * Return a LayoutRecord for the given position 2829 * @param position 2830 * @param span 2831 * @return 2832 */ 2833 final LayoutRecord getNextRecordUp(int position, int span) { 2834 LayoutRecord rec = mLayoutRecords.get(position); 2835 if (rec == null || rec.span != span) { 2836 if (span > mColCount) { 2837 throw new IllegalStateException("Span larger than column count! Span:" + span 2838 + " ColumnCount:" + mColCount); 2839 } 2840 rec = new LayoutRecord(); 2841 rec.span = span; 2842 mLayoutRecords.put(position, rec); 2843 } 2844 int targetCol = -1; 2845 int bottomMost = Integer.MIN_VALUE; 2846 2847 // For LTR layout, we start from the bottom-right corner upwards when we need to find the 2848 // NextRecordUp. For RTL, we will start from bottom-left. 2849 final int colCount = mColCount; 2850 if (mIsRtlLayout) { 2851 for (int i = span - 1; i < colCount; i++) { 2852 int top = Integer.MAX_VALUE; 2853 for (int j = i; j > i - span; j--) { 2854 final int singleTop = mItemTops[j]; 2855 if (singleTop < top) { 2856 top = singleTop; 2857 } 2858 } 2859 if (top > bottomMost) { 2860 bottomMost = top; 2861 targetCol = i; 2862 } 2863 } 2864 } else { 2865 for (int i = colCount - span; i >= 0; i--) { 2866 int top = Integer.MAX_VALUE; 2867 for (int j = i; j < i + span; j++) { 2868 final int singleTop = mItemTops[j]; 2869 if (singleTop < top) { 2870 top = singleTop; 2871 } 2872 } 2873 if (top > bottomMost) { 2874 bottomMost = top; 2875 targetCol = i; 2876 } 2877 } 2878 } 2879 2880 rec.column = targetCol; 2881 2882 // Once we've found the target column for the view at this position, we update mItemTops 2883 // for all columns that this view will occupy. We set the margin such that mItemTops is 2884 // equal for all columns in the view's span. For LTR layout, we start at targetCol and 2885 // move right, and for RTL, we start at targetCol and move left. 2886 for (int i = 0; i < span; i++) { 2887 final int nextCol = mIsRtlLayout ? targetCol - i : targetCol + i; 2888 rec.setMarginBelow(i, mItemTops[nextCol] - bottomMost); 2889 } 2890 2891 return rec; 2892 } 2893 2894 /** 2895 * @return column that the next view filling downwards should occupy. This is the top-most 2896 * position available. 2897 */ 2898 final int getNextColumnDown() { 2899 int topMost = Integer.MAX_VALUE; 2900 int result = 0; 2901 final int colCount = mColCount; 2902 2903 for (int i = 0; i < colCount; i++) { 2904 final int index = mIsRtlLayout ? colCount - (i + 1) : i; 2905 final int bottom = mItemBottoms[index]; 2906 if (bottom < topMost) { 2907 topMost = bottom; 2908 result = index; 2909 } 2910 } 2911 2912 return result; 2913 } 2914 2915 final LayoutRecord getNextRecordDown(int position, int span) { 2916 LayoutRecord rec = mLayoutRecords.get(position); 2917 if (rec == null || rec.span != span) { 2918 if (span > mColCount) { 2919 throw new IllegalStateException("Span larger than column count! Span:" + span 2920 + " ColumnCount:" + mColCount); 2921 } 2922 2923 rec = new LayoutRecord(); 2924 rec.span = span; 2925 mLayoutRecords.put(position, rec); 2926 } 2927 2928 int targetCol = -1; 2929 int topMost = Integer.MAX_VALUE; 2930 2931 final int colCount = mColCount; 2932 2933 // For LTR layout, we start from the top-left corner and move right-downwards, when we 2934 // need to find the NextRecordDown. For RTL we will start from Top-Right corner, and move 2935 // left-downwards. 2936 if (mIsRtlLayout) { 2937 for (int i = colCount - 1; i >= span - 1; i--) { 2938 int bottom = Integer.MIN_VALUE; 2939 for (int j = i; j > i - span; j--) { 2940 final int singleBottom = mItemBottoms[j]; 2941 if (singleBottom > bottom) { 2942 bottom = singleBottom; 2943 } 2944 } 2945 if (bottom < topMost) { 2946 topMost = bottom; 2947 targetCol = i; 2948 } 2949 } 2950 } else { 2951 for (int i = 0; i <= colCount - span; i++) { 2952 int bottom = Integer.MIN_VALUE; 2953 for (int j = i; j < i + span; j++) { 2954 final int singleBottom = mItemBottoms[j]; 2955 if (singleBottom > bottom) { 2956 bottom = singleBottom; 2957 } 2958 } 2959 if (bottom < topMost) { 2960 topMost = bottom; 2961 targetCol = i; 2962 } 2963 } 2964 } 2965 2966 rec.column = targetCol; 2967 2968 // Once we've found the target column for the view at this position, we update mItemBottoms 2969 // for all columns that this view will occupy. We set the margins such that mItemBottoms 2970 // is equal for all columns in the view's span. For LTR layout, we start at targetCol and 2971 // move right, and for RTL, we start at targetCol and move left. 2972 for (int i = 0; i < span; i++) { 2973 final int nextCol = mIsRtlLayout ? targetCol - i : targetCol + i; 2974 rec.setMarginAbove(i, topMost - mItemBottoms[nextCol]); 2975 } 2976 2977 return rec; 2978 } 2979 2980 private int getItemWidth(int itemColumnSpan) { 2981 final int colWidth = (getWidth() - getPaddingLeft() - getPaddingRight() - 2982 mItemMargin * (mColCount - 1)) / mColCount; 2983 return colWidth * itemColumnSpan + mItemMargin * (itemColumnSpan - 1); 2984 } 2985 2986 /** 2987 * Obtain a populated view from the adapter. This method checks to see if the view to populate 2988 * is already laid out on screen somewhere by comparing the item ids. 2989 * 2990 * If the view is already laid out, and the view type has not changed, populate the contents 2991 * and return. 2992 * 2993 * If the view is not laid out on screen somewhere, grab a view from the recycler and populate. 2994 * 2995 * NOTE: This method should be called during layout. 2996 * 2997 * TODO: This can probably be consolidated with the overloaded {@link #obtainView(int, View)}. 2998 * 2999 * @param position Position to get the view for. 3000 */ 3001 final View obtainView(int position) { 3002 // TODO: This method currently does not support transient state views. 3003 3004 final Object item = mAdapter.getItem(position); 3005 3006 View scrap = null; 3007 final int positionViewType = mAdapter.getItemViewType(item, position); 3008 3009 final long id = mAdapter.getItemId(item, position); 3010 final ViewRectPair viewRectPair = mChildRectsForAnimation.get(id); 3011 if (viewRectPair != null) { 3012 scrap = viewRectPair.view; 3013 3014 // TODO: Make use of stable ids by retrieving the cached views using stable ids. In 3015 // theory, we should maintain a list of active views, and then fetch the views 3016 // from that list. If that fails, then we should go to the recycler. 3017 // For the collection holding stable ids, we must ensure that those views don't get 3018 // repurposed for other items at different positions. 3019 } 3020 3021 final int scrapViewType = scrap != null && 3022 (scrap.getLayoutParams() instanceof LayoutParams) ? 3023 ((LayoutParams) scrap.getLayoutParams()).viewType : -1; 3024 3025 if (scrap == null || scrapViewType != positionViewType) { 3026 // If there is no cached view or the cached view's type no longer match the type 3027 // of the item at the specified position, retrieve a new view from the recycler and 3028 // recycle the cached view. 3029 if (scrap != null) { 3030 // The cached view we had is not valid, so add it to the recycler and 3031 // remove it from the current layout. 3032 recycleView(scrap); 3033 } 3034 3035 scrap = mRecycler.getScrapView(positionViewType); 3036 } 3037 3038 final int itemColumnSpan = mAdapter.getItemColumnSpan(item, position); 3039 final int itemWidth = getItemWidth(itemColumnSpan); 3040 final View view = mAdapter.getView(item, position, scrap, this, itemWidth); 3041 3042 ViewGroup.LayoutParams lp = view.getLayoutParams(); 3043 if (view.getParent() != this) { 3044 if (lp == null) { 3045 lp = generateDefaultLayoutParams(); 3046 } else if (!checkLayoutParams(lp)) { 3047 lp = generateLayoutParams(lp); 3048 } 3049 3050 view.setLayoutParams(lp); 3051 } 3052 3053 final LayoutParams sglp = (LayoutParams) view.getLayoutParams(); 3054 sglp.position = position; 3055 sglp.viewType = positionViewType; 3056 sglp.id = id; 3057 sglp.span = itemColumnSpan; 3058 3059 // When the view at the positions we are tracking update, make sure to 3060 // update our views as well. That way, we have the correct 3061 // rectangle for comparing when the drag target enters/ leaves the 3062 // placeholder view. 3063 if (isDragReorderingSupported() && mReorderHelper.getDraggedChildId() == id) { 3064 mReorderHelper.updateDraggedChildView(view); 3065 mReorderHelper.updateDraggedOverChildView(view); 3066 } 3067 return view; 3068 } 3069 3070 /** 3071 * Obtain a populated view from the adapter. If optScrap is non-null and is not 3072 * reused it will be placed in the recycle bin. 3073 * 3074 * @param position position to get view for 3075 * @param optScrap Optional scrap view; will be reused if possible 3076 * @return A new view, a recycled view from mRecycler, or optScrap 3077 */ 3078 final View obtainView(int position, View optScrap) { 3079 View view = mRecycler.getTransientStateView(position); 3080 final Object item = mAdapter.getItem(position); 3081 final int positionViewType = mAdapter.getItemViewType(item, position); 3082 3083 if (view == null) { 3084 // Reuse optScrap if it's of the right type (and not null) 3085 final int optType = optScrap != null ? 3086 ((LayoutParams) optScrap.getLayoutParams()).viewType : -1; 3087 3088 final View scrap = optType == positionViewType ? 3089 optScrap : mRecycler.getScrapView(positionViewType); 3090 3091 final int itemColumnSpan = mAdapter.getItemColumnSpan(item, position); 3092 final int itemWidth = getItemWidth(itemColumnSpan); 3093 view = mAdapter.getView(item, position, scrap, this, itemWidth); 3094 3095 if (view != scrap && scrap != null) { 3096 // The adapter didn't use it; put it back. 3097 mRecycler.addScrap(scrap); 3098 } 3099 3100 ViewGroup.LayoutParams lp = view.getLayoutParams(); 3101 3102 if (view.getParent() != this) { 3103 if (lp == null) { 3104 lp = generateDefaultLayoutParams(); 3105 } else if (!checkLayoutParams(lp)) { 3106 lp = generateLayoutParams(lp); 3107 } 3108 3109 view.setLayoutParams(lp); 3110 } 3111 } 3112 3113 final LayoutParams sglp = (LayoutParams) view.getLayoutParams(); 3114 sglp.position = position; 3115 sglp.viewType = positionViewType; 3116 final long id = mAdapter.getItemIdFromView(view, position); 3117 sglp.id = id; 3118 sglp.span = mAdapter.getItemColumnSpan(item, position); 3119 3120 // When the view at the positions we are tracking update, make sure to 3121 // update our views as well. That way, we have the correct 3122 // rectangle for comparing when the drag target enters/ leaves the 3123 // placeholder view. 3124 if (isDragReorderingSupported() && mReorderHelper.getDraggedChildId() == id) { 3125 mReorderHelper.updateDraggedChildView(view); 3126 mReorderHelper.updateDraggedOverChildView(view); 3127 } 3128 3129 return view; 3130 } 3131 3132 /** 3133 * Animation mode to play for new data coming in as well as the stale data that should be 3134 * animated out. 3135 * @param animationIn The animation to play to introduce new or updated data into view 3136 * @param animationOut The animation to play to transition stale data out of view. 3137 */ 3138 public void setAnimationMode(AnimationIn animationIn, AnimationOut animationOut) { 3139 mAnimationInMode = animationIn; 3140 mAnimationOutMode = animationOut; 3141 } 3142 3143 public AnimationIn getAnimationInMode() { 3144 return mAnimationInMode; 3145 } 3146 3147 public AnimationOut getAnimationOutMode() { 3148 return mAnimationOutMode; 3149 } 3150 3151 public GridAdapter getAdapter() { 3152 return mAdapter; 3153 } 3154 3155 public void setAdapter(GridAdapter adapter) { 3156 if (mAdapter != null) { 3157 mAdapter.unregisterDataSetObserver(mObserver); 3158 } 3159 3160 clearAllState(); 3161 3162 mAdapter = adapter; 3163 mDataChanged = true; 3164 mItemCount = adapter != null ? adapter.getCount() : 0; 3165 3166 if (adapter != null) { 3167 adapter.registerDataSetObserver(mObserver); 3168 mRecycler.setViewTypeCount(adapter.getViewTypeCount()); 3169 mHasStableIds = adapter.hasStableIds(); 3170 } else { 3171 mHasStableIds = false; 3172 } 3173 3174 if (isDragReorderingSupported()) { 3175 updateReorderStates(ReorderUtils.DRAG_STATE_NONE); 3176 } 3177 3178 updateEmptyStatus(); 3179 } 3180 3181 public void setAdapter(GridAdapter adapter, ScrollState scrollState) { 3182 setAdapter(adapter); 3183 mCurrentScrollState = scrollState; 3184 } 3185 3186 /** 3187 * Clear all state because the grid will be used for a completely different set of data. 3188 */ 3189 private void clearAllState() { 3190 // Clear all layout records and views 3191 mLayoutRecords.clear(); 3192 removeAllViews(); 3193 3194 mItemTops = null; 3195 mItemBottoms = null; 3196 3197 setSelectionToTop(); 3198 3199 // Clear recycler because there could be different view types now 3200 mRecycler.clear(); 3201 3202 // Reset the last touch y coordinate so that any animation/events won't use stale values. 3203 mLastTouchY = 0; 3204 3205 // Reset the first changed position to 0. At least we will update all views. 3206 mFirstChangedPosition = 0; 3207 } 3208 3209 /** 3210 * Scroll the list so the first visible position in the grid is the first item in the adapter. 3211 */ 3212 public void setSelectionToTop() { 3213 mCurrentScrollState = null; 3214 setFirstPositionAndOffsets(0 /* position */, getPaddingTop() /* offset */); 3215 } 3216 3217 /** 3218 * Get {@link #mFirstPosition}, which is the adapter position of the View 3219 * returned by getChildAt(0). 3220 */ 3221 public int getCurrentFirstPosition() { 3222 return mFirstPosition; 3223 } 3224 3225 /** 3226 * Indicate whether the scrolling state is currently at the topmost of this grid 3227 * @return boolean Indicates whether the current view is the top most of this grid. 3228 */ 3229 private boolean isSelectionAtTop() { 3230 if (mCurrentScrollState != null && mCurrentScrollState.getAdapterPosition() == 0) { 3231 // ScrollState is how far the top of the first child is from the top of the screen, and 3232 // does not include top padding when the adapter position is the first child. If the 3233 // vertical offset of the scroll state is exactly equal to {@link #mItemMargin}, then 3234 // the first item, and therefore the view of the grid, is at the top. 3235 return mCurrentScrollState.getVerticalOffset() == mItemMargin; 3236 } 3237 3238 return false; 3239 } 3240 3241 /** 3242 * Set the first position and offset so that on layout, we would start laying out starting 3243 * with the specified position at the top of the view. 3244 * @param position The child position to place at the top of this view. 3245 * @param offset The vertical layout offset of the view at the specified position. 3246 */ 3247 public void setFirstPositionAndOffsets(int position, int offset) { 3248 // Reset the first visible position in the grid to be item 0 3249 mFirstPosition = position; 3250 if (mItemTops == null || mItemBottoms == null) { 3251 mItemTops = new int[mColCount]; 3252 mItemBottoms = new int[mColCount]; 3253 } 3254 3255 calculateLayoutStartOffsets(offset); 3256 } 3257 3258 /** 3259 * Restore the view to the states specified by the {@link ScrollState}. 3260 * @param scrollState {@link ScrollState} containing the scroll states to restore to. 3261 */ 3262 private void restoreScrollPosition(ScrollState scrollState) { 3263 if (mAdapter == null || scrollState == null || mAdapter.getCount() == 0) { 3264 return; 3265 } 3266 3267 Log.v(TAG, "[restoreScrollPosition] " + scrollState); 3268 3269 int targetPosition = 0; 3270 long itemId = -1; 3271 3272 final int originalPosition = scrollState.getAdapterPosition(); 3273 final int adapterCount = mAdapter.getCount(); 3274 // ScrollState is defined as the vertical offset of the first item that is laid out 3275 // on screen. To restore scroll state, we check within a window to see if we can 3276 // find that original first item in this new data set. If we can, restore that item 3277 // to the first position on screen, offset by its previous vertical offset. If we 3278 // cannot find that item, then we'll simply layout out everything from the beginning 3279 // again. 3280 3281 // TODO: Perhaps it is more efficient if we check the cursor in one direction first 3282 // before going backwards, rather than jumping back and forth as we are doing now. 3283 for (int i = 0; i < SCROLL_RESTORE_WINDOW_SIZE; i++) { 3284 if (originalPosition + i < adapterCount) { 3285 itemId = mAdapter.getItemId(originalPosition + i); 3286 if (itemId != -1 && itemId == scrollState.getItemId()) { 3287 targetPosition = originalPosition + i; 3288 break; 3289 } 3290 } 3291 3292 if (originalPosition - i >= 0 && originalPosition - i < adapterCount) { 3293 itemId = mAdapter.getItemId(originalPosition - i); 3294 if (itemId != -1 && itemId == scrollState.getItemId()) { 3295 targetPosition = originalPosition - i; 3296 break; 3297 } 3298 } 3299 } 3300 3301 // layoutChildren(), fillDown() and fillUp() always apply mItemMargin when laying out 3302 // views. Since restoring scroll position is effectively laying out a particular child 3303 // as the first child, we need to ensure we strip mItemMargin from the offset, as it 3304 // will be re-applied when the view is laid out. 3305 // 3306 // Since top padding varies with screen orientation and is not stored in the scroll 3307 // state when the scroll adapter position is the first child, we add it here. 3308 int offset = scrollState.getVerticalOffset() - mItemMargin; 3309 if (targetPosition == 0) { 3310 offset += getPaddingTop(); 3311 } 3312 3313 setFirstPositionAndOffsets(targetPosition, offset); 3314 mCurrentScrollState = null; 3315 } 3316 3317 /** 3318 * Return the current scroll state of this view. 3319 * @return {@link ScrollState} The current scroll state 3320 */ 3321 public ScrollState getScrollState() { 3322 final View v = getChildAt(0); 3323 if (v == null) { 3324 return null; 3325 } 3326 3327 final LayoutParams lp = (LayoutParams) v.getLayoutParams(); 3328 // Since top padding varies with screen orientation, it is not stored in the scroll state 3329 // when the scroll adapter position is the first child. 3330 final int offset = (lp.position == 0 ? v.getTop() - getPaddingTop() : v.getTop()); 3331 return new ScrollState(lp.id, lp.position, offset); 3332 } 3333 3334 /** 3335 * NOTE This method is borrowed from {@link ScrollView}. 3336 */ 3337 @Override 3338 public boolean requestChildRectangleOnScreen(View child, Rect rectangle, 3339 boolean immediate) { 3340 // offset into coordinate space of this scroll view 3341 rectangle.offset(child.getLeft() - child.getScrollX(), 3342 child.getTop() - child.getScrollY()); 3343 3344 return scrollToChildRect(rectangle, immediate); 3345 } 3346 3347 /** 3348 * If rect is off screen, scroll just enough to get it (or at least the 3349 * first screen size chunk of it) on screen. 3350 * NOTE This method is borrowed from {@link ScrollView}. 3351 * 3352 * @param rect The rectangle. 3353 * @param immediate True to scroll immediately without animation. Not used here. 3354 * @return true if scrolling was performed 3355 */ 3356 private boolean scrollToChildRect(Rect rect, boolean immediate) { 3357 final int delta = computeScrollDeltaToGetChildRectOnScreen(rect); 3358 final boolean scroll = delta != 0; 3359 if (scroll) { 3360 // TODO smoothScrollBy if immediate is false. 3361 scrollBy(0, delta); 3362 } 3363 return scroll; 3364 } 3365 3366 @Override 3367 protected void onSizeChanged(int w, int h, int oldw, int oldh) { 3368 super.onSizeChanged(w, h, oldw, oldh); 3369 3370 if (mOnSizeChangedListener != null) { 3371 mOnSizeChangedListener.onSizeChanged(w, h, oldw, oldh); 3372 } 3373 3374 // NOTE Below is borrowed from {@link ScrollView}. 3375 final View currentFocused = findFocus(); 3376 if (null == currentFocused || this == currentFocused) { 3377 return; 3378 } 3379 3380 // If the currently-focused view was visible on the screen when the 3381 // screen was at the old height, then scroll the screen to make that 3382 // view visible with the new screen height. 3383 if (isWithinDeltaOfScreen(currentFocused, 0, oldh)) { 3384 currentFocused.getDrawingRect(mTempRect); 3385 offsetDescendantRectToMyCoords(currentFocused, mTempRect); 3386 scrollBy(0, computeScrollDeltaToGetChildRectOnScreen(mTempRect)); 3387 } 3388 } 3389 3390 /** 3391 * 3392 * NOTE This method is borrowed from {@link ScrollView}. 3393 * 3394 * @return whether the descendant of this scroll view is within delta 3395 * pixels of being on the screen. 3396 */ 3397 private boolean isWithinDeltaOfScreen(View descendant, int delta, int height) { 3398 descendant.getDrawingRect(mTempRect); 3399 offsetDescendantRectToMyCoords(descendant, mTempRect); 3400 3401 return (mTempRect.bottom + delta) >= getScrollY() 3402 && (mTempRect.top - delta) <= (getScrollY() + height); 3403 } 3404 3405 /** 3406 * NOTE: borrowed from {@link GridView} 3407 * Comments from {@link View} 3408 * 3409 * Compute the vertical extent of the vertical scrollbar's thumb within the vertical range. 3410 * This value is used to compute the length of the thumb within the scrollbar's track. 3411 * The range is expressed in arbitrary units that must be the same as the units used by 3412 * {@link #computeVerticalScrollRange} and {@link #computeVerticalScrollOffset}. 3413 * 3414 * The default extent is the drawing height of this view. 3415 * 3416 * @return the vertical extent of the scrollbar's thumb 3417 */ 3418 @Override 3419 protected int computeVerticalScrollExtent() { 3420 3421 final int count = getChildCount(); 3422 if (count > 0) { 3423 if (mSmoothScrollbarEnabled) { 3424 final int rowCount = (count + mColCount - 1) / mColCount; 3425 int extent = rowCount * SCROLLING_ESTIMATED_ITEM_HEIGHT; 3426 3427 View view = getChildAt(0); 3428 final int top = view.getTop(); 3429 int height = view.getHeight(); 3430 if (height > 0) { 3431 extent += (top * SCROLLING_ESTIMATED_ITEM_HEIGHT) / height; 3432 } 3433 3434 view = getChildAt(count - 1); 3435 final int bottom = view.getBottom(); 3436 height = view.getHeight(); 3437 if (height > 0) { 3438 extent -= ((bottom - getHeight()) * SCROLLING_ESTIMATED_ITEM_HEIGHT) / height; 3439 } 3440 3441 return extent; 3442 } else { 3443 return 1; 3444 } 3445 } 3446 return 0; 3447 } 3448 3449 /** 3450 * NOTE: borrowed from {@link GridView} and altered as appropriate to accommodate for 3451 * {@link StaggeredGridView} 3452 * 3453 * Comments from {@link View} 3454 * 3455 * Compute the vertical offset of the vertical scrollbar's thumb within the horizontal range. 3456 * This value is used to compute the position of the thumb within the scrollbar's track. 3457 * The range is expressed in arbitrary units that must be the same as the units used by 3458 * {@link #computeVerticalScrollRange()} and {@link #computeVerticalScrollExtent()}. 3459 * 3460 * The default offset is the scroll offset of this view. 3461 * 3462 * @return the vertical offset of the scrollbar's thumb 3463 */ 3464 @Override 3465 protected int computeVerticalScrollOffset() { 3466 final int firstPosition = mFirstPosition; 3467 final int childCount = getChildCount(); 3468 final int paddingTop = getPaddingTop(); 3469 3470 if (firstPosition >= 0 && childCount > 0) { 3471 if (mSmoothScrollbarEnabled) { 3472 final View view = getChildAt(0); 3473 final int top = view.getTop(); 3474 final int currentTopViewHeight = view.getHeight(); 3475 if (currentTopViewHeight > 0) { 3476 // In an ideal world, all items would have a fixed height that we would know 3477 // a priori, calculating the scroll offset would simply be: 3478 // [A] (mFirstPosition * fixedHeight) - childView[0].top 3479 // where childView[0] is the first view on screen. 3480 // 3481 // However, given that we do not know the height ahead of time, and that each 3482 // item in this grid can have varying heights, we'd need to assign an arbitrary 3483 // item height (SCROLLING_ESTIMATED_ITEM_HEIGHT) in order to estimate the scroll 3484 // offset. The previous equation thus transforms to: 3485 // [B] (mFirstPosition * SCROLLING_ESTIMATED_ITEM_HEIGHT) - 3486 // ((childView[0].top * SCROLLING_ESTIMATED_ITEM_HEIGHT) / 3487 // childView[0].height) 3488 // 3489 // Equation [B] gives a pretty good calculation of the offset if this were a 3490 // single column grid view, for a multi-column grid, one slight modification is 3491 // needed: 3492 // [C] ((mFirstPosition * SCROLLING_ESTIMATED_ITEM_HEIGHT) / mColCount) - 3493 // ((childView[0].top * SCROLLING_ESTIMATED_ITEM_HEIGHT) / 3494 // childView[0].height) 3495 final int estimatedScrollOffset = 3496 ((firstPosition * SCROLLING_ESTIMATED_ITEM_HEIGHT) / mColCount) - 3497 ((top * SCROLLING_ESTIMATED_ITEM_HEIGHT) / currentTopViewHeight); 3498 3499 final int rowCount = (mItemCount + mColCount - 1) / mColCount; 3500 final int overScrollCompensation = (int) ((float) getScrollY() / getHeight() * 3501 rowCount * SCROLLING_ESTIMATED_ITEM_HEIGHT); 3502 3503 int val = Math.max(estimatedScrollOffset + overScrollCompensation, 0); 3504 // If mFirstPosition is currently the very first item in the adapter, check to 3505 // see if we need to take into account any top padding. This is so that we 3506 // don't return 0 when in fact the user may still be scrolling through some 3507 // top padding. 3508 if (firstPosition == 0 && paddingTop > 0) { 3509 val += paddingTop - top + mItemMargin; 3510 } 3511 return val; 3512 } 3513 } else { 3514 int index; 3515 final int count = mItemCount; 3516 if (firstPosition == 0) { 3517 index = 0; 3518 } else if (firstPosition + childCount == count) { 3519 index = count; 3520 } else { 3521 index = firstPosition + childCount / 2; 3522 } 3523 return (int) (firstPosition + childCount * (index / (float) count)); 3524 } 3525 } 3526 3527 return paddingTop; 3528 } 3529 3530 /** 3531 * NOTE: borrowed from {@link GridView} and altered as appropriate to accommodate for 3532 * {@link StaggeredGridView} 3533 * 3534 * Comments from {@link View} 3535 * 3536 * Compute the vertical range that the vertical scrollbar represents. 3537 * The range is expressed in arbitrary units that must be the same as the units used by 3538 * {@link #computeVerticalScrollExtent} and {@link #computeVerticalScrollOffset}. 3539 * 3540 * The default range is the drawing height of this view. 3541 * 3542 * @return the total vertical range represented by the vertical scrollbar 3543 */ 3544 @Override 3545 protected int computeVerticalScrollRange() { 3546 final int rowCount = (mItemCount + mColCount - 1) / mColCount; 3547 int result = Math.max(rowCount * SCROLLING_ESTIMATED_ITEM_HEIGHT, 0); 3548 3549 if (mSmoothScrollbarEnabled) { 3550 if (getScrollY() != 0) { 3551 // Compensate for overscroll 3552 result += Math.abs((int) ((float) getScrollY() / getHeight() * rowCount 3553 * SCROLLING_ESTIMATED_ITEM_HEIGHT)); 3554 } 3555 } else { 3556 result = mItemCount; 3557 } 3558 3559 return result; 3560 } 3561 3562 /** 3563 * Compute the amount to scroll in the Y direction in order to get 3564 * a rectangle completely on the screen (or, if taller than the screen, 3565 * at least the first screen size chunk of it). 3566 * 3567 * NOTE This method is borrowed from {@link ScrollView}. 3568 * 3569 * @param rect The rect. 3570 * @return The scroll delta. 3571 */ 3572 protected int computeScrollDeltaToGetChildRectOnScreen(Rect rect) { 3573 if (getChildCount() == 0) { 3574 return 0; 3575 } 3576 3577 final int height = getHeight(); 3578 final int fadingEdge = getVerticalFadingEdgeLength(); 3579 3580 int screenTop = getScrollY(); 3581 int screenBottom = screenTop + height; 3582 3583 // leave room for top fading edge as long as rect isn't at very top 3584 if (rect.top > 0) { 3585 screenTop += fadingEdge; 3586 } 3587 3588 // leave room for bottom fading edge as long as rect isn't at very bottom 3589 if (rect.bottom < getHeight()) { 3590 screenBottom -= fadingEdge; 3591 } 3592 3593 int scrollYDelta = 0; 3594 3595 if (rect.bottom > screenBottom && rect.top > screenTop) { 3596 // need to move down to get it in view: move down just enough so 3597 // that the entire rectangle is in view (or at least the first 3598 // screen size chunk). 3599 3600 if (rect.height() > height) { 3601 // just enough to get screen size chunk on 3602 scrollYDelta = screenTop - rect.top; 3603 } else { 3604 // get entire rect at bottom of screen 3605 scrollYDelta = screenBottom - rect.bottom; 3606 } 3607 } else if (rect.top < screenTop && rect.bottom < screenBottom) { 3608 // need to move up to get it in view: move up just enough so that 3609 // entire rectangle is in view (or at least the first screen 3610 // size chunk of it). 3611 3612 if (rect.height() > height) { 3613 // screen size chunk 3614 scrollYDelta = screenBottom - rect.bottom; 3615 } else { 3616 // entire rect at top 3617 scrollYDelta = screenTop - rect.top; 3618 } 3619 } 3620 return scrollYDelta; 3621 } 3622 3623 @Override 3624 protected LayoutParams generateDefaultLayoutParams() { 3625 return new LayoutParams(LayoutParams.WRAP_CONTENT); 3626 } 3627 3628 @Override 3629 protected LayoutParams generateLayoutParams(ViewGroup.LayoutParams lp) { 3630 return new LayoutParams(lp); 3631 } 3632 3633 @Override 3634 protected boolean checkLayoutParams(ViewGroup.LayoutParams lp) { 3635 return lp instanceof LayoutParams; 3636 } 3637 3638 @Override 3639 public ViewGroup.LayoutParams generateLayoutParams(AttributeSet attrs) { 3640 return new LayoutParams(getContext(), attrs); 3641 } 3642 3643 @Override 3644 public Parcelable onSaveInstanceState() { 3645 final Parcelable superState = super.onSaveInstanceState(); 3646 final SavedState ss = new SavedState(superState); 3647 final int position = mFirstPosition; 3648 ss.position = position; 3649 if (position >= 0 && mAdapter != null && position < mAdapter.getCount()) { 3650 ss.firstId = mAdapter.getItemId(position); 3651 } 3652 if (getChildCount() > 0) { 3653 // Since top padding varies with screen orientation, it is not stored in the scroll 3654 // state when the scroll adapter position is the first child. 3655 ss.topOffset = position == 0 ? 3656 getChildAt(0).getTop() - getPaddingTop() : getChildAt(0).getTop(); 3657 } 3658 return ss; 3659 } 3660 3661 @Override 3662 public void onRestoreInstanceState(Parcelable state) { 3663 final SavedState ss = (SavedState) state; 3664 super.onRestoreInstanceState(ss.getSuperState()); 3665 mDataChanged = true; 3666 mFirstPosition = ss.position; 3667 mCurrentScrollState = new ScrollState(ss.firstId, ss.position, ss.topOffset); 3668 requestLayout(); 3669 } 3670 3671 public static class LayoutParams extends ViewGroup.LayoutParams { 3672 private static final int[] LAYOUT_ATTRS = new int[] { 3673 android.R.attr.layout_span 3674 }; 3675 3676 private static final int SPAN_INDEX = 0; 3677 3678 /** 3679 * The number of columns this item should span 3680 */ 3681 public int span = 1; 3682 3683 /** 3684 * Item position this view represents 3685 */ 3686 public int position = -1; 3687 3688 /** 3689 * Type of this view as reported by the adapter 3690 */ 3691 int viewType; 3692 3693 /** 3694 * The column this view is occupying 3695 */ 3696 int column; 3697 3698 /** 3699 * The stable ID of the item this view displays 3700 */ 3701 long id = -1; 3702 3703 /** 3704 * The position where reordering can happen for this view 3705 */ 3706 public int reorderingArea = ReorderUtils.REORDER_AREA_NONE; 3707 3708 public LayoutParams(int height) { 3709 super(MATCH_PARENT, height); 3710 3711 if (this.height == MATCH_PARENT) { 3712 Log.w(TAG, "Constructing LayoutParams with height FILL_PARENT - " + 3713 "impossible! Falling back to WRAP_CONTENT"); 3714 this.height = WRAP_CONTENT; 3715 } 3716 } 3717 3718 public LayoutParams(Context c, AttributeSet attrs) { 3719 super(c, attrs); 3720 3721 if (this.width != MATCH_PARENT) { 3722 Log.w(TAG, "Inflation setting LayoutParams width to " + this.width + 3723 " - must be MATCH_PARENT"); 3724 this.width = MATCH_PARENT; 3725 } 3726 if (this.height == MATCH_PARENT) { 3727 Log.w(TAG, "Inflation setting LayoutParams height to MATCH_PARENT - " + 3728 "impossible! Falling back to WRAP_CONTENT"); 3729 this.height = WRAP_CONTENT; 3730 } 3731 3732 final TypedArray a = c.obtainStyledAttributes(attrs, LAYOUT_ATTRS); 3733 span = a.getInteger(SPAN_INDEX, 1); 3734 a.recycle(); 3735 } 3736 3737 public LayoutParams(ViewGroup.LayoutParams other) { 3738 super(other); 3739 3740 if (this.width != MATCH_PARENT) { 3741 Log.w(TAG, "Constructing LayoutParams with width " + this.width + 3742 " - must be MATCH_PARENT"); 3743 this.width = MATCH_PARENT; 3744 } 3745 if (this.height == MATCH_PARENT) { 3746 Log.w(TAG, "Constructing LayoutParams with height MATCH_PARENT - " + 3747 "impossible! Falling back to WRAP_CONTENT"); 3748 this.height = WRAP_CONTENT; 3749 } 3750 } 3751 } 3752 3753 private class RecycleBin { 3754 private ArrayList<View>[] mScrapViews; 3755 private int mViewTypeCount; 3756 private int mMaxScrap; 3757 3758 private SparseArray<View> mTransientStateViews; 3759 3760 public void setViewTypeCount(int viewTypeCount) { 3761 if (viewTypeCount < 1) { 3762 throw new IllegalArgumentException("Must have at least one view type (" + 3763 viewTypeCount + " types reported)"); 3764 } 3765 if (viewTypeCount == mViewTypeCount) { 3766 return; 3767 } 3768 3769 final ArrayList<View>[] scrapViews = new ArrayList[viewTypeCount]; 3770 for (int i = 0; i < viewTypeCount; i++) { 3771 scrapViews[i] = new ArrayList<View>(); 3772 } 3773 mViewTypeCount = viewTypeCount; 3774 mScrapViews = scrapViews; 3775 } 3776 3777 public void clear() { 3778 final int typeCount = mViewTypeCount; 3779 for (int i = 0; i < typeCount; i++) { 3780 mScrapViews[i].clear(); 3781 } 3782 if (mTransientStateViews != null) { 3783 mTransientStateViews.clear(); 3784 } 3785 } 3786 3787 public void clearTransientViews() { 3788 if (mTransientStateViews != null) { 3789 mTransientStateViews.clear(); 3790 } 3791 } 3792 3793 public void addScrap(View v) { 3794 if (!(v.getLayoutParams() instanceof LayoutParams)) { 3795 return; 3796 } 3797 3798 final LayoutParams lp = (LayoutParams) v.getLayoutParams(); 3799 if (ViewCompat.hasTransientState(v)) { 3800 if (mTransientStateViews == null) { 3801 mTransientStateViews = new SparseArray<View>(); 3802 } 3803 mTransientStateViews.put(lp.position, v); 3804 return; 3805 } 3806 3807 final int childCount = getChildCount(); 3808 if (childCount > mMaxScrap) { 3809 mMaxScrap = childCount; 3810 } 3811 3812 // Clear possible modified states applied to the view when adding to the recycler. 3813 // This view may have been part of a cancelled animation, so clear that state so that 3814 // future consumer of this view won't have to deal with states from its past life. 3815 v.setTranslationX(0); 3816 v.setTranslationY(0); 3817 v.setRotation(0); 3818 v.setAlpha(1.0f); 3819 v.setScaleY(1.0f); 3820 3821 final ArrayList<View> scrap = mScrapViews[lp.viewType]; 3822 if (scrap.size() < mMaxScrap) { 3823 // The number of scraps have not yet exceeded our limit, check to see that this 3824 // view does not already exist in the recycler. This can happen if a caller 3825 // mistakenly calls addScrap(view) multiple times for the same view. 3826 if (!scrap.contains(v)) { 3827 scrap.add(v); 3828 } 3829 } 3830 } 3831 3832 public View getTransientStateView(int position) { 3833 if (mTransientStateViews == null) { 3834 return null; 3835 } 3836 3837 final View result = mTransientStateViews.get(position); 3838 if (result != null) { 3839 mTransientStateViews.remove(position); 3840 } 3841 return result; 3842 } 3843 3844 public View getScrapView(int type) { 3845 final ArrayList<View> scrap = mScrapViews[type]; 3846 if (scrap.isEmpty()) { 3847 return null; 3848 } 3849 3850 final int index = scrap.size() - 1; 3851 final View result = scrap.remove(index); 3852 3853 return result; 3854 } 3855 3856 // TODO: Implement support to maintain a list of active views so that we can make use of 3857 // stable ids to retrieve the same view that is currently laid out for a particular item. 3858 // Currently, all views "recycled" are shoved into the same collection, this may not be 3859 // the most effective way. Refer to the RecycleBin as implemented for AbsListView. 3860 public View getView(int type, long stableId) { 3861 final ArrayList<View> scrap = mScrapViews[type]; 3862 if (scrap.isEmpty()) { 3863 return null; 3864 } 3865 3866 for (int i = 0; i < scrap.size(); i++) { 3867 final View v = scrap.get(i); 3868 final LayoutParams lp = (LayoutParams) v.getLayoutParams(); 3869 if (lp.id == stableId) { 3870 scrap.remove(i); 3871 return v; 3872 } 3873 } 3874 3875 return null; 3876 } 3877 } 3878 3879 private class AdapterDataSetObserver extends DataSetObserver { 3880 @Override 3881 public void onChanged() { 3882 mDataChanged = true; 3883 3884 mItemCount = mAdapter.getCount(); 3885 mFirstChangedPosition = mAdapter.getFirstChangedPosition(); 3886 if (mFirstPosition >= mItemCount) { 3887 // If the latest data set has fewer data items than mFirstPosition, we will not be 3888 // able to accurately restore scroll state, so just reset to the top. 3889 mFirstPosition = 0; 3890 mCurrentScrollState = null; 3891 } 3892 3893 // TODO: Consider matching these back up if we have stable IDs. 3894 mRecycler.clearTransientViews(); 3895 3896 if (mHasStableIds) { 3897 // If we will animate the transition to the new layout, cache the current positions 3898 // of the visible children. This is before any views get removed below. 3899 cacheChildRects(); 3900 } else { 3901 // Clear all layout records 3902 mLayoutRecords.clear(); 3903 3904 // Reset item bottoms to be equal to item tops 3905 final int colCount = mColCount; 3906 for (int i = 0; i < colCount; i++) { 3907 mItemBottoms[i] = mItemTops[i]; 3908 } 3909 } 3910 3911 updateEmptyStatus(); 3912 3913 // TODO: consider repopulating in a deferred runnable instead 3914 // (so that successive changes may still be batched) 3915 requestLayout(); 3916 } 3917 3918 @Override 3919 public void onInvalidated() { 3920 } 3921 } 3922 3923 static class SavedState extends BaseSavedState { 3924 long firstId = -1; 3925 int position; 3926 3927 // topOffset is the vertical value that the view specified by position should 3928 // start rendering from. If it is 0, the view would be at the top of the grid. 3929 int topOffset; 3930 3931 SavedState(Parcelable superState) { 3932 super(superState); 3933 } 3934 3935 private SavedState(Parcel in) { 3936 super(in); 3937 firstId = in.readLong(); 3938 position = in.readInt(); 3939 topOffset = in.readInt(); 3940 } 3941 3942 @Override 3943 public void writeToParcel(Parcel out, int flags) { 3944 super.writeToParcel(out, flags); 3945 out.writeLong(firstId); 3946 out.writeInt(position); 3947 out.writeInt(topOffset); 3948 } 3949 3950 @Override 3951 public String toString() { 3952 return "StaggereGridView.SavedState{" 3953 + Integer.toHexString(System.identityHashCode(this)) 3954 + " firstId=" + firstId 3955 + " position=" + position + "}"; 3956 } 3957 3958 public static final Parcelable.Creator<SavedState> CREATOR 3959 = new Parcelable.Creator<SavedState>() { 3960 @Override 3961 public SavedState createFromParcel(Parcel in) { 3962 return new SavedState(in); 3963 } 3964 3965 @Override 3966 public SavedState[] newArray(int size) { 3967 return new SavedState[size]; 3968 } 3969 }; 3970 } 3971 3972 public void setDropListener(ReorderListener listener) { 3973 mReorderHelper = new ReorderHelper(listener, this); 3974 } 3975 3976 public void setScrollListener(ScrollListener listener) { 3977 mScrollListener = listener; 3978 } 3979 3980 public void setOnSizeChangedListener(OnSizeChangedListener listener) { 3981 mOnSizeChangedListener = listener; 3982 } 3983 3984 /** 3985 * Helper class to store a {@link View} with its corresponding layout positions 3986 * as a {@link Rect}. 3987 */ 3988 private static class ViewRectPair { 3989 public final View view; 3990 public final Rect rect; 3991 3992 public ViewRectPair(View v, Rect r) { 3993 view = v; 3994 rect = r; 3995 } 3996 } 3997 3998 public static class ScrollState implements Parcelable { 3999 private final long mItemId; 4000 private final int mAdapterPosition; 4001 4002 // The offset that the view specified by mAdapterPosition should start rendering from. If 4003 // this value is 0, then the view would be rendered from the very top of this grid. 4004 private int mVerticalOffset; 4005 4006 public ScrollState(long itemId, int adapterPosition, int offset) { 4007 mItemId = itemId; 4008 mAdapterPosition = adapterPosition; 4009 mVerticalOffset = offset; 4010 } 4011 4012 private ScrollState(Parcel in) { 4013 mItemId = in.readLong(); 4014 mAdapterPosition = in.readInt(); 4015 mVerticalOffset = in.readInt(); 4016 } 4017 4018 public long getItemId() { 4019 return mItemId; 4020 } 4021 4022 public int getAdapterPosition() { 4023 return mAdapterPosition; 4024 } 4025 4026 public void setVerticalOffset(int offset) { 4027 mVerticalOffset = offset; 4028 } 4029 4030 public int getVerticalOffset() { 4031 return mVerticalOffset; 4032 } 4033 4034 @Override 4035 public int describeContents() { 4036 return 0; 4037 } 4038 4039 @Override 4040 public void writeToParcel(Parcel dest, int flags) { 4041 dest.writeLong(mItemId); 4042 dest.writeInt(mAdapterPosition); 4043 dest.writeInt(mVerticalOffset); 4044 } 4045 4046 public static final Parcelable.Creator<ScrollState> CREATOR = 4047 new Parcelable.Creator<ScrollState>() { 4048 @Override 4049 public ScrollState createFromParcel(Parcel source) { 4050 return new ScrollState(source); 4051 } 4052 4053 @Override 4054 public ScrollState[] newArray(int size) { 4055 return new ScrollState[size]; 4056 } 4057 }; 4058 4059 @Override 4060 public String toString() { 4061 return "ScrollState {mItemId=" + mItemId + 4062 " mAdapterPosition=" + mAdapterPosition + 4063 " mVerticalOffset=" + mVerticalOffset + "}"; 4064 } 4065 } 4066 4067 /** 4068 * Listener of {@Link StaggeredGridView} for grid size change. 4069 */ 4070 public interface OnSizeChangedListener { 4071 void onSizeChanged(int width, int height, int oldWidth, int oldHeight); 4072 } 4073 4074 /** 4075 * Listener of {@Link StaggeredGridView} for scroll change. 4076 */ 4077 public interface ScrollListener { 4078 4079 /** 4080 * Called when scroll happens on this view. 4081 * 4082 * @param offset The scroll offset amount. 4083 * @param currentScrollY The current y position of this view. 4084 * @param maxScrollY The maximum amount of scroll possible in this view. 4085 */ 4086 void onScrollChanged(int offset, int currentScrollY, int maxScrollY); 4087 } 4088 4089 /** 4090 * Listener of {@link StaggeredGridView} for animations. This listener is responsible 4091 * for playing all animations created by this {@link StaggeredGridView} 4092 */ 4093 public interface AnimationListener { 4094 /** 4095 * Called when animations are ready to be played 4096 * @param animationMode The current animation mode based on the state of the data. Valid 4097 * animation modes are {@link ANIMATION_MODE_NONE}, {@link ANIMATION_MODE_NEW_DATA}, and 4098 * {@link ANIMATION_MODE_UPDATE_DATA}. 4099 * @param animators The list of animators to be played 4100 */ 4101 void onAnimationReady(int animationMode, List<Animator> animators); 4102 } 4103 4104 /** 4105 * Listener of {@link StaggeredGridView} for drag and drop reordering of child views. 4106 */ 4107 public interface ReorderListener { 4108 4109 /** 4110 * onPickedUp is called to notify listeners that an item has been picked up for reordering. 4111 * @param draggedChild the original child view that picked up. 4112 */ 4113 void onPickedUp(View draggedChild); 4114 4115 /** 4116 * onDrop is called to notify listeners that an intent to drop the 4117 * item at position "from" over the position "target" 4118 * @param draggedView the original child view that was dropped 4119 * @param sourcePosition the original position where the item was dragged from 4120 * @param targetPosition the target position where the item is dropped at 4121 */ 4122 void onDrop(View draggedView, int sourcePosition, int targetPosition); 4123 4124 /** 4125 * onCancelDrag is called to notify listeners that the drag event has been cancelled. 4126 * @param draggediew the original child view that was dragged. 4127 */ 4128 void onCancelDrag(View draggediew); 4129 4130 /** 4131 * onReorder is called to notify listeners that an intent to move the 4132 * item at position "from" to position "to" 4133 * @param draggedView the original child view that was dragged 4134 * @param id id of the original item that was picked up 4135 * @param from 4136 * @param to the target position where the item is dropped at 4137 */ 4138 boolean onReorder(View draggedView, long id, int from, int to); 4139 4140 /** 4141 * Event handler for a drag entering the {@link StaggeredGridView} element's 4142 * reordering area. 4143 * @param view The child view that just received an enter event on the reordering area. 4144 * @param position The adapter position of the view that just received an enter event. 4145 */ 4146 void onEnterReorderArea(View view, int position); 4147 } 4148 } 4149