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
17package com.android.ex.widget;
18
19import android.content.Context;
20import android.content.res.TypedArray;
21import android.database.DataSetObserver;
22import android.graphics.Canvas;
23import android.os.Parcel;
24import android.os.Parcelable;
25import android.support.v4.util.SparseArrayCompat;
26import android.support.v4.view.MotionEventCompat;
27import android.support.v4.view.VelocityTrackerCompat;
28import android.support.v4.view.ViewCompat;
29import android.util.AttributeSet;
30import android.util.Log;
31import android.util.SparseArray;
32import android.view.MotionEvent;
33import android.view.VelocityTracker;
34import android.view.View;
35import android.view.ViewConfiguration;
36import android.view.ViewGroup;
37import android.widget.ListAdapter;
38
39import java.util.ArrayList;
40import 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 */
56public 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