Home | History | Annotate | Download | only in widget
      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 
    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 
    150         public final int getMarginAbove(int col) {
    151             if (mMargins == null) {
    152                 return 0;
    153             }
    154             return mMargins[col * 2];
    155         }
    156 
    157         public final int getMarginBelow(int col) {
    158             if (mMargins == null) {
    159                 return 0;
    160             }
    161             return mMargins[col * 2 + 1];
    162         }
    163 
    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 
    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
    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 
    197     public StaggeredGridView(Context context) {
    198         this(context, null);
    199     }
    200 
    201     public StaggeredGridView(Context context, AttributeSet attrs) {
    202         this(context, attrs, 0);
    203     }
    204 
    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      */
    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 
    241     public int getColumnCount() {
    242         return mColCount;
    243     }
    244 
    245     /**
    246      * Set a minimum column width for
    247      * @param minColWidth
    248      */
    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      */
    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      */
    274     public int getFirstPosition() {
    275         return mFirstPosition;
    276     }
    277 
    278     @Override
    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
    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      */
    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 
    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 
    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      */
    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 
    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
    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 
    597     public void beginFastChildLayout() {
    598         mFastChildLayout = true;
    599     }
    600 
    601     public void endFastChildLayout() {
    602         mFastChildLayout = false;
    603         populate();
    604     }
    605 
    606     @Override
    607     public void requestLayout() {
    608         if (!mPopulating && !mFastChildLayout) {
    609             super.requestLayout();
    610         }
    611     }
    612 
    613     @Override
    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
    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 
    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 
    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 
    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      */
    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 
    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 
    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      */
    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      */
    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      */
   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      */
   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      */
   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 
   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      */
   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 
   1265     public ListAdapter getAdapter() {
   1266         return mAdapter;
   1267     }
   1268 
   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      */
   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      */
   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      */
   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
   1339     protected LayoutParams generateDefaultLayoutParams() {
   1340         return new LayoutParams(LayoutParams.WRAP_CONTENT);
   1341     }
   1342 
   1343     @Override
   1344     protected LayoutParams generateLayoutParams(ViewGroup.LayoutParams lp) {
   1345         return new LayoutParams(lp);
   1346     }
   1347 
   1348     @Override
   1349     protected boolean checkLayoutParams(ViewGroup.LayoutParams lp) {
   1350         return lp instanceof LayoutParams;
   1351     }
   1352 
   1353     @Override
   1354     public ViewGroup.LayoutParams generateLayoutParams(AttributeSet attrs) {
   1355         return new LayoutParams(getContext(), attrs);
   1356     }
   1357 
   1358     @Override
   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
   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 
   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 
   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 
   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 
   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 
   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 
   1494         public void clearTransientViews() {
   1495             if (mTransientStateViews != null) {
   1496                 mTransientStateViews.clear();
   1497             }
   1498         }
   1499 
   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 
   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 
   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
   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
   1574         public void onInvalidated() {
   1575         }
   1576     }
   1577 
   1578     static class SavedState extends BaseSavedState {
   1579         long firstId = -1;
   1580         int position;
   1581         int topOffset;
   1582 
   1583         SavedState(Parcelable superState) {
   1584             super(superState);
   1585         }
   1586 
   1587         private SavedState(Parcel in) {
   1588             super(in);
   1589             firstId = in.readLong();
   1590             position = in.readInt();
   1591             topOffset = in.readInt();
   1592         }
   1593 
   1594         @Override
   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
   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