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.ex.widget; 18 19 import android.content.Context; 20 import android.content.res.TypedArray; 21 import android.database.DataSetObserver; 22 import android.graphics.Canvas; 23 import android.os.Parcel; 24 import android.os.Parcelable; 25 import android.support.v4.util.SparseArrayCompat; 26 import android.support.v4.view.MotionEventCompat; 27 import android.support.v4.view.VelocityTrackerCompat; 28 import android.support.v4.view.ViewCompat; 29 import android.util.AttributeSet; 30 import android.util.Log; 31 import android.util.SparseArray; 32 import android.view.MotionEvent; 33 import android.view.VelocityTracker; 34 import android.view.View; 35 import android.view.ViewConfiguration; 36 import android.view.ViewGroup; 37 import android.widget.ListAdapter; 38 39 import java.util.ArrayList; 40 import java.util.Arrays; 41 42 /** 43 * ListView and GridView just not complex enough? Try StaggeredGridView! 44 * 45 * <p>StaggeredGridView presents a multi-column grid with consistent column sizes 46 * but varying row sizes between the columns. Each successive item from a 47 * {@link android.widget.ListAdapter ListAdapter} will be arranged from top to bottom, 48 * left to right. The largest vertical gap is always filled first.</p> 49 * 50 * <p>Item views may span multiple columns as specified by their {@link LayoutParams}. 51 * The attribute <code>android:layout_span</code> may be used when inflating 52 * item views from xml.</p> 53 * 54 * <p>This class is still under development and is not fully functional yet.</p> 55 */ 56 public class StaggeredGridView extends ViewGroup { 57 private static final String TAG = "StaggeredGridView"; 58 private static final boolean DEBUG = false; 59 60 /* 61 * There are a few things you should know if you're going to make modifications 62 * to StaggeredGridView. 63 * 64 * Like ListView, SGV populates from an adapter and recycles views that fall out 65 * of the visible boundaries of the grid. A few invariants always hold: 66 * 67 * - mFirstPosition is the adapter position of the View returned by getChildAt(0). 68 * - Any child index can be translated to an adapter position by adding mFirstPosition. 69 * - Any adapter position can be translated to a child index by subtracting mFirstPosition. 70 * - Views for items in the range [mFirstPosition, mFirstPosition + getChildCount()) are 71 * currently attached to the grid as children. All other adapter positions do not have 72 * active views. 73 * 74 * This means a few things thanks to the staggered grid's nature. Some views may stay attached 75 * long after they have scrolled offscreen if removing and recycling them would result in 76 * breaking one of the invariants above. 77 * 78 * LayoutRecords are used to track data about a particular item's layout after the associated 79 * view has been removed. These let positioning and the choice of column for an item 80 * remain consistent even though the rules for filling content up vs. filling down vary. 81 * 82 * Whenever layout parameters for a known LayoutRecord change, other LayoutRecords before 83 * or after it may need to be invalidated. e.g. if the item's height or the number 84 * of columns it spans changes, all bets for other items in the same direction are off 85 * since the cached information no longer applies. 86 */ 87 88 private ListAdapter mAdapter; 89 90 public static final int COLUMN_COUNT_AUTO = -1; 91 92 private int mColCountSetting = 2; 93 private int mColCount = 2; 94 private int mMinColWidth = 0; 95 private int mItemMargin; 96 97 private int[] mItemTops; 98 private int[] mItemBottoms; 99 100 private boolean mFastChildLayout; 101 private boolean mPopulating; 102 private boolean mForcePopulateOnLayout; 103 private boolean mInLayout; 104 private int mRestoreOffset; 105 106 private final RecycleBin mRecycler = new RecycleBin(); 107 108 private final AdapterDataSetObserver mObserver = new AdapterDataSetObserver(); 109 110 private boolean mDataChanged; 111 private int mOldItemCount; 112 private int mItemCount; 113 private boolean mHasStableIds; 114 115 private int mFirstPosition; 116 117 private int mTouchSlop; 118 private int mMaximumVelocity; 119 private int mFlingVelocity; 120 private float mLastTouchY; 121 private float mTouchRemainderY; 122 private int mActivePointerId; 123 124 private static final int TOUCH_MODE_IDLE = 0; 125 private static final int TOUCH_MODE_DRAGGING = 1; 126 private static final int TOUCH_MODE_FLINGING = 2; 127 128 private int mTouchMode; 129 private final VelocityTracker mVelocityTracker = VelocityTracker.obtain(); 130 private final ScrollerCompat mScroller; 131 132 private final EdgeEffectCompat mTopEdge; 133 private final EdgeEffectCompat mBottomEdge; 134 135 private static final class LayoutRecord { 136 public int column; 137 public long id = -1; 138 public int height; 139 public int span; 140 private int[] mMargins; 141 ensureMargins()142 private final void ensureMargins() { 143 if (mMargins == null) { 144 // Don't need to confirm length; 145 // all layoutrecords are purged when column count changes. 146 mMargins = new int[span * 2]; 147 } 148 } 149 getMarginAbove(int col)150 public final int getMarginAbove(int col) { 151 if (mMargins == null) { 152 return 0; 153 } 154 return mMargins[col * 2]; 155 } 156 getMarginBelow(int col)157 public final int getMarginBelow(int col) { 158 if (mMargins == null) { 159 return 0; 160 } 161 return mMargins[col * 2 + 1]; 162 } 163 setMarginAbove(int col, int margin)164 public final void setMarginAbove(int col, int margin) { 165 if (mMargins == null && margin == 0) { 166 return; 167 } 168 ensureMargins(); 169 mMargins[col * 2] = margin; 170 } 171 setMarginBelow(int col, int margin)172 public final void setMarginBelow(int col, int margin) { 173 if (mMargins == null && margin == 0) { 174 return; 175 } 176 ensureMargins(); 177 mMargins[col * 2 + 1] = margin; 178 } 179 180 @Override toString()181 public String toString() { 182 String result = "LayoutRecord{c=" + column + ", id=" + id + " h=" + height + 183 " s=" + span; 184 if (mMargins != null) { 185 result += " margins[above, below]("; 186 for (int i = 0; i < mMargins.length; i += 2) { 187 result += "[" + mMargins[i] + ", " + mMargins[i+1] + "]"; 188 } 189 result += ")"; 190 } 191 return result + "}"; 192 } 193 } 194 private final SparseArrayCompat<LayoutRecord> mLayoutRecords = 195 new SparseArrayCompat<LayoutRecord>(); 196 StaggeredGridView(Context context)197 public StaggeredGridView(Context context) { 198 this(context, null); 199 } 200 StaggeredGridView(Context context, AttributeSet attrs)201 public StaggeredGridView(Context context, AttributeSet attrs) { 202 this(context, attrs, 0); 203 } 204 StaggeredGridView(Context context, AttributeSet attrs, int defStyle)205 public StaggeredGridView(Context context, AttributeSet attrs, int defStyle) { 206 super(context, attrs, defStyle); 207 208 final ViewConfiguration vc = ViewConfiguration.get(context); 209 mTouchSlop = vc.getScaledTouchSlop(); 210 mMaximumVelocity = vc.getScaledMaximumFlingVelocity(); 211 mFlingVelocity = vc.getScaledMinimumFlingVelocity(); 212 mScroller = ScrollerCompat.from(context); 213 214 mTopEdge = new EdgeEffectCompat(context); 215 mBottomEdge = new EdgeEffectCompat(context); 216 setWillNotDraw(false); 217 setClipToPadding(false); 218 } 219 220 /** 221 * Set a fixed number of columns for this grid. Space will be divided evenly 222 * among all columns, respecting the item margin between columns. 223 * The default is 2. (If it were 1, perhaps you should be using a 224 * {@link android.widget.ListView ListView}.) 225 * 226 * @param colCount Number of columns to display. 227 * @see #setMinColumnWidth(int) 228 */ setColumnCount(int colCount)229 public void setColumnCount(int colCount) { 230 if (colCount < 1 && colCount != COLUMN_COUNT_AUTO) { 231 throw new IllegalArgumentException("Column count must be at least 1 - received " + 232 colCount); 233 } 234 final boolean needsPopulate = colCount != mColCount; 235 mColCount = mColCountSetting = colCount; 236 if (needsPopulate) { 237 populate(); 238 } 239 } 240 getColumnCount()241 public int getColumnCount() { 242 return mColCount; 243 } 244 245 /** 246 * Set a minimum column width for 247 * @param minColWidth 248 */ setMinColumnWidth(int minColWidth)249 public void setMinColumnWidth(int minColWidth) { 250 mMinColWidth = minColWidth; 251 setColumnCount(COLUMN_COUNT_AUTO); 252 } 253 254 /** 255 * Set the margin between items in pixels. This margin is applied 256 * both vertically and horizontally. 257 * 258 * @param marginPixels Spacing between items in pixels 259 */ setItemMargin(int marginPixels)260 public void setItemMargin(int marginPixels) { 261 final boolean needsPopulate = marginPixels != mItemMargin; 262 mItemMargin = marginPixels; 263 if (needsPopulate) { 264 populate(); 265 } 266 } 267 268 /** 269 * Return the first adapter position with a view currently attached as 270 * a child view of this grid. 271 * 272 * @return the adapter position represented by the view at getChildAt(0). 273 */ getFirstPosition()274 public int getFirstPosition() { 275 return mFirstPosition; 276 } 277 278 @Override onInterceptTouchEvent(MotionEvent ev)279 public boolean onInterceptTouchEvent(MotionEvent ev) { 280 mVelocityTracker.addMovement(ev); 281 final int action = ev.getAction() & MotionEventCompat.ACTION_MASK; 282 switch (action) { 283 case MotionEvent.ACTION_DOWN: 284 mVelocityTracker.clear(); 285 mScroller.abortAnimation(); 286 mLastTouchY = ev.getY(); 287 mActivePointerId = MotionEventCompat.getPointerId(ev, 0); 288 mTouchRemainderY = 0; 289 if (mTouchMode == TOUCH_MODE_FLINGING) { 290 // Catch! 291 mTouchMode = TOUCH_MODE_DRAGGING; 292 return true; 293 } 294 break; 295 296 case MotionEvent.ACTION_MOVE: { 297 final int index = MotionEventCompat.findPointerIndex(ev, mActivePointerId); 298 if (index < 0) { 299 Log.e(TAG, "onInterceptTouchEvent could not find pointer with id " + 300 mActivePointerId + " - did StaggeredGridView receive an inconsistent " + 301 "event stream?"); 302 return false; 303 } 304 final float y = MotionEventCompat.getY(ev, index); 305 final float dy = y - mLastTouchY + mTouchRemainderY; 306 final int deltaY = (int) dy; 307 mTouchRemainderY = dy - deltaY; 308 309 if (Math.abs(dy) > mTouchSlop) { 310 mTouchMode = TOUCH_MODE_DRAGGING; 311 return true; 312 } 313 } 314 } 315 316 return false; 317 } 318 319 @Override onTouchEvent(MotionEvent ev)320 public boolean onTouchEvent(MotionEvent ev) { 321 mVelocityTracker.addMovement(ev); 322 final int action = ev.getAction() & MotionEventCompat.ACTION_MASK; 323 switch (action) { 324 case MotionEvent.ACTION_DOWN: 325 mVelocityTracker.clear(); 326 mScroller.abortAnimation(); 327 mLastTouchY = ev.getY(); 328 mActivePointerId = MotionEventCompat.getPointerId(ev, 0); 329 mTouchRemainderY = 0; 330 break; 331 332 case MotionEvent.ACTION_MOVE: { 333 final int index = MotionEventCompat.findPointerIndex(ev, mActivePointerId); 334 if (index < 0) { 335 Log.e(TAG, "onInterceptTouchEvent could not find pointer with id " + 336 mActivePointerId + " - did StaggeredGridView receive an inconsistent " + 337 "event stream?"); 338 return false; 339 } 340 final float y = MotionEventCompat.getY(ev, index); 341 final float dy = y - mLastTouchY + mTouchRemainderY; 342 final int deltaY = (int) dy; 343 mTouchRemainderY = dy - deltaY; 344 345 if (Math.abs(dy) > mTouchSlop) { 346 mTouchMode = TOUCH_MODE_DRAGGING; 347 } 348 349 if (mTouchMode == TOUCH_MODE_DRAGGING) { 350 mLastTouchY = y; 351 352 if (!trackMotionScroll(deltaY, true)) { 353 // Break fling velocity if we impacted an edge. 354 mVelocityTracker.clear(); 355 } 356 } 357 } break; 358 359 case MotionEvent.ACTION_CANCEL: 360 mTouchMode = TOUCH_MODE_IDLE; 361 break; 362 363 case MotionEvent.ACTION_UP: { 364 mVelocityTracker.computeCurrentVelocity(1000, mMaximumVelocity); 365 final float velocity = VelocityTrackerCompat.getYVelocity(mVelocityTracker, 366 mActivePointerId); 367 if (Math.abs(velocity) > mFlingVelocity) { // TODO 368 mTouchMode = TOUCH_MODE_FLINGING; 369 mScroller.fling(0, 0, 0, (int) velocity, 0, 0, 370 Integer.MIN_VALUE, Integer.MAX_VALUE); 371 mLastTouchY = 0; 372 ViewCompat.postInvalidateOnAnimation(this); 373 } else { 374 mTouchMode = TOUCH_MODE_IDLE; 375 } 376 377 } break; 378 } 379 return true; 380 } 381 382 /** 383 * 384 * @param deltaY Pixels that content should move by 385 * @return true if the movement completed, false if it was stopped prematurely. 386 */ trackMotionScroll(int deltaY, boolean allowOverScroll)387 private boolean trackMotionScroll(int deltaY, boolean allowOverScroll) { 388 final boolean contentFits = contentFits(); 389 final int allowOverhang = Math.abs(deltaY); 390 391 final int overScrolledBy; 392 final int movedBy; 393 if (!contentFits) { 394 final int overhang; 395 final boolean up; 396 mPopulating = true; 397 if (deltaY > 0) { 398 overhang = fillUp(mFirstPosition - 1, allowOverhang); 399 up = true; 400 } else { 401 overhang = fillDown(mFirstPosition + getChildCount(), allowOverhang) + mItemMargin; 402 up = false; 403 } 404 movedBy = Math.min(overhang, allowOverhang); 405 offsetChildren(up ? movedBy : -movedBy); 406 recycleOffscreenViews(); 407 mPopulating = false; 408 overScrolledBy = allowOverhang - overhang; 409 } else { 410 overScrolledBy = allowOverhang; 411 movedBy = 0; 412 } 413 414 if (allowOverScroll) { 415 final int overScrollMode = ViewCompat.getOverScrollMode(this); 416 417 if (overScrollMode == ViewCompat.OVER_SCROLL_ALWAYS || 418 (overScrollMode == ViewCompat.OVER_SCROLL_IF_CONTENT_SCROLLS && !contentFits)) { 419 420 if (overScrolledBy > 0) { 421 EdgeEffectCompat edge = deltaY > 0 ? mTopEdge : mBottomEdge; 422 edge.onPull((float) Math.abs(deltaY) / getHeight()); 423 ViewCompat.postInvalidateOnAnimation(this); 424 } 425 } 426 } 427 428 return deltaY == 0 || movedBy != 0; 429 } 430 contentFits()431 private final boolean contentFits() { 432 if (mFirstPosition != 0 || getChildCount() != mItemCount) { 433 return false; 434 } 435 436 int topmost = Integer.MAX_VALUE; 437 int bottommost = Integer.MIN_VALUE; 438 for (int i = 0; i < mColCount; i++) { 439 if (mItemTops[i] < topmost) { 440 topmost = mItemTops[i]; 441 } 442 if (mItemBottoms[i] > bottommost) { 443 bottommost = mItemBottoms[i]; 444 } 445 } 446 447 return topmost >= getPaddingTop() && bottommost <= getHeight() - getPaddingBottom(); 448 } 449 recycleAllViews()450 private void recycleAllViews() { 451 for (int i = 0; i < getChildCount(); i++) { 452 mRecycler.addScrap(getChildAt(i)); 453 } 454 455 if (mInLayout) { 456 removeAllViewsInLayout(); 457 } else { 458 removeAllViews(); 459 } 460 } 461 462 /** 463 * Important: this method will leave offscreen views attached if they 464 * are required to maintain the invariant that child view with index i 465 * is always the view corresponding to position mFirstPosition + i. 466 */ recycleOffscreenViews()467 private void recycleOffscreenViews() { 468 final int height = getHeight(); 469 final int clearAbove = -mItemMargin; 470 final int clearBelow = height + mItemMargin; 471 for (int i = getChildCount() - 1; i >= 0; i--) { 472 final View child = getChildAt(i); 473 if (child.getTop() <= clearBelow) { 474 // There may be other offscreen views, but we need to maintain 475 // the invariant documented above. 476 break; 477 } 478 479 if (mInLayout) { 480 removeViewsInLayout(i, 1); 481 } else { 482 removeViewAt(i); 483 } 484 485 mRecycler.addScrap(child); 486 } 487 488 while (getChildCount() > 0) { 489 final View child = getChildAt(0); 490 if (child.getBottom() >= clearAbove) { 491 // There may be other offscreen views, but we need to maintain 492 // the invariant documented above. 493 break; 494 } 495 496 if (mInLayout) { 497 removeViewsInLayout(0, 1); 498 } else { 499 removeViewAt(0); 500 } 501 502 mRecycler.addScrap(child); 503 mFirstPosition++; 504 } 505 506 final int childCount = getChildCount(); 507 if (childCount > 0) { 508 // Repair the top and bottom column boundaries from the views we still have 509 Arrays.fill(mItemTops, Integer.MAX_VALUE); 510 Arrays.fill(mItemBottoms, Integer.MIN_VALUE); 511 512 for (int i = 0; i < childCount; i++){ 513 final View child = getChildAt(i); 514 final LayoutParams lp = (LayoutParams) child.getLayoutParams(); 515 final int top = child.getTop() - mItemMargin; 516 final int bottom = child.getBottom(); 517 final LayoutRecord rec = mLayoutRecords.get(mFirstPosition + i); 518 519 final int colEnd = lp.column + Math.min(mColCount, lp.span); 520 for (int col = lp.column; col < colEnd; col++) { 521 final int colTop = top - rec.getMarginAbove(col - lp.column); 522 final int colBottom = bottom + rec.getMarginBelow(col - lp.column); 523 if (colTop < mItemTops[col]) { 524 mItemTops[col] = colTop; 525 } 526 if (colBottom > mItemBottoms[col]) { 527 mItemBottoms[col] = colBottom; 528 } 529 } 530 } 531 532 for (int col = 0; col < mColCount; col++) { 533 if (mItemTops[col] == Integer.MAX_VALUE) { 534 // If one was untouched, both were. 535 mItemTops[col] = 0; 536 mItemBottoms[col] = 0; 537 } 538 } 539 } 540 } 541 computeScroll()542 public void computeScroll() { 543 if (mScroller.computeScrollOffset()) { 544 final int y = mScroller.getCurrY(); 545 final int dy = (int) (y - mLastTouchY); 546 mLastTouchY = y; 547 final boolean stopped = !trackMotionScroll(dy, false); 548 549 if (!stopped && !mScroller.isFinished()) { 550 ViewCompat.postInvalidateOnAnimation(this); 551 } else { 552 if (stopped) { 553 final int overScrollMode = ViewCompat.getOverScrollMode(this); 554 if (overScrollMode != ViewCompat.OVER_SCROLL_NEVER) { 555 final EdgeEffectCompat edge; 556 if (dy > 0) { 557 edge = mTopEdge; 558 } else { 559 edge = mBottomEdge; 560 } 561 edge.onAbsorb(Math.abs((int) mScroller.getCurrVelocity())); 562 ViewCompat.postInvalidateOnAnimation(this); 563 } 564 mScroller.abortAnimation(); 565 } 566 mTouchMode = TOUCH_MODE_IDLE; 567 } 568 } 569 } 570 571 @Override draw(Canvas canvas)572 public void draw(Canvas canvas) { 573 super.draw(canvas); 574 575 if (mTopEdge != null) { 576 boolean needsInvalidate = false; 577 if (!mTopEdge.isFinished()) { 578 mTopEdge.draw(canvas); 579 needsInvalidate = true; 580 } 581 if (!mBottomEdge.isFinished()) { 582 final int restoreCount = canvas.save(); 583 final int width = getWidth(); 584 canvas.translate(-width, getHeight()); 585 canvas.rotate(180, width, 0); 586 mBottomEdge.draw(canvas); 587 canvas.restoreToCount(restoreCount); 588 needsInvalidate = true; 589 } 590 591 if (needsInvalidate) { 592 ViewCompat.postInvalidateOnAnimation(this); 593 } 594 } 595 } 596 beginFastChildLayout()597 public void beginFastChildLayout() { 598 mFastChildLayout = true; 599 } 600 endFastChildLayout()601 public void endFastChildLayout() { 602 mFastChildLayout = false; 603 populate(); 604 } 605 606 @Override requestLayout()607 public void requestLayout() { 608 if (!mPopulating && !mFastChildLayout) { 609 super.requestLayout(); 610 } 611 } 612 613 @Override onMeasure(int widthMeasureSpec, int heightMeasureSpec)614 protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { 615 int widthMode = MeasureSpec.getMode(widthMeasureSpec); 616 int heightMode = MeasureSpec.getMode(heightMeasureSpec); 617 int widthSize = MeasureSpec.getSize(widthMeasureSpec); 618 int heightSize = MeasureSpec.getSize(heightMeasureSpec); 619 620 if (widthMode != MeasureSpec.EXACTLY) { 621 Log.e(TAG, "onMeasure: must have an exact width or match_parent! " + 622 "Using fallback spec of EXACTLY " + widthSize); 623 widthMode = MeasureSpec.EXACTLY; 624 } 625 if (heightMode != MeasureSpec.EXACTLY) { 626 Log.e(TAG, "onMeasure: must have an exact height or match_parent! " + 627 "Using fallback spec of EXACTLY " + heightSize); 628 heightMode = MeasureSpec.EXACTLY; 629 } 630 631 setMeasuredDimension(widthSize, heightSize); 632 633 if (mColCountSetting == COLUMN_COUNT_AUTO) { 634 final int colCount = widthSize / mMinColWidth; 635 if (colCount != mColCount) { 636 mColCount = colCount; 637 mForcePopulateOnLayout = true; 638 } 639 } 640 } 641 642 @Override onLayout(boolean changed, int l, int t, int r, int b)643 protected void onLayout(boolean changed, int l, int t, int r, int b) { 644 mInLayout = true; 645 populate(); 646 mInLayout = false; 647 mForcePopulateOnLayout = false; 648 649 final int width = r - l; 650 final int height = b - t; 651 mTopEdge.setSize(width, height); 652 mBottomEdge.setSize(width, height); 653 } 654 populate()655 private void populate() { 656 if (getWidth() == 0 || getHeight() == 0) { 657 return; 658 } 659 660 if (mColCount == COLUMN_COUNT_AUTO) { 661 final int colCount = getWidth() / mMinColWidth; 662 if (colCount != mColCount) { 663 mColCount = colCount; 664 } 665 } 666 667 final int colCount = mColCount; 668 if (mItemTops == null || mItemTops.length != colCount) { 669 mItemTops = new int[colCount]; 670 mItemBottoms = new int[colCount]; 671 final int top = getPaddingTop(); 672 final int offset = top + Math.min(mRestoreOffset, 0); 673 Arrays.fill(mItemTops, offset); 674 Arrays.fill(mItemBottoms, offset); 675 mLayoutRecords.clear(); 676 if (mInLayout) { 677 removeAllViewsInLayout(); 678 } else { 679 removeAllViews(); 680 } 681 mRestoreOffset = 0; 682 } 683 684 mPopulating = true; 685 layoutChildren(mDataChanged); 686 fillDown(mFirstPosition + getChildCount(), 0); 687 fillUp(mFirstPosition - 1, 0); 688 mPopulating = false; 689 mDataChanged = false; 690 } 691 dumpItemPositions()692 private void dumpItemPositions() { 693 final int childCount = getChildCount(); 694 Log.d(TAG, "dumpItemPositions:"); 695 Log.d(TAG, " => Tops:"); 696 for (int i = 0; i < mColCount; i++) { 697 Log.d(TAG, " => " + mItemTops[i]); 698 boolean found = false; 699 for (int j = 0; j < childCount; j++) { 700 final View child = getChildAt(j); 701 if (mItemTops[i] == child.getTop() - mItemMargin) { 702 found = true; 703 } 704 } 705 if (!found) { 706 Log.d(TAG, "!!! No top item found for column " + i + " value " + mItemTops[i]); 707 } 708 } 709 Log.d(TAG, " => Bottoms:"); 710 for (int i = 0; i < mColCount; i++) { 711 Log.d(TAG, " => " + mItemBottoms[i]); 712 boolean found = false; 713 for (int j = 0; j < childCount; j++) { 714 final View child = getChildAt(j); 715 if (mItemBottoms[i] == child.getBottom()) { 716 found = true; 717 } 718 } 719 if (!found) { 720 Log.d(TAG, "!!! No bottom item found for column " + i + " value " + mItemBottoms[i]); 721 } 722 } 723 } 724 offsetChildren(int offset)725 final void offsetChildren(int offset) { 726 final int childCount = getChildCount(); 727 for (int i = 0; i < childCount; i++) { 728 final View child = getChildAt(i); 729 child.layout(child.getLeft(), child.getTop() + offset, 730 child.getRight(), child.getBottom() + offset); 731 } 732 733 final int colCount = mColCount; 734 for (int i = 0; i < colCount; i++) { 735 mItemTops[i] += offset; 736 mItemBottoms[i] += offset; 737 } 738 } 739 740 /** 741 * Measure and layout all currently visible children. 742 * 743 * @param queryAdapter true to requery the adapter for view data 744 */ layoutChildren(boolean queryAdapter)745 final void layoutChildren(boolean queryAdapter) { 746 final int paddingLeft = getPaddingLeft(); 747 final int paddingRight = getPaddingRight(); 748 final int itemMargin = mItemMargin; 749 final int colWidth = 750 (getWidth() - paddingLeft - paddingRight - itemMargin * (mColCount - 1)) / mColCount; 751 int rebuildLayoutRecordsBefore = -1; 752 int rebuildLayoutRecordsAfter = -1; 753 754 Arrays.fill(mItemBottoms, Integer.MIN_VALUE); 755 756 final int childCount = getChildCount(); 757 for (int i = 0; i < childCount; i++) { 758 View child = getChildAt(i); 759 LayoutParams lp = (LayoutParams) child.getLayoutParams(); 760 final int col = lp.column; 761 final int position = mFirstPosition + i; 762 final boolean needsLayout = queryAdapter || child.isLayoutRequested(); 763 764 if (queryAdapter) { 765 View newView = obtainView(position, child); 766 if (newView != child) { 767 removeViewAt(i); 768 addView(newView, i); 769 child = newView; 770 } 771 lp = (LayoutParams) child.getLayoutParams(); // Might have changed 772 } 773 774 final int span = Math.min(mColCount, lp.span); 775 final int widthSize = colWidth * span + itemMargin * (span - 1); 776 777 if (needsLayout) { 778 final int widthSpec = MeasureSpec.makeMeasureSpec(widthSize, MeasureSpec.EXACTLY); 779 780 final int heightSpec; 781 if (lp.height == LayoutParams.WRAP_CONTENT) { 782 heightSpec = MeasureSpec.makeMeasureSpec(0, MeasureSpec.UNSPECIFIED); 783 } else { 784 heightSpec = MeasureSpec.makeMeasureSpec(lp.height, MeasureSpec.EXACTLY); 785 } 786 787 child.measure(widthSpec, heightSpec); 788 } 789 790 int childTop = mItemBottoms[col] > Integer.MIN_VALUE ? 791 mItemBottoms[col] + mItemMargin : child.getTop(); 792 if (span > 1) { 793 int lowest = childTop; 794 for (int j = col + 1; j < col + span; j++) { 795 final int bottom = mItemBottoms[j] + mItemMargin; 796 if (bottom > lowest) { 797 lowest = bottom; 798 } 799 } 800 childTop = lowest; 801 } 802 final int childHeight = child.getMeasuredHeight(); 803 final int childBottom = childTop + childHeight; 804 final int childLeft = paddingLeft + col * (colWidth + itemMargin); 805 final int childRight = childLeft + child.getMeasuredWidth(); 806 child.layout(childLeft, childTop, childRight, childBottom); 807 808 for (int j = col; j < col + span; j++) { 809 mItemBottoms[j] = childBottom; 810 } 811 812 final LayoutRecord rec = mLayoutRecords.get(position); 813 if (rec != null && rec.height != childHeight) { 814 // Invalidate our layout records for everything before this. 815 rec.height = childHeight; 816 rebuildLayoutRecordsBefore = position; 817 } 818 819 if (rec != null && rec.span != span) { 820 // Invalidate our layout records for everything after this. 821 rec.span = span; 822 rebuildLayoutRecordsAfter = position; 823 } 824 } 825 826 // Update mItemBottoms for any empty columns 827 for (int i = 0; i < mColCount; i++) { 828 if (mItemBottoms[i] == Integer.MIN_VALUE) { 829 mItemBottoms[i] = mItemTops[i]; 830 } 831 } 832 833 if (rebuildLayoutRecordsBefore >= 0 || rebuildLayoutRecordsAfter >= 0) { 834 if (rebuildLayoutRecordsBefore >= 0) { 835 invalidateLayoutRecordsBeforePosition(rebuildLayoutRecordsBefore); 836 } 837 if (rebuildLayoutRecordsAfter >= 0) { 838 invalidateLayoutRecordsAfterPosition(rebuildLayoutRecordsAfter); 839 } 840 for (int i = 0; i < childCount; i++) { 841 final int position = mFirstPosition + i; 842 final View child = getChildAt(i); 843 final LayoutParams lp = (LayoutParams) child.getLayoutParams(); 844 LayoutRecord rec = mLayoutRecords.get(position); 845 if (rec == null) { 846 rec = new LayoutRecord(); 847 mLayoutRecords.put(position, rec); 848 } 849 rec.column = lp.column; 850 rec.height = child.getHeight(); 851 rec.id = lp.id; 852 rec.span = Math.min(mColCount, lp.span); 853 } 854 } 855 } 856 invalidateLayoutRecordsBeforePosition(int position)857 final void invalidateLayoutRecordsBeforePosition(int position) { 858 int endAt = 0; 859 while (endAt < mLayoutRecords.size() && mLayoutRecords.keyAt(endAt) < position) { 860 endAt++; 861 } 862 mLayoutRecords.removeAtRange(0, endAt); 863 } 864 invalidateLayoutRecordsAfterPosition(int position)865 final void invalidateLayoutRecordsAfterPosition(int position) { 866 int beginAt = mLayoutRecords.size() - 1; 867 while (beginAt >= 0 && mLayoutRecords.keyAt(beginAt) > position) { 868 beginAt--; 869 } 870 beginAt++; 871 mLayoutRecords.removeAtRange(beginAt + 1, mLayoutRecords.size() - beginAt); 872 } 873 874 /** 875 * Should be called with mPopulating set to true 876 * 877 * @param fromPosition Position to start filling from 878 * @param overhang the number of extra pixels to fill beyond the current top edge 879 * @return the max overhang beyond the beginning of the view of any added items at the top 880 */ fillUp(int fromPosition, int overhang)881 final int fillUp(int fromPosition, int overhang) { 882 final int paddingLeft = getPaddingLeft(); 883 final int paddingRight = getPaddingRight(); 884 final int itemMargin = mItemMargin; 885 final int colWidth = 886 (getWidth() - paddingLeft - paddingRight - itemMargin * (mColCount - 1)) / mColCount; 887 final int gridTop = getPaddingTop(); 888 final int fillTo = gridTop - overhang; 889 int nextCol = getNextColumnUp(); 890 int position = fromPosition; 891 892 while (nextCol >= 0 && mItemTops[nextCol] > fillTo && position >= 0) { 893 final View child = obtainView(position, null); 894 LayoutParams lp = (LayoutParams) child.getLayoutParams(); 895 896 if (child.getParent() != this) { 897 if (mInLayout) { 898 addViewInLayout(child, 0, lp); 899 } else { 900 addView(child, 0); 901 } 902 } 903 904 final int span = Math.min(mColCount, lp.span); 905 final int widthSize = colWidth * span + itemMargin * (span - 1); 906 final int widthSpec = MeasureSpec.makeMeasureSpec(widthSize, MeasureSpec.EXACTLY); 907 908 LayoutRecord rec; 909 if (span > 1) { 910 rec = getNextRecordUp(position, span); 911 nextCol = rec.column; 912 } else { 913 rec = mLayoutRecords.get(position); 914 } 915 916 boolean invalidateBefore = false; 917 if (rec == null) { 918 rec = new LayoutRecord(); 919 mLayoutRecords.put(position, rec); 920 rec.column = nextCol; 921 rec.span = span; 922 } else if (span != rec.span) { 923 rec.span = span; 924 rec.column = nextCol; 925 invalidateBefore = true; 926 } else { 927 nextCol = rec.column; 928 } 929 930 if (mHasStableIds) { 931 final long id = mAdapter.getItemId(position); 932 rec.id = id; 933 lp.id = id; 934 } 935 936 lp.column = nextCol; 937 938 final int heightSpec; 939 if (lp.height == LayoutParams.WRAP_CONTENT) { 940 heightSpec = MeasureSpec.makeMeasureSpec(0, MeasureSpec.UNSPECIFIED); 941 } else { 942 heightSpec = MeasureSpec.makeMeasureSpec(lp.height, MeasureSpec.EXACTLY); 943 } 944 child.measure(widthSpec, heightSpec); 945 946 final int childHeight = child.getMeasuredHeight(); 947 if (invalidateBefore || (childHeight != rec.height && rec.height > 0)) { 948 invalidateLayoutRecordsBeforePosition(position); 949 } 950 rec.height = childHeight; 951 952 final int startFrom; 953 if (span > 1) { 954 int highest = mItemTops[nextCol]; 955 for (int i = nextCol + 1; i < nextCol + span; i++) { 956 final int top = mItemTops[i]; 957 if (top < highest) { 958 highest = top; 959 } 960 } 961 startFrom = highest; 962 } else { 963 startFrom = mItemTops[nextCol]; 964 } 965 final int childBottom = startFrom; 966 final int childTop = childBottom - childHeight; 967 final int childLeft = paddingLeft + nextCol * (colWidth + itemMargin); 968 final int childRight = childLeft + child.getMeasuredWidth(); 969 child.layout(childLeft, childTop, childRight, childBottom); 970 971 for (int i = nextCol; i < nextCol + span; i++) { 972 mItemTops[i] = childTop - rec.getMarginAbove(i - nextCol) - itemMargin; 973 } 974 975 nextCol = getNextColumnUp(); 976 mFirstPosition = position--; 977 } 978 979 int highestView = getHeight(); 980 for (int i = 0; i < mColCount; i++) { 981 if (mItemTops[i] < highestView) { 982 highestView = mItemTops[i]; 983 } 984 } 985 return gridTop - highestView; 986 } 987 988 /** 989 * Should be called with mPopulating set to true 990 * 991 * @param fromPosition Position to start filling from 992 * @param overhang the number of extra pixels to fill beyond the current bottom edge 993 * @return the max overhang beyond the end of the view of any added items at the bottom 994 */ fillDown(int fromPosition, int overhang)995 final int fillDown(int fromPosition, int overhang) { 996 final int paddingLeft = getPaddingLeft(); 997 final int paddingRight = getPaddingRight(); 998 final int itemMargin = mItemMargin; 999 final int colWidth = 1000 (getWidth() - paddingLeft - paddingRight - itemMargin * (mColCount - 1)) / mColCount; 1001 final int gridBottom = getHeight() - getPaddingBottom(); 1002 final int fillTo = gridBottom + overhang; 1003 int nextCol = getNextColumnDown(); 1004 int position = fromPosition; 1005 1006 while (nextCol >= 0 && mItemBottoms[nextCol] < fillTo && position < mItemCount) { 1007 final View child = obtainView(position, null); 1008 LayoutParams lp = (LayoutParams) child.getLayoutParams(); 1009 1010 if (child.getParent() != this) { 1011 if (mInLayout) { 1012 addViewInLayout(child, -1, lp); 1013 } else { 1014 addView(child); 1015 } 1016 } 1017 1018 final int span = Math.min(mColCount, lp.span); 1019 final int widthSize = colWidth * span + itemMargin * (span - 1); 1020 final int widthSpec = MeasureSpec.makeMeasureSpec(widthSize, MeasureSpec.EXACTLY); 1021 1022 LayoutRecord rec; 1023 if (span > 1) { 1024 rec = getNextRecordDown(position, span); 1025 nextCol = rec.column; 1026 } else { 1027 rec = mLayoutRecords.get(position); 1028 } 1029 1030 boolean invalidateAfter = false; 1031 if (rec == null) { 1032 rec = new LayoutRecord(); 1033 mLayoutRecords.put(position, rec); 1034 rec.column = nextCol; 1035 rec.span = span; 1036 } else if (span != rec.span) { 1037 rec.span = span; 1038 rec.column = nextCol; 1039 invalidateAfter = true; 1040 } else { 1041 nextCol = rec.column; 1042 } 1043 1044 if (mHasStableIds) { 1045 final long id = mAdapter.getItemId(position); 1046 rec.id = id; 1047 lp.id = id; 1048 } 1049 1050 lp.column = nextCol; 1051 1052 final int heightSpec; 1053 if (lp.height == LayoutParams.WRAP_CONTENT) { 1054 heightSpec = MeasureSpec.makeMeasureSpec(0, MeasureSpec.UNSPECIFIED); 1055 } else { 1056 heightSpec = MeasureSpec.makeMeasureSpec(lp.height, MeasureSpec.EXACTLY); 1057 } 1058 child.measure(widthSpec, heightSpec); 1059 1060 final int childHeight = child.getMeasuredHeight(); 1061 if (invalidateAfter || (childHeight != rec.height && rec.height > 0)) { 1062 invalidateLayoutRecordsAfterPosition(position); 1063 } 1064 rec.height = childHeight; 1065 1066 final int startFrom; 1067 if (span > 1) { 1068 int lowest = mItemBottoms[nextCol]; 1069 for (int i = nextCol + 1; i < nextCol + span; i++) { 1070 final int bottom = mItemBottoms[i]; 1071 if (bottom > lowest) { 1072 lowest = bottom; 1073 } 1074 } 1075 startFrom = lowest; 1076 } else { 1077 startFrom = mItemBottoms[nextCol]; 1078 } 1079 final int childTop = startFrom + itemMargin; 1080 final int childBottom = childTop + childHeight; 1081 final int childLeft = paddingLeft + nextCol * (colWidth + itemMargin); 1082 final int childRight = childLeft + child.getMeasuredWidth(); 1083 child.layout(childLeft, childTop, childRight, childBottom); 1084 1085 for (int i = nextCol; i < nextCol + span; i++) { 1086 mItemBottoms[i] = childBottom + rec.getMarginBelow(i - nextCol); 1087 } 1088 1089 nextCol = getNextColumnDown(); 1090 position++; 1091 } 1092 1093 int lowestView = 0; 1094 for (int i = 0; i < mColCount; i++) { 1095 if (mItemBottoms[i] > lowestView) { 1096 lowestView = mItemBottoms[i]; 1097 } 1098 } 1099 return lowestView - gridBottom; 1100 } 1101 1102 /** 1103 * @return column that the next view filling upwards should occupy. This is the bottom-most 1104 * position available for a single-column item. 1105 */ getNextColumnUp()1106 final int getNextColumnUp() { 1107 int result = -1; 1108 int bottomMost = Integer.MIN_VALUE; 1109 1110 final int colCount = mColCount; 1111 for (int i = colCount - 1; i >= 0; i--) { 1112 final int top = mItemTops[i]; 1113 if (top > bottomMost) { 1114 bottomMost = top; 1115 result = i; 1116 } 1117 } 1118 return result; 1119 } 1120 1121 /** 1122 * Return a LayoutRecord for the given position 1123 * @param position 1124 * @param span 1125 * @return 1126 */ getNextRecordUp(int position, int span)1127 final LayoutRecord getNextRecordUp(int position, int span) { 1128 LayoutRecord rec = mLayoutRecords.get(position); 1129 if (rec == null) { 1130 rec = new LayoutRecord(); 1131 rec.span = span; 1132 mLayoutRecords.put(position, rec); 1133 } else if (rec.span != span) { 1134 throw new IllegalStateException("Invalid LayoutRecord! Record had span=" + rec.span + 1135 " but caller requested span=" + span + " for position=" + position); 1136 } 1137 int targetCol = -1; 1138 int bottomMost = Integer.MIN_VALUE; 1139 1140 final int colCount = mColCount; 1141 for (int i = colCount - span; i >= 0; i--) { 1142 int top = Integer.MAX_VALUE; 1143 for (int j = i; j < i + span; j++) { 1144 final int singleTop = mItemTops[j]; 1145 if (singleTop < top) { 1146 top = singleTop; 1147 } 1148 } 1149 if (top > bottomMost) { 1150 bottomMost = top; 1151 targetCol = i; 1152 } 1153 } 1154 1155 rec.column = targetCol; 1156 1157 for (int i = 0; i < span; i++) { 1158 rec.setMarginBelow(i, mItemTops[i + targetCol] - bottomMost); 1159 } 1160 1161 return rec; 1162 } 1163 1164 /** 1165 * @return column that the next view filling downwards should occupy. This is the top-most 1166 * position available. 1167 */ getNextColumnDown()1168 final int getNextColumnDown() { 1169 int result = -1; 1170 int topMost = Integer.MAX_VALUE; 1171 1172 final int colCount = mColCount; 1173 for (int i = 0; i < colCount; i++) { 1174 final int bottom = mItemBottoms[i]; 1175 if (bottom < topMost) { 1176 topMost = bottom; 1177 result = i; 1178 } 1179 } 1180 return result; 1181 } 1182 getNextRecordDown(int position, int span)1183 final LayoutRecord getNextRecordDown(int position, int span) { 1184 LayoutRecord rec = mLayoutRecords.get(position); 1185 if (rec == null) { 1186 rec = new LayoutRecord(); 1187 rec.span = span; 1188 mLayoutRecords.put(position, rec); 1189 } else if (rec.span != span) { 1190 throw new IllegalStateException("Invalid LayoutRecord! Record had span=" + rec.span + 1191 " but caller requested span=" + span + " for position=" + position); 1192 } 1193 int targetCol = -1; 1194 int topMost = Integer.MAX_VALUE; 1195 1196 final int colCount = mColCount; 1197 for (int i = 0; i <= colCount - span; i++) { 1198 int bottom = Integer.MIN_VALUE; 1199 for (int j = i; j < i + span; j++) { 1200 final int singleBottom = mItemBottoms[j]; 1201 if (singleBottom > bottom) { 1202 bottom = singleBottom; 1203 } 1204 } 1205 if (bottom < topMost) { 1206 topMost = bottom; 1207 targetCol = i; 1208 } 1209 } 1210 1211 rec.column = targetCol; 1212 1213 for (int i = 0; i < span; i++) { 1214 rec.setMarginAbove(i, topMost - mItemBottoms[i + targetCol]); 1215 } 1216 1217 return rec; 1218 } 1219 1220 /** 1221 * Obtain a populated view from the adapter. If optScrap is non-null and is not 1222 * reused it will be placed in the recycle bin. 1223 * 1224 * @param position position to get view for 1225 * @param optScrap Optional scrap view; will be reused if possible 1226 * @return A new view, a recycled view from mRecycler, or optScrap 1227 */ obtainView(int position, View optScrap)1228 final View obtainView(int position, View optScrap) { 1229 View view = mRecycler.getTransientStateView(position); 1230 if (view != null) { 1231 return view; 1232 } 1233 1234 // Reuse optScrap if it's of the right type (and not null) 1235 final int optType = optScrap != null ? 1236 ((LayoutParams) optScrap.getLayoutParams()).viewType : -1; 1237 final int positionViewType = mAdapter.getItemViewType(position); 1238 final View scrap = optType == positionViewType ? 1239 optScrap : mRecycler.getScrapView(positionViewType); 1240 1241 view = mAdapter.getView(position, scrap, this); 1242 1243 if (view != scrap && scrap != null) { 1244 // The adapter didn't use it; put it back. 1245 mRecycler.addScrap(scrap); 1246 } 1247 1248 ViewGroup.LayoutParams lp = view.getLayoutParams(); 1249 1250 if (view.getParent() != this) { 1251 if (lp == null) { 1252 lp = generateDefaultLayoutParams(); 1253 } else if (!checkLayoutParams(lp)) { 1254 lp = generateLayoutParams(lp); 1255 } 1256 } 1257 1258 final LayoutParams sglp = (LayoutParams) lp; 1259 sglp.position = position; 1260 sglp.viewType = positionViewType; 1261 1262 return view; 1263 } 1264 getAdapter()1265 public ListAdapter getAdapter() { 1266 return mAdapter; 1267 } 1268 setAdapter(ListAdapter adapter)1269 public void setAdapter(ListAdapter adapter) { 1270 if (mAdapter != null) { 1271 mAdapter.unregisterDataSetObserver(mObserver); 1272 } 1273 // TODO: If the new adapter says that there are stable IDs, remove certain layout records 1274 // and onscreen views if they have changed instead of removing all of the state here. 1275 clearAllState(); 1276 mAdapter = adapter; 1277 mDataChanged = true; 1278 mOldItemCount = mItemCount = adapter != null ? adapter.getCount() : 0; 1279 if (adapter != null) { 1280 adapter.registerDataSetObserver(mObserver); 1281 mRecycler.setViewTypeCount(adapter.getViewTypeCount()); 1282 mHasStableIds = adapter.hasStableIds(); 1283 } else { 1284 mHasStableIds = false; 1285 } 1286 populate(); 1287 } 1288 1289 /** 1290 * Clear all state because the grid will be used for a completely different set of data. 1291 */ clearAllState()1292 private void clearAllState() { 1293 // Clear all layout records and views 1294 mLayoutRecords.clear(); 1295 removeAllViews(); 1296 1297 // Reset to the top of the grid 1298 resetStateForGridTop(); 1299 1300 // Clear recycler because there could be different view types now 1301 mRecycler.clear(); 1302 } 1303 1304 /** 1305 * Reset all internal state to be at the top of the grid. 1306 */ resetStateForGridTop()1307 private void resetStateForGridTop() { 1308 // Reset mItemTops and mItemBottoms 1309 final int colCount = mColCount; 1310 if (mItemTops == null || mItemTops.length != colCount) { 1311 mItemTops = new int[colCount]; 1312 mItemBottoms = new int[colCount]; 1313 } 1314 final int top = getPaddingTop(); 1315 Arrays.fill(mItemTops, top); 1316 Arrays.fill(mItemBottoms, top); 1317 1318 // Reset the first visible position in the grid to be item 0 1319 mFirstPosition = 0; 1320 mRestoreOffset = 0; 1321 } 1322 1323 /** 1324 * Scroll the list so the first visible position in the grid is the first item in the adapter. 1325 */ setSelectionToTop()1326 public void setSelectionToTop() { 1327 // Clear out the views (but don't clear out the layout records or recycler because the data 1328 // has not changed) 1329 removeAllViews(); 1330 1331 // Reset to top of grid 1332 resetStateForGridTop(); 1333 1334 // Start populating again 1335 populate(); 1336 } 1337 1338 @Override generateDefaultLayoutParams()1339 protected LayoutParams generateDefaultLayoutParams() { 1340 return new LayoutParams(LayoutParams.WRAP_CONTENT); 1341 } 1342 1343 @Override generateLayoutParams(ViewGroup.LayoutParams lp)1344 protected LayoutParams generateLayoutParams(ViewGroup.LayoutParams lp) { 1345 return new LayoutParams(lp); 1346 } 1347 1348 @Override checkLayoutParams(ViewGroup.LayoutParams lp)1349 protected boolean checkLayoutParams(ViewGroup.LayoutParams lp) { 1350 return lp instanceof LayoutParams; 1351 } 1352 1353 @Override generateLayoutParams(AttributeSet attrs)1354 public ViewGroup.LayoutParams generateLayoutParams(AttributeSet attrs) { 1355 return new LayoutParams(getContext(), attrs); 1356 } 1357 1358 @Override onSaveInstanceState()1359 public Parcelable onSaveInstanceState() { 1360 final Parcelable superState = super.onSaveInstanceState(); 1361 final SavedState ss = new SavedState(superState); 1362 final int position = mFirstPosition; 1363 ss.position = position; 1364 if (position >= 0 && mAdapter != null && position < mAdapter.getCount()) { 1365 ss.firstId = mAdapter.getItemId(position); 1366 } 1367 if (getChildCount() > 0) { 1368 ss.topOffset = getChildAt(0).getTop() - mItemMargin - getPaddingTop(); 1369 } 1370 return ss; 1371 } 1372 1373 @Override onRestoreInstanceState(Parcelable state)1374 public void onRestoreInstanceState(Parcelable state) { 1375 SavedState ss = (SavedState) state; 1376 super.onRestoreInstanceState(ss.getSuperState()); 1377 mDataChanged = true; 1378 mFirstPosition = ss.position; 1379 mRestoreOffset = ss.topOffset; 1380 requestLayout(); 1381 } 1382 1383 public static class LayoutParams extends ViewGroup.LayoutParams { 1384 private static final int[] LAYOUT_ATTRS = new int[] { 1385 android.R.attr.layout_span 1386 }; 1387 1388 private static final int SPAN_INDEX = 0; 1389 1390 /** 1391 * The number of columns this item should span 1392 */ 1393 public int span = 1; 1394 1395 /** 1396 * Item position this view represents 1397 */ 1398 int position; 1399 1400 /** 1401 * Type of this view as reported by the adapter 1402 */ 1403 int viewType; 1404 1405 /** 1406 * The column this view is occupying 1407 */ 1408 int column; 1409 1410 /** 1411 * The stable ID of the item this view displays 1412 */ 1413 long id = -1; 1414 LayoutParams(int height)1415 public LayoutParams(int height) { 1416 super(FILL_PARENT, height); 1417 1418 if (this.height == FILL_PARENT) { 1419 Log.w(TAG, "Constructing LayoutParams with height FILL_PARENT - " + 1420 "impossible! Falling back to WRAP_CONTENT"); 1421 this.height = WRAP_CONTENT; 1422 } 1423 } 1424 LayoutParams(Context c, AttributeSet attrs)1425 public LayoutParams(Context c, AttributeSet attrs) { 1426 super(c, attrs); 1427 1428 if (this.width != FILL_PARENT) { 1429 Log.w(TAG, "Inflation setting LayoutParams width to " + this.width + 1430 " - must be MATCH_PARENT"); 1431 this.width = FILL_PARENT; 1432 } 1433 if (this.height == FILL_PARENT) { 1434 Log.w(TAG, "Inflation setting LayoutParams height to MATCH_PARENT - " + 1435 "impossible! Falling back to WRAP_CONTENT"); 1436 this.height = WRAP_CONTENT; 1437 } 1438 1439 TypedArray a = c.obtainStyledAttributes(attrs, LAYOUT_ATTRS); 1440 span = a.getInteger(SPAN_INDEX, 1); 1441 a.recycle(); 1442 } 1443 LayoutParams(ViewGroup.LayoutParams other)1444 public LayoutParams(ViewGroup.LayoutParams other) { 1445 super(other); 1446 1447 if (this.width != FILL_PARENT) { 1448 Log.w(TAG, "Constructing LayoutParams with width " + this.width + 1449 " - must be MATCH_PARENT"); 1450 this.width = FILL_PARENT; 1451 } 1452 if (this.height == FILL_PARENT) { 1453 Log.w(TAG, "Constructing LayoutParams with height MATCH_PARENT - " + 1454 "impossible! Falling back to WRAP_CONTENT"); 1455 this.height = WRAP_CONTENT; 1456 } 1457 } 1458 } 1459 1460 private class RecycleBin { 1461 private ArrayList<View>[] mScrapViews; 1462 private int mViewTypeCount; 1463 private int mMaxScrap; 1464 1465 private SparseArray<View> mTransientStateViews; 1466 setViewTypeCount(int viewTypeCount)1467 public void setViewTypeCount(int viewTypeCount) { 1468 if (viewTypeCount < 1) { 1469 throw new IllegalArgumentException("Must have at least one view type (" + 1470 viewTypeCount + " types reported)"); 1471 } 1472 if (viewTypeCount == mViewTypeCount) { 1473 return; 1474 } 1475 1476 ArrayList<View>[] scrapViews = new ArrayList[viewTypeCount]; 1477 for (int i = 0; i < viewTypeCount; i++) { 1478 scrapViews[i] = new ArrayList<View>(); 1479 } 1480 mViewTypeCount = viewTypeCount; 1481 mScrapViews = scrapViews; 1482 } 1483 clear()1484 public void clear() { 1485 final int typeCount = mViewTypeCount; 1486 for (int i = 0; i < typeCount; i++) { 1487 mScrapViews[i].clear(); 1488 } 1489 if (mTransientStateViews != null) { 1490 mTransientStateViews.clear(); 1491 } 1492 } 1493 clearTransientViews()1494 public void clearTransientViews() { 1495 if (mTransientStateViews != null) { 1496 mTransientStateViews.clear(); 1497 } 1498 } 1499 addScrap(View v)1500 public void addScrap(View v) { 1501 final LayoutParams lp = (LayoutParams) v.getLayoutParams(); 1502 if (ViewCompat.hasTransientState(v)) { 1503 if (mTransientStateViews == null) { 1504 mTransientStateViews = new SparseArray<View>(); 1505 } 1506 mTransientStateViews.put(lp.position, v); 1507 return; 1508 } 1509 1510 final int childCount = getChildCount(); 1511 if (childCount > mMaxScrap) { 1512 mMaxScrap = childCount; 1513 } 1514 1515 ArrayList<View> scrap = mScrapViews[lp.viewType]; 1516 if (scrap.size() < mMaxScrap) { 1517 scrap.add(v); 1518 } 1519 } 1520 getTransientStateView(int position)1521 public View getTransientStateView(int position) { 1522 if (mTransientStateViews == null) { 1523 return null; 1524 } 1525 1526 final View result = mTransientStateViews.get(position); 1527 if (result != null) { 1528 mTransientStateViews.remove(position); 1529 } 1530 return result; 1531 } 1532 getScrapView(int type)1533 public View getScrapView(int type) { 1534 ArrayList<View> scrap = mScrapViews[type]; 1535 if (scrap.isEmpty()) { 1536 return null; 1537 } 1538 1539 final int index = scrap.size() - 1; 1540 final View result = scrap.get(index); 1541 scrap.remove(index); 1542 return result; 1543 } 1544 } 1545 1546 private class AdapterDataSetObserver extends DataSetObserver { 1547 @Override onChanged()1548 public void onChanged() { 1549 mDataChanged = true; 1550 mOldItemCount = mItemCount; 1551 mItemCount = mAdapter.getCount(); 1552 1553 // TODO: Consider matching these back up if we have stable IDs. 1554 mRecycler.clearTransientViews(); 1555 1556 if (!mHasStableIds) { 1557 // Clear all layout records and recycle the views 1558 mLayoutRecords.clear(); 1559 recycleAllViews(); 1560 1561 // Reset item bottoms to be equal to item tops 1562 final int colCount = mColCount; 1563 for (int i = 0; i < colCount; i++) { 1564 mItemBottoms[i] = mItemTops[i]; 1565 } 1566 } 1567 1568 // TODO: consider repopulating in a deferred runnable instead 1569 // (so that successive changes may still be batched) 1570 requestLayout(); 1571 } 1572 1573 @Override onInvalidated()1574 public void onInvalidated() { 1575 } 1576 } 1577 1578 static class SavedState extends BaseSavedState { 1579 long firstId = -1; 1580 int position; 1581 int topOffset; 1582 SavedState(Parcelable superState)1583 SavedState(Parcelable superState) { 1584 super(superState); 1585 } 1586 SavedState(Parcel in)1587 private SavedState(Parcel in) { 1588 super(in); 1589 firstId = in.readLong(); 1590 position = in.readInt(); 1591 topOffset = in.readInt(); 1592 } 1593 1594 @Override writeToParcel(Parcel out, int flags)1595 public void writeToParcel(Parcel out, int flags) { 1596 super.writeToParcel(out, flags); 1597 out.writeLong(firstId); 1598 out.writeInt(position); 1599 out.writeInt(topOffset); 1600 } 1601 1602 @Override toString()1603 public String toString() { 1604 return "StaggereGridView.SavedState{" 1605 + Integer.toHexString(System.identityHashCode(this)) 1606 + " firstId=" + firstId 1607 + " position=" + position + "}"; 1608 } 1609 1610 public static final Parcelable.Creator<SavedState> CREATOR 1611 = new Parcelable.Creator<SavedState>() { 1612 public SavedState createFromParcel(Parcel in) { 1613 return new SavedState(in); 1614 } 1615 1616 public SavedState[] newArray(int size) { 1617 return new SavedState[size]; 1618 } 1619 }; 1620 } 1621 } 1622