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