summaryrefslogtreecommitdiffstats
path: root/widget
diff options
context:
space:
mode:
authorAlice Yang <alice@google.com>2012-05-09 10:02:53 -0700
committerAlice Yang <alice@google.com>2012-05-09 10:02:53 -0700
commit8347d6ca5f1b3f4f1594d3bc16a0be2de4cc214a (patch)
treebc23e022c86ffc662dbe2c9ef431c0e46e8d0cf3 /widget
parent7e12aaa7748c44952d91b88c6f2863c16fe06deb (diff)
downloadandroid_frameworks_ex-8347d6ca5f1b3f4f1594d3bc16a0be2de4cc214a.tar.gz
android_frameworks_ex-8347d6ca5f1b3f4f1594d3bc16a0be2de4cc214a.tar.bz2
android_frameworks_ex-8347d6ca5f1b3f4f1594d3bc16a0be2de4cc214a.zip
Move StaggeredGridView into ex from v4 support library
Move this class because it's still experimental and undergoing development. Below is the git history log of the original file. ************************************************************* commit b469af6dc2f8cda4020a78fb4582c1483089fd6e Author: Adam Powell <adamp@google.com> Date: Thu Apr 12 10:58:19 2012 -0700 Post invalidations on the animation tick if available for StaggeredGridView/ViewPager. Fix interaction with nested scrolling in ViewPager. Finish settling a ViewPager that is close enough to the end of a fling when touched. Change-Id: Ia68be871c92cdb448d4030fdc4b806c78ea69c1a commit 77e2f963d4bcac5e0a4704cd4ea684b04735053e Author: Katherine Kuan <katherinekuan@google.com> Date: Mon Apr 2 14:46:54 2012 -0700 Reset item tops and bottoms for StaggeredGridView When the StaggeredGridView is going to display a completely new set of data, offer a reset method so item top and bottom information is cleared. Fix NPE when adapter notifyDataSetChanged() is called. Bug: 6282841 Change-Id: If1aee80358e32420d2b3a0d4e48ede39002d650a commit 25df93b813befb678835eb040c8158bb5aa8fe8d Author: Adam Powell <adamp@google.com> Date: Mon Mar 19 20:46:57 2012 -0700 Fix a bug with changing the column count in StaggeredGridView. Change-Id: I7aa6544e03e4c7426c96a906f7c78b91d2e8ebb6 commit 121dba12686f0ff01cfe348c43a4f6f0edc18477 Author: Adam Powell <adamp@google.com> Date: Mon Mar 19 20:22:22 2012 -0700 Properly handle top padding in StaggeredGridView while freshly populating items. Change-Id: I6aa920320217017bc8720a4170739aee06b17537 commit 346e2f2390f0d743fd10e7d01a015df6b32292cd Author: Adam Powell <adamp@google.com> Date: Tue Feb 28 10:13:02 2012 -0800 StaggeredGridView and supporting functionality Stable IDs are not yet supported. Move/rename HCSparseArray => SparseArrayCompat; make it public. Add some new features to ViewCompat. Add ScrollerCompat; leave it package-private for now as it needs a reasonable fallback implementation for new methods. Change-Id: I87d6952ef2c7748a40558759372a2525d6a52cf0 Change-Id: I794a7b9666b0414d75a99ff2a5f939193b78026a
Diffstat (limited to 'widget')
-rw-r--r--widget/java/com/android/ex/widget/StaggeredGridView.java1621
1 files changed, 1621 insertions, 0 deletions
diff --git a/widget/java/com/android/ex/widget/StaggeredGridView.java b/widget/java/com/android/ex/widget/StaggeredGridView.java
new file mode 100644
index 0000000..6b6b938
--- /dev/null
+++ b/widget/java/com/android/ex/widget/StaggeredGridView.java
@@ -0,0 +1,1621 @@
+/*
+ * Copyright (C) 2012 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.ex.widget;
+
+import android.content.Context;
+import android.content.res.TypedArray;
+import android.database.DataSetObserver;
+import android.graphics.Canvas;
+import android.os.Parcel;
+import android.os.Parcelable;
+import android.support.v4.util.SparseArrayCompat;
+import android.support.v4.view.MotionEventCompat;
+import android.support.v4.view.VelocityTrackerCompat;
+import android.support.v4.view.ViewCompat;
+import android.util.AttributeSet;
+import android.util.Log;
+import android.util.SparseArray;
+import android.view.MotionEvent;
+import android.view.VelocityTracker;
+import android.view.View;
+import android.view.ViewConfiguration;
+import android.view.ViewGroup;
+import android.widget.ListAdapter;
+
+import java.util.ArrayList;
+import java.util.Arrays;
+
+/**
+ * ListView and GridView just not complex enough? Try StaggeredGridView!
+ *
+ * <p>StaggeredGridView presents a multi-column grid with consistent column sizes
+ * but varying row sizes between the columns. Each successive item from a
+ * {@link android.widget.ListAdapter ListAdapter} will be arranged from top to bottom,
+ * left to right. The largest vertical gap is always filled first.</p>
+ *
+ * <p>Item views may span multiple columns as specified by their {@link LayoutParams}.
+ * The attribute <code>android:layout_span</code> may be used when inflating
+ * item views from xml.</p>
+ *
+ * <p>This class is still under development and is not fully functional yet.</p>
+ */
+public class StaggeredGridView extends ViewGroup {
+ private static final String TAG = "StaggeredGridView";
+ private static final boolean DEBUG = false;
+
+ /*
+ * There are a few things you should know if you're going to make modifications
+ * to StaggeredGridView.
+ *
+ * Like ListView, SGV populates from an adapter and recycles views that fall out
+ * of the visible boundaries of the grid. A few invariants always hold:
+ *
+ * - mFirstPosition is the adapter position of the View returned by getChildAt(0).
+ * - Any child index can be translated to an adapter position by adding mFirstPosition.
+ * - Any adapter position can be translated to a child index by subtracting mFirstPosition.
+ * - Views for items in the range [mFirstPosition, mFirstPosition + getChildCount()) are
+ * currently attached to the grid as children. All other adapter positions do not have
+ * active views.
+ *
+ * This means a few things thanks to the staggered grid's nature. Some views may stay attached
+ * long after they have scrolled offscreen if removing and recycling them would result in
+ * breaking one of the invariants above.
+ *
+ * LayoutRecords are used to track data about a particular item's layout after the associated
+ * view has been removed. These let positioning and the choice of column for an item
+ * remain consistent even though the rules for filling content up vs. filling down vary.
+ *
+ * Whenever layout parameters for a known LayoutRecord change, other LayoutRecords before
+ * or after it may need to be invalidated. e.g. if the item's height or the number
+ * of columns it spans changes, all bets for other items in the same direction are off
+ * since the cached information no longer applies.
+ */
+
+ private ListAdapter mAdapter;
+
+ public static final int COLUMN_COUNT_AUTO = -1;
+
+ private int mColCountSetting = 2;
+ private int mColCount = 2;
+ private int mMinColWidth = 0;
+ private int mItemMargin;
+
+ private int[] mItemTops;
+ private int[] mItemBottoms;
+
+ private boolean mFastChildLayout;
+ private boolean mPopulating;
+ private boolean mForcePopulateOnLayout;
+ private boolean mInLayout;
+ private int mRestoreOffset;
+
+ private final RecycleBin mRecycler = new RecycleBin();
+
+ private final AdapterDataSetObserver mObserver = new AdapterDataSetObserver();
+
+ private boolean mDataChanged;
+ private int mOldItemCount;
+ private int mItemCount;
+ private boolean mHasStableIds;
+
+ private int mFirstPosition;
+
+ private int mTouchSlop;
+ private int mMaximumVelocity;
+ private int mFlingVelocity;
+ private float mLastTouchY;
+ private float mTouchRemainderY;
+ private int mActivePointerId;
+
+ private static final int TOUCH_MODE_IDLE = 0;
+ private static final int TOUCH_MODE_DRAGGING = 1;
+ private static final int TOUCH_MODE_FLINGING = 2;
+
+ private int mTouchMode;
+ private final VelocityTracker mVelocityTracker = VelocityTracker.obtain();
+ private final ScrollerCompat mScroller;
+
+ private final EdgeEffectCompat mTopEdge;
+ private final EdgeEffectCompat mBottomEdge;
+
+ private static final class LayoutRecord {
+ public int column;
+ public long id = -1;
+ public int height;
+ public int span;
+ private int[] mMargins;
+
+ private final void ensureMargins() {
+ if (mMargins == null) {
+ // Don't need to confirm length;
+ // all layoutrecords are purged when column count changes.
+ mMargins = new int[span * 2];
+ }
+ }
+
+ public final int getMarginAbove(int col) {
+ if (mMargins == null) {
+ return 0;
+ }
+ return mMargins[col * 2];
+ }
+
+ public final int getMarginBelow(int col) {
+ if (mMargins == null) {
+ return 0;
+ }
+ return mMargins[col * 2 + 1];
+ }
+
+ public final void setMarginAbove(int col, int margin) {
+ if (mMargins == null && margin == 0) {
+ return;
+ }
+ ensureMargins();
+ mMargins[col * 2] = margin;
+ }
+
+ public final void setMarginBelow(int col, int margin) {
+ if (mMargins == null && margin == 0) {
+ return;
+ }
+ ensureMargins();
+ mMargins[col * 2 + 1] = margin;
+ }
+
+ @Override
+ public String toString() {
+ String result = "LayoutRecord{c=" + column + ", id=" + id + " h=" + height +
+ " s=" + span;
+ if (mMargins != null) {
+ result += " margins[above, below](";
+ for (int i = 0; i < mMargins.length; i += 2) {
+ result += "[" + mMargins[i] + ", " + mMargins[i+1] + "]";
+ }
+ result += ")";
+ }
+ return result + "}";
+ }
+ }
+ private final SparseArrayCompat<LayoutRecord> mLayoutRecords =
+ new SparseArrayCompat<LayoutRecord>();
+
+ public StaggeredGridView(Context context) {
+ this(context, null);
+ }
+
+ public StaggeredGridView(Context context, AttributeSet attrs) {
+ this(context, attrs, 0);
+ }
+
+ public StaggeredGridView(Context context, AttributeSet attrs, int defStyle) {
+ super(context, attrs, defStyle);
+
+ final ViewConfiguration vc = ViewConfiguration.get(context);
+ mTouchSlop = vc.getScaledTouchSlop();
+ mMaximumVelocity = vc.getScaledMaximumFlingVelocity();
+ mFlingVelocity = vc.getScaledMinimumFlingVelocity();
+ mScroller = ScrollerCompat.from(context);
+
+ mTopEdge = new EdgeEffectCompat(context);
+ mBottomEdge = new EdgeEffectCompat(context);
+ setWillNotDraw(false);
+ setClipToPadding(false);
+ }
+
+ /**
+ * Set a fixed number of columns for this grid. Space will be divided evenly
+ * among all columns, respecting the item margin between columns.
+ * The default is 2. (If it were 1, perhaps you should be using a
+ * {@link android.widget.ListView ListView}.)
+ *
+ * @param colCount Number of columns to display.
+ * @see #setMinColumnWidth(int)
+ */
+ public void setColumnCount(int colCount) {
+ if (colCount < 1 && colCount != COLUMN_COUNT_AUTO) {
+ throw new IllegalArgumentException("Column count must be at least 1 - received " +
+ colCount);
+ }
+ final boolean needsPopulate = colCount != mColCount;
+ mColCount = mColCountSetting = colCount;
+ if (needsPopulate) {
+ populate();
+ }
+ }
+
+ public int getColumnCount() {
+ return mColCount;
+ }
+
+ /**
+ * Set a minimum column width for
+ * @param minColWidth
+ */
+ public void setMinColumnWidth(int minColWidth) {
+ mMinColWidth = minColWidth;
+ setColumnCount(COLUMN_COUNT_AUTO);
+ }
+
+ /**
+ * Set the margin between items in pixels. This margin is applied
+ * both vertically and horizontally.
+ *
+ * @param marginPixels Spacing between items in pixels
+ */
+ public void setItemMargin(int marginPixels) {
+ final boolean needsPopulate = marginPixels != mItemMargin;
+ mItemMargin = marginPixels;
+ if (needsPopulate) {
+ populate();
+ }
+ }
+
+ /**
+ * Return the first adapter position with a view currently attached as
+ * a child view of this grid.
+ *
+ * @return the adapter position represented by the view at getChildAt(0).
+ */
+ public int getFirstPosition() {
+ return mFirstPosition;
+ }
+
+ @Override
+ public boolean onInterceptTouchEvent(MotionEvent ev) {
+ mVelocityTracker.addMovement(ev);
+ final int action = ev.getAction() & MotionEventCompat.ACTION_MASK;
+ switch (action) {
+ case MotionEvent.ACTION_DOWN:
+ mVelocityTracker.clear();
+ mScroller.abortAnimation();
+ mLastTouchY = ev.getY();
+ mActivePointerId = MotionEventCompat.getPointerId(ev, 0);
+ mTouchRemainderY = 0;
+ if (mTouchMode == TOUCH_MODE_FLINGING) {
+ // Catch!
+ mTouchMode = TOUCH_MODE_DRAGGING;
+ return true;
+ }
+ break;
+
+ case MotionEvent.ACTION_MOVE: {
+ final int index = MotionEventCompat.findPointerIndex(ev, mActivePointerId);
+ if (index < 0) {
+ Log.e(TAG, "onInterceptTouchEvent could not find pointer with id " +
+ mActivePointerId + " - did StaggeredGridView receive an inconsistent " +
+ "event stream?");
+ return false;
+ }
+ final float y = MotionEventCompat.getY(ev, index);
+ final float dy = y - mLastTouchY + mTouchRemainderY;
+ final int deltaY = (int) dy;
+ mTouchRemainderY = dy - deltaY;
+
+ if (Math.abs(dy) > mTouchSlop) {
+ mTouchMode = TOUCH_MODE_DRAGGING;
+ return true;
+ }
+ }
+ }
+
+ return false;
+ }
+
+ @Override
+ public boolean onTouchEvent(MotionEvent ev) {
+ mVelocityTracker.addMovement(ev);
+ final int action = ev.getAction() & MotionEventCompat.ACTION_MASK;
+ switch (action) {
+ case MotionEvent.ACTION_DOWN:
+ mVelocityTracker.clear();
+ mScroller.abortAnimation();
+ mLastTouchY = ev.getY();
+ mActivePointerId = MotionEventCompat.getPointerId(ev, 0);
+ mTouchRemainderY = 0;
+ break;
+
+ case MotionEvent.ACTION_MOVE: {
+ final int index = MotionEventCompat.findPointerIndex(ev, mActivePointerId);
+ if (index < 0) {
+ Log.e(TAG, "onInterceptTouchEvent could not find pointer with id " +
+ mActivePointerId + " - did StaggeredGridView receive an inconsistent " +
+ "event stream?");
+ return false;
+ }
+ final float y = MotionEventCompat.getY(ev, index);
+ final float dy = y - mLastTouchY + mTouchRemainderY;
+ final int deltaY = (int) dy;
+ mTouchRemainderY = dy - deltaY;
+
+ if (Math.abs(dy) > mTouchSlop) {
+ mTouchMode = TOUCH_MODE_DRAGGING;
+ }
+
+ if (mTouchMode == TOUCH_MODE_DRAGGING) {
+ mLastTouchY = y;
+
+ if (!trackMotionScroll(deltaY, true)) {
+ // Break fling velocity if we impacted an edge.
+ mVelocityTracker.clear();
+ }
+ }
+ } break;
+
+ case MotionEvent.ACTION_CANCEL:
+ mTouchMode = TOUCH_MODE_IDLE;
+ break;
+
+ case MotionEvent.ACTION_UP: {
+ mVelocityTracker.computeCurrentVelocity(1000, mMaximumVelocity);
+ final float velocity = VelocityTrackerCompat.getYVelocity(mVelocityTracker,
+ mActivePointerId);
+ if (Math.abs(velocity) > mFlingVelocity) { // TODO
+ mTouchMode = TOUCH_MODE_FLINGING;
+ mScroller.fling(0, 0, 0, (int) velocity, 0, 0,
+ Integer.MIN_VALUE, Integer.MAX_VALUE);
+ mLastTouchY = 0;
+ ViewCompat.postInvalidateOnAnimation(this);
+ } else {
+ mTouchMode = TOUCH_MODE_IDLE;
+ }
+
+ } break;
+ }
+ return true;
+ }
+
+ /**
+ *
+ * @param deltaY Pixels that content should move by
+ * @return true if the movement completed, false if it was stopped prematurely.
+ */
+ private boolean trackMotionScroll(int deltaY, boolean allowOverScroll) {
+ final boolean contentFits = contentFits();
+ final int allowOverhang = Math.abs(deltaY);
+
+ final int overScrolledBy;
+ final int movedBy;
+ if (!contentFits) {
+ final int overhang;
+ final boolean up;
+ mPopulating = true;
+ if (deltaY > 0) {
+ overhang = fillUp(mFirstPosition - 1, allowOverhang);
+ up = true;
+ } else {
+ overhang = fillDown(mFirstPosition + getChildCount(), allowOverhang) + mItemMargin;
+ up = false;
+ }
+ movedBy = Math.min(overhang, allowOverhang);
+ offsetChildren(up ? movedBy : -movedBy);
+ recycleOffscreenViews();
+ mPopulating = false;
+ overScrolledBy = allowOverhang - overhang;
+ } else {
+ overScrolledBy = allowOverhang;
+ movedBy = 0;
+ }
+
+ if (allowOverScroll) {
+ final int overScrollMode = ViewCompat.getOverScrollMode(this);
+
+ if (overScrollMode == ViewCompat.OVER_SCROLL_ALWAYS ||
+ (overScrollMode == ViewCompat.OVER_SCROLL_IF_CONTENT_SCROLLS && !contentFits)) {
+
+ if (overScrolledBy > 0) {
+ EdgeEffectCompat edge = deltaY > 0 ? mTopEdge : mBottomEdge;
+ edge.onPull((float) Math.abs(deltaY) / getHeight());
+ ViewCompat.postInvalidateOnAnimation(this);
+ }
+ }
+ }
+
+ return deltaY == 0 || movedBy != 0;
+ }
+
+ private final boolean contentFits() {
+ if (mFirstPosition != 0 || getChildCount() != mItemCount) {
+ return false;
+ }
+
+ int topmost = Integer.MAX_VALUE;
+ int bottommost = Integer.MIN_VALUE;
+ for (int i = 0; i < mColCount; i++) {
+ if (mItemTops[i] < topmost) {
+ topmost = mItemTops[i];
+ }
+ if (mItemBottoms[i] > bottommost) {
+ bottommost = mItemBottoms[i];
+ }
+ }
+
+ return topmost >= getPaddingTop() && bottommost <= getHeight() - getPaddingBottom();
+ }
+
+ private void recycleAllViews() {
+ for (int i = 0; i < getChildCount(); i++) {
+ mRecycler.addScrap(getChildAt(i));
+ }
+
+ if (mInLayout) {
+ removeAllViewsInLayout();
+ } else {
+ removeAllViews();
+ }
+ }
+
+ /**
+ * Important: this method will leave offscreen views attached if they
+ * are required to maintain the invariant that child view with index i
+ * is always the view corresponding to position mFirstPosition + i.
+ */
+ private void recycleOffscreenViews() {
+ final int height = getHeight();
+ final int clearAbove = -mItemMargin;
+ final int clearBelow = height + mItemMargin;
+ for (int i = getChildCount() - 1; i >= 0; i--) {
+ final View child = getChildAt(i);
+ if (child.getTop() <= clearBelow) {
+ // There may be other offscreen views, but we need to maintain
+ // the invariant documented above.
+ break;
+ }
+
+ if (mInLayout) {
+ removeViewsInLayout(i, 1);
+ } else {
+ removeViewAt(i);
+ }
+
+ mRecycler.addScrap(child);
+ }
+
+ while (getChildCount() > 0) {
+ final View child = getChildAt(0);
+ if (child.getBottom() >= clearAbove) {
+ // There may be other offscreen views, but we need to maintain
+ // the invariant documented above.
+ break;
+ }
+
+ if (mInLayout) {
+ removeViewsInLayout(0, 1);
+ } else {
+ removeViewAt(0);
+ }
+
+ mRecycler.addScrap(child);
+ mFirstPosition++;
+ }
+
+ final int childCount = getChildCount();
+ if (childCount > 0) {
+ // Repair the top and bottom column boundaries from the views we still have
+ Arrays.fill(mItemTops, Integer.MAX_VALUE);
+ Arrays.fill(mItemBottoms, Integer.MIN_VALUE);
+
+ for (int i = 0; i < childCount; i++){
+ final View child = getChildAt(i);
+ final LayoutParams lp = (LayoutParams) child.getLayoutParams();
+ final int top = child.getTop() - mItemMargin;
+ final int bottom = child.getBottom();
+ final LayoutRecord rec = mLayoutRecords.get(mFirstPosition + i);
+
+ final int colEnd = lp.column + Math.min(mColCount, lp.span);
+ for (int col = lp.column; col < colEnd; col++) {
+ final int colTop = top - rec.getMarginAbove(col - lp.column);
+ final int colBottom = bottom + rec.getMarginBelow(col - lp.column);
+ if (colTop < mItemTops[col]) {
+ mItemTops[col] = colTop;
+ }
+ if (colBottom > mItemBottoms[col]) {
+ mItemBottoms[col] = colBottom;
+ }
+ }
+ }
+
+ for (int col = 0; col < mColCount; col++) {
+ if (mItemTops[col] == Integer.MAX_VALUE) {
+ // If one was untouched, both were.
+ mItemTops[col] = 0;
+ mItemBottoms[col] = 0;
+ }
+ }
+ }
+ }
+
+ public void computeScroll() {
+ if (mScroller.computeScrollOffset()) {
+ final int y = mScroller.getCurrY();
+ final int dy = (int) (y - mLastTouchY);
+ mLastTouchY = y;
+ final boolean stopped = !trackMotionScroll(dy, false);
+
+ if (!stopped && !mScroller.isFinished()) {
+ ViewCompat.postInvalidateOnAnimation(this);
+ } else {
+ if (stopped) {
+ final int overScrollMode = ViewCompat.getOverScrollMode(this);
+ if (overScrollMode != ViewCompat.OVER_SCROLL_NEVER) {
+ final EdgeEffectCompat edge;
+ if (dy > 0) {
+ edge = mTopEdge;
+ } else {
+ edge = mBottomEdge;
+ }
+ edge.onAbsorb(Math.abs((int) mScroller.getCurrVelocity()));
+ ViewCompat.postInvalidateOnAnimation(this);
+ }
+ mScroller.abortAnimation();
+ }
+ mTouchMode = TOUCH_MODE_IDLE;
+ }
+ }
+ }
+
+ @Override
+ public void draw(Canvas canvas) {
+ super.draw(canvas);
+
+ if (mTopEdge != null) {
+ boolean needsInvalidate = false;
+ if (!mTopEdge.isFinished()) {
+ mTopEdge.draw(canvas);
+ needsInvalidate = true;
+ }
+ if (!mBottomEdge.isFinished()) {
+ final int restoreCount = canvas.save();
+ final int width = getWidth();
+ canvas.translate(-width, getHeight());
+ canvas.rotate(180, width, 0);
+ mBottomEdge.draw(canvas);
+ canvas.restoreToCount(restoreCount);
+ needsInvalidate = true;
+ }
+
+ if (needsInvalidate) {
+ ViewCompat.postInvalidateOnAnimation(this);
+ }
+ }
+ }
+
+ public void beginFastChildLayout() {
+ mFastChildLayout = true;
+ }
+
+ public void endFastChildLayout() {
+ mFastChildLayout = false;
+ populate();
+ }
+
+ @Override
+ public void requestLayout() {
+ if (!mPopulating && !mFastChildLayout) {
+ super.requestLayout();
+ }
+ }
+
+ @Override
+ protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
+ int widthMode = MeasureSpec.getMode(widthMeasureSpec);
+ int heightMode = MeasureSpec.getMode(heightMeasureSpec);
+ int widthSize = MeasureSpec.getSize(widthMeasureSpec);
+ int heightSize = MeasureSpec.getSize(heightMeasureSpec);
+
+ if (widthMode != MeasureSpec.EXACTLY) {
+ Log.e(TAG, "onMeasure: must have an exact width or match_parent! " +
+ "Using fallback spec of EXACTLY " + widthSize);
+ widthMode = MeasureSpec.EXACTLY;
+ }
+ if (heightMode != MeasureSpec.EXACTLY) {
+ Log.e(TAG, "onMeasure: must have an exact height or match_parent! " +
+ "Using fallback spec of EXACTLY " + heightSize);
+ heightMode = MeasureSpec.EXACTLY;
+ }
+
+ setMeasuredDimension(widthSize, heightSize);
+
+ if (mColCountSetting == COLUMN_COUNT_AUTO) {
+ final int colCount = widthSize / mMinColWidth;
+ if (colCount != mColCount) {
+ mColCount = colCount;
+ mForcePopulateOnLayout = true;
+ }
+ }
+ }
+
+ @Override
+ protected void onLayout(boolean changed, int l, int t, int r, int b) {
+ mInLayout = true;
+ populate();
+ mInLayout = false;
+ mForcePopulateOnLayout = false;
+
+ final int width = r - l;
+ final int height = b - t;
+ mTopEdge.setSize(width, height);
+ mBottomEdge.setSize(width, height);
+ }
+
+ private void populate() {
+ if (getWidth() == 0 || getHeight() == 0) {
+ return;
+ }
+
+ if (mColCount == COLUMN_COUNT_AUTO) {
+ final int colCount = getWidth() / mMinColWidth;
+ if (colCount != mColCount) {
+ mColCount = colCount;
+ }
+ }
+
+ final int colCount = mColCount;
+ if (mItemTops == null || mItemTops.length != colCount) {
+ mItemTops = new int[colCount];
+ mItemBottoms = new int[colCount];
+ final int top = getPaddingTop();
+ final int offset = top + Math.min(mRestoreOffset, 0);
+ Arrays.fill(mItemTops, offset);
+ Arrays.fill(mItemBottoms, offset);
+ mLayoutRecords.clear();
+ if (mInLayout) {
+ removeAllViewsInLayout();
+ } else {
+ removeAllViews();
+ }
+ mRestoreOffset = 0;
+ }
+
+ mPopulating = true;
+ layoutChildren(mDataChanged);
+ fillDown(mFirstPosition + getChildCount(), 0);
+ fillUp(mFirstPosition - 1, 0);
+ mPopulating = false;
+ mDataChanged = false;
+ }
+
+ private void dumpItemPositions() {
+ final int childCount = getChildCount();
+ Log.d(TAG, "dumpItemPositions:");
+ Log.d(TAG, " => Tops:");
+ for (int i = 0; i < mColCount; i++) {
+ Log.d(TAG, " => " + mItemTops[i]);
+ boolean found = false;
+ for (int j = 0; j < childCount; j++) {
+ final View child = getChildAt(j);
+ if (mItemTops[i] == child.getTop() - mItemMargin) {
+ found = true;
+ }
+ }
+ if (!found) {
+ Log.d(TAG, "!!! No top item found for column " + i + " value " + mItemTops[i]);
+ }
+ }
+ Log.d(TAG, " => Bottoms:");
+ for (int i = 0; i < mColCount; i++) {
+ Log.d(TAG, " => " + mItemBottoms[i]);
+ boolean found = false;
+ for (int j = 0; j < childCount; j++) {
+ final View child = getChildAt(j);
+ if (mItemBottoms[i] == child.getBottom()) {
+ found = true;
+ }
+ }
+ if (!found) {
+ Log.d(TAG, "!!! No bottom item found for column " + i + " value " + mItemBottoms[i]);
+ }
+ }
+ }
+
+ final void offsetChildren(int offset) {
+ final int childCount = getChildCount();
+ for (int i = 0; i < childCount; i++) {
+ final View child = getChildAt(i);
+ child.layout(child.getLeft(), child.getTop() + offset,
+ child.getRight(), child.getBottom() + offset);
+ }
+
+ final int colCount = mColCount;
+ for (int i = 0; i < colCount; i++) {
+ mItemTops[i] += offset;
+ mItemBottoms[i] += offset;
+ }
+ }
+
+ /**
+ * Measure and layout all currently visible children.
+ *
+ * @param queryAdapter true to requery the adapter for view data
+ */
+ final void layoutChildren(boolean queryAdapter) {
+ final int paddingLeft = getPaddingLeft();
+ final int paddingRight = getPaddingRight();
+ final int itemMargin = mItemMargin;
+ final int colWidth =
+ (getWidth() - paddingLeft - paddingRight - itemMargin * (mColCount - 1)) / mColCount;
+ int rebuildLayoutRecordsBefore = -1;
+ int rebuildLayoutRecordsAfter = -1;
+
+ Arrays.fill(mItemBottoms, Integer.MIN_VALUE);
+
+ final int childCount = getChildCount();
+ for (int i = 0; i < childCount; i++) {
+ View child = getChildAt(i);
+ LayoutParams lp = (LayoutParams) child.getLayoutParams();
+ final int col = lp.column;
+ final int position = mFirstPosition + i;
+ final boolean needsLayout = queryAdapter || child.isLayoutRequested();
+
+ if (queryAdapter) {
+ View newView = obtainView(position, child);
+ if (newView != child) {
+ removeViewAt(i);
+ addView(newView, i);
+ child = newView;
+ }
+ lp = (LayoutParams) child.getLayoutParams(); // Might have changed
+ }
+
+ final int span = Math.min(mColCount, lp.span);
+ final int widthSize = colWidth * span + itemMargin * (span - 1);
+
+ if (needsLayout) {
+ final int widthSpec = MeasureSpec.makeMeasureSpec(widthSize, MeasureSpec.EXACTLY);
+
+ final int heightSpec;
+ if (lp.height == LayoutParams.WRAP_CONTENT) {
+ heightSpec = MeasureSpec.makeMeasureSpec(0, MeasureSpec.UNSPECIFIED);
+ } else {
+ heightSpec = MeasureSpec.makeMeasureSpec(lp.height, MeasureSpec.EXACTLY);
+ }
+
+ child.measure(widthSpec, heightSpec);
+ }
+
+ int childTop = mItemBottoms[col] > Integer.MIN_VALUE ?
+ mItemBottoms[col] + mItemMargin : child.getTop();
+ if (span > 1) {
+ int lowest = childTop;
+ for (int j = col + 1; j < col + span; j++) {
+ final int bottom = mItemBottoms[j] + mItemMargin;
+ if (bottom > lowest) {
+ lowest = bottom;
+ }
+ }
+ childTop = lowest;
+ }
+ final int childHeight = child.getMeasuredHeight();
+ final int childBottom = childTop + childHeight;
+ final int childLeft = paddingLeft + col * (colWidth + itemMargin);
+ final int childRight = childLeft + child.getMeasuredWidth();
+ child.layout(childLeft, childTop, childRight, childBottom);
+
+ for (int j = col; j < col + span; j++) {
+ mItemBottoms[j] = childBottom;
+ }
+
+ final LayoutRecord rec = mLayoutRecords.get(position);
+ if (rec != null && rec.height != childHeight) {
+ // Invalidate our layout records for everything before this.
+ rec.height = childHeight;
+ rebuildLayoutRecordsBefore = position;
+ }
+
+ if (rec != null && rec.span != span) {
+ // Invalidate our layout records for everything after this.
+ rec.span = span;
+ rebuildLayoutRecordsAfter = position;
+ }
+ }
+
+ // Update mItemBottoms for any empty columns
+ for (int i = 0; i < mColCount; i++) {
+ if (mItemBottoms[i] == Integer.MIN_VALUE) {
+ mItemBottoms[i] = mItemTops[i];
+ }
+ }
+
+ if (rebuildLayoutRecordsBefore >= 0 || rebuildLayoutRecordsAfter >= 0) {
+ if (rebuildLayoutRecordsBefore >= 0) {
+ invalidateLayoutRecordsBeforePosition(rebuildLayoutRecordsBefore);
+ }
+ if (rebuildLayoutRecordsAfter >= 0) {
+ invalidateLayoutRecordsAfterPosition(rebuildLayoutRecordsAfter);
+ }
+ for (int i = 0; i < childCount; i++) {
+ final int position = mFirstPosition + i;
+ final View child = getChildAt(i);
+ final LayoutParams lp = (LayoutParams) child.getLayoutParams();
+ LayoutRecord rec = mLayoutRecords.get(position);
+ if (rec == null) {
+ rec = new LayoutRecord();
+ mLayoutRecords.put(position, rec);
+ }
+ rec.column = lp.column;
+ rec.height = child.getHeight();
+ rec.id = lp.id;
+ rec.span = Math.min(mColCount, lp.span);
+ }
+ }
+ }
+
+ final void invalidateLayoutRecordsBeforePosition(int position) {
+ int endAt = 0;
+ while (endAt < mLayoutRecords.size() && mLayoutRecords.keyAt(endAt) < position) {
+ endAt++;
+ }
+ mLayoutRecords.removeAtRange(0, endAt);
+ }
+
+ final void invalidateLayoutRecordsAfterPosition(int position) {
+ int beginAt = mLayoutRecords.size() - 1;
+ while (beginAt >= 0 && mLayoutRecords.keyAt(beginAt) > position) {
+ beginAt--;
+ }
+ beginAt++;
+ mLayoutRecords.removeAtRange(beginAt + 1, mLayoutRecords.size() - beginAt);
+ }
+
+ /**
+ * Should be called with mPopulating set to true
+ *
+ * @param fromPosition Position to start filling from
+ * @param overhang the number of extra pixels to fill beyond the current top edge
+ * @return the max overhang beyond the beginning of the view of any added items at the top
+ */
+ final int fillUp(int fromPosition, int overhang) {
+ final int paddingLeft = getPaddingLeft();
+ final int paddingRight = getPaddingRight();
+ final int itemMargin = mItemMargin;
+ final int colWidth =
+ (getWidth() - paddingLeft - paddingRight - itemMargin * (mColCount - 1)) / mColCount;
+ final int gridTop = getPaddingTop();
+ final int fillTo = gridTop - overhang;
+ int nextCol = getNextColumnUp();
+ int position = fromPosition;
+
+ while (nextCol >= 0 && mItemTops[nextCol] > fillTo && position >= 0) {
+ final View child = obtainView(position, null);
+ LayoutParams lp = (LayoutParams) child.getLayoutParams();
+
+ if (child.getParent() != this) {
+ if (mInLayout) {
+ addViewInLayout(child, 0, lp);
+ } else {
+ addView(child, 0);
+ }
+ }
+
+ final int span = Math.min(mColCount, lp.span);
+ final int widthSize = colWidth * span + itemMargin * (span - 1);
+ final int widthSpec = MeasureSpec.makeMeasureSpec(widthSize, MeasureSpec.EXACTLY);
+
+ LayoutRecord rec;
+ if (span > 1) {
+ rec = getNextRecordUp(position, span);
+ nextCol = rec.column;
+ } else {
+ rec = mLayoutRecords.get(position);
+ }
+
+ boolean invalidateBefore = false;
+ if (rec == null) {
+ rec = new LayoutRecord();
+ mLayoutRecords.put(position, rec);
+ rec.column = nextCol;
+ rec.span = span;
+ } else if (span != rec.span) {
+ rec.span = span;
+ rec.column = nextCol;
+ invalidateBefore = true;
+ } else {
+ nextCol = rec.column;
+ }
+
+ if (mHasStableIds) {
+ final long id = mAdapter.getItemId(position);
+ rec.id = id;
+ lp.id = id;
+ }
+
+ lp.column = nextCol;
+
+ final int heightSpec;
+ if (lp.height == LayoutParams.WRAP_CONTENT) {
+ heightSpec = MeasureSpec.makeMeasureSpec(0, MeasureSpec.UNSPECIFIED);
+ } else {
+ heightSpec = MeasureSpec.makeMeasureSpec(lp.height, MeasureSpec.EXACTLY);
+ }
+ child.measure(widthSpec, heightSpec);
+
+ final int childHeight = child.getMeasuredHeight();
+ if (invalidateBefore || (childHeight != rec.height && rec.height > 0)) {
+ invalidateLayoutRecordsBeforePosition(position);
+ }
+ rec.height = childHeight;
+
+ final int startFrom;
+ if (span > 1) {
+ int highest = mItemTops[nextCol];
+ for (int i = nextCol + 1; i < nextCol + span; i++) {
+ final int top = mItemTops[i];
+ if (top < highest) {
+ highest = top;
+ }
+ }
+ startFrom = highest;
+ } else {
+ startFrom = mItemTops[nextCol];
+ }
+ final int childBottom = startFrom;
+ final int childTop = childBottom - childHeight;
+ final int childLeft = paddingLeft + nextCol * (colWidth + itemMargin);
+ final int childRight = childLeft + child.getMeasuredWidth();
+ child.layout(childLeft, childTop, childRight, childBottom);
+
+ for (int i = nextCol; i < nextCol + span; i++) {
+ mItemTops[i] = childTop - rec.getMarginAbove(i - nextCol) - itemMargin;
+ }
+
+ nextCol = getNextColumnUp();
+ mFirstPosition = position--;
+ }
+
+ int highestView = getHeight();
+ for (int i = 0; i < mColCount; i++) {
+ if (mItemTops[i] < highestView) {
+ highestView = mItemTops[i];
+ }
+ }
+ return gridTop - highestView;
+ }
+
+ /**
+ * Should be called with mPopulating set to true
+ *
+ * @param fromPosition Position to start filling from
+ * @param overhang the number of extra pixels to fill beyond the current bottom edge
+ * @return the max overhang beyond the end of the view of any added items at the bottom
+ */
+ final int fillDown(int fromPosition, int overhang) {
+ final int paddingLeft = getPaddingLeft();
+ final int paddingRight = getPaddingRight();
+ final int itemMargin = mItemMargin;
+ final int colWidth =
+ (getWidth() - paddingLeft - paddingRight - itemMargin * (mColCount - 1)) / mColCount;
+ final int gridBottom = getHeight() - getPaddingBottom();
+ final int fillTo = gridBottom + overhang;
+ int nextCol = getNextColumnDown();
+ int position = fromPosition;
+
+ while (nextCol >= 0 && mItemBottoms[nextCol] < fillTo && position < mItemCount) {
+ final View child = obtainView(position, null);
+ LayoutParams lp = (LayoutParams) child.getLayoutParams();
+
+ if (child.getParent() != this) {
+ if (mInLayout) {
+ addViewInLayout(child, -1, lp);
+ } else {
+ addView(child);
+ }
+ }
+
+ final int span = Math.min(mColCount, lp.span);
+ final int widthSize = colWidth * span + itemMargin * (span - 1);
+ final int widthSpec = MeasureSpec.makeMeasureSpec(widthSize, MeasureSpec.EXACTLY);
+
+ LayoutRecord rec;
+ if (span > 1) {
+ rec = getNextRecordDown(position, span);
+ nextCol = rec.column;
+ } else {
+ rec = mLayoutRecords.get(position);
+ }
+
+ boolean invalidateAfter = false;
+ if (rec == null) {
+ rec = new LayoutRecord();
+ mLayoutRecords.put(position, rec);
+ rec.column = nextCol;
+ rec.span = span;
+ } else if (span != rec.span) {
+ rec.span = span;
+ rec.column = nextCol;
+ invalidateAfter = true;
+ } else {
+ nextCol = rec.column;
+ }
+
+ if (mHasStableIds) {
+ final long id = mAdapter.getItemId(position);
+ rec.id = id;
+ lp.id = id;
+ }
+
+ lp.column = nextCol;
+
+ final int heightSpec;
+ if (lp.height == LayoutParams.WRAP_CONTENT) {
+ heightSpec = MeasureSpec.makeMeasureSpec(0, MeasureSpec.UNSPECIFIED);
+ } else {
+ heightSpec = MeasureSpec.makeMeasureSpec(lp.height, MeasureSpec.EXACTLY);
+ }
+ child.measure(widthSpec, heightSpec);
+
+ final int childHeight = child.getMeasuredHeight();
+ if (invalidateAfter || (childHeight != rec.height && rec.height > 0)) {
+ invalidateLayoutRecordsAfterPosition(position);
+ }
+ rec.height = childHeight;
+
+ final int startFrom;
+ if (span > 1) {
+ int lowest = mItemBottoms[nextCol];
+ for (int i = nextCol + 1; i < nextCol + span; i++) {
+ final int bottom = mItemBottoms[i];
+ if (bottom > lowest) {
+ lowest = bottom;
+ }
+ }
+ startFrom = lowest;
+ } else {
+ startFrom = mItemBottoms[nextCol];
+ }
+ final int childTop = startFrom + itemMargin;
+ final int childBottom = childTop + childHeight;
+ final int childLeft = paddingLeft + nextCol * (colWidth + itemMargin);
+ final int childRight = childLeft + child.getMeasuredWidth();
+ child.layout(childLeft, childTop, childRight, childBottom);
+
+ for (int i = nextCol; i < nextCol + span; i++) {
+ mItemBottoms[i] = childBottom + rec.getMarginBelow(i - nextCol);
+ }
+
+ nextCol = getNextColumnDown();
+ position++;
+ }
+
+ int lowestView = 0;
+ for (int i = 0; i < mColCount; i++) {
+ if (mItemBottoms[i] > lowestView) {
+ lowestView = mItemBottoms[i];
+ }
+ }
+ return lowestView - gridBottom;
+ }
+
+ /**
+ * @return column that the next view filling upwards should occupy. This is the bottom-most
+ * position available for a single-column item.
+ */
+ final int getNextColumnUp() {
+ int result = -1;
+ int bottomMost = Integer.MIN_VALUE;
+
+ final int colCount = mColCount;
+ for (int i = colCount - 1; i >= 0; i--) {
+ final int top = mItemTops[i];
+ if (top > bottomMost) {
+ bottomMost = top;
+ result = i;
+ }
+ }
+ return result;
+ }
+
+ /**
+ * Return a LayoutRecord for the given position
+ * @param position
+ * @param span
+ * @return
+ */
+ final LayoutRecord getNextRecordUp(int position, int span) {
+ LayoutRecord rec = mLayoutRecords.get(position);
+ if (rec == null) {
+ rec = new LayoutRecord();
+ rec.span = span;
+ mLayoutRecords.put(position, rec);
+ } else if (rec.span != span) {
+ throw new IllegalStateException("Invalid LayoutRecord! Record had span=" + rec.span +
+ " but caller requested span=" + span + " for position=" + position);
+ }
+ int targetCol = -1;
+ int bottomMost = Integer.MIN_VALUE;
+
+ final int colCount = mColCount;
+ for (int i = colCount - span; i >= 0; i--) {
+ int top = Integer.MAX_VALUE;
+ for (int j = i; j < i + span; j++) {
+ final int singleTop = mItemTops[j];
+ if (singleTop < top) {
+ top = singleTop;
+ }
+ }
+ if (top > bottomMost) {
+ bottomMost = top;
+ targetCol = i;
+ }
+ }
+
+ rec.column = targetCol;
+
+ for (int i = 0; i < span; i++) {
+ rec.setMarginBelow(i, mItemTops[i + targetCol] - bottomMost);
+ }
+
+ return rec;
+ }
+
+ /**
+ * @return column that the next view filling downwards should occupy. This is the top-most
+ * position available.
+ */
+ final int getNextColumnDown() {
+ int result = -1;
+ int topMost = Integer.MAX_VALUE;
+
+ final int colCount = mColCount;
+ for (int i = 0; i < colCount; i++) {
+ final int bottom = mItemBottoms[i];
+ if (bottom < topMost) {
+ topMost = bottom;
+ result = i;
+ }
+ }
+ return result;
+ }
+
+ final LayoutRecord getNextRecordDown(int position, int span) {
+ LayoutRecord rec = mLayoutRecords.get(position);
+ if (rec == null) {
+ rec = new LayoutRecord();
+ rec.span = span;
+ mLayoutRecords.put(position, rec);
+ } else if (rec.span != span) {
+ throw new IllegalStateException("Invalid LayoutRecord! Record had span=" + rec.span +
+ " but caller requested span=" + span + " for position=" + position);
+ }
+ int targetCol = -1;
+ int topMost = Integer.MAX_VALUE;
+
+ final int colCount = mColCount;
+ for (int i = 0; i <= colCount - span; i++) {
+ int bottom = Integer.MIN_VALUE;
+ for (int j = i; j < i + span; j++) {
+ final int singleBottom = mItemBottoms[j];
+ if (singleBottom > bottom) {
+ bottom = singleBottom;
+ }
+ }
+ if (bottom < topMost) {
+ topMost = bottom;
+ targetCol = i;
+ }
+ }
+
+ rec.column = targetCol;
+
+ for (int i = 0; i < span; i++) {
+ rec.setMarginAbove(i, topMost - mItemBottoms[i + targetCol]);
+ }
+
+ return rec;
+ }
+
+ /**
+ * Obtain a populated view from the adapter. If optScrap is non-null and is not
+ * reused it will be placed in the recycle bin.
+ *
+ * @param position position to get view for
+ * @param optScrap Optional scrap view; will be reused if possible
+ * @return A new view, a recycled view from mRecycler, or optScrap
+ */
+ final View obtainView(int position, View optScrap) {
+ View view = mRecycler.getTransientStateView(position);
+ if (view != null) {
+ return view;
+ }
+
+ // Reuse optScrap if it's of the right type (and not null)
+ final int optType = optScrap != null ?
+ ((LayoutParams) optScrap.getLayoutParams()).viewType : -1;
+ final int positionViewType = mAdapter.getItemViewType(position);
+ final View scrap = optType == positionViewType ?
+ optScrap : mRecycler.getScrapView(positionViewType);
+
+ view = mAdapter.getView(position, scrap, this);
+
+ if (view != scrap && scrap != null) {
+ // The adapter didn't use it; put it back.
+ mRecycler.addScrap(scrap);
+ }
+
+ ViewGroup.LayoutParams lp = view.getLayoutParams();
+
+ if (view.getParent() != this) {
+ if (lp == null) {
+ lp = generateDefaultLayoutParams();
+ } else if (!checkLayoutParams(lp)) {
+ lp = generateLayoutParams(lp);
+ }
+ }
+
+ final LayoutParams sglp = (LayoutParams) lp;
+ sglp.position = position;
+ sglp.viewType = positionViewType;
+
+ return view;
+ }
+
+ public ListAdapter getAdapter() {
+ return mAdapter;
+ }
+
+ public void setAdapter(ListAdapter adapter) {
+ if (mAdapter != null) {
+ mAdapter.unregisterDataSetObserver(mObserver);
+ }
+ // TODO: If the new adapter says that there are stable IDs, remove certain layout records
+ // and onscreen views if they have changed instead of removing all of the state here.
+ clearAllState();
+ mAdapter = adapter;
+ mDataChanged = true;
+ mOldItemCount = mItemCount = adapter != null ? adapter.getCount() : 0;
+ if (adapter != null) {
+ adapter.registerDataSetObserver(mObserver);
+ mRecycler.setViewTypeCount(adapter.getViewTypeCount());
+ mHasStableIds = adapter.hasStableIds();
+ } else {
+ mHasStableIds = false;
+ }
+ populate();
+ }
+
+ /**
+ * Clear all state because the grid will be used for a completely different set of data.
+ */
+ private void clearAllState() {
+ // Clear all layout records and views
+ mLayoutRecords.clear();
+ removeAllViews();
+
+ // Reset to the top of the grid
+ resetStateForGridTop();
+
+ // Clear recycler because there could be different view types now
+ mRecycler.clear();
+ }
+
+ /**
+ * Reset all internal state to be at the top of the grid.
+ */
+ private void resetStateForGridTop() {
+ // Reset mItemTops and mItemBottoms
+ final int colCount = mColCount;
+ if (mItemTops == null || mItemTops.length != colCount) {
+ mItemTops = new int[colCount];
+ mItemBottoms = new int[colCount];
+ }
+ final int top = getPaddingTop();
+ Arrays.fill(mItemTops, top);
+ Arrays.fill(mItemBottoms, top);
+
+ // Reset the first visible position in the grid to be item 0
+ mFirstPosition = 0;
+ mRestoreOffset = 0;
+ }
+
+ /**
+ * Scroll the list so the first visible position in the grid is the first item in the adapter.
+ */
+ public void setSelectionToTop() {
+ // Clear out the views (but don't clear out the layout records or recycler because the data
+ // has not changed)
+ removeAllViews();
+
+ // Reset to top of grid
+ resetStateForGridTop();
+
+ // Start populating again
+ populate();
+ }
+
+ @Override
+ protected LayoutParams generateDefaultLayoutParams() {
+ return new LayoutParams(LayoutParams.WRAP_CONTENT);
+ }
+
+ @Override
+ protected LayoutParams generateLayoutParams(ViewGroup.LayoutParams lp) {
+ return new LayoutParams(lp);
+ }
+
+ @Override
+ protected boolean checkLayoutParams(ViewGroup.LayoutParams lp) {
+ return lp instanceof LayoutParams;
+ }
+
+ @Override
+ public ViewGroup.LayoutParams generateLayoutParams(AttributeSet attrs) {
+ return new LayoutParams(getContext(), attrs);
+ }
+
+ @Override
+ public Parcelable onSaveInstanceState() {
+ final Parcelable superState = super.onSaveInstanceState();
+ final SavedState ss = new SavedState(superState);
+ final int position = mFirstPosition;
+ ss.position = position;
+ if (position >= 0 && mAdapter != null && position < mAdapter.getCount()) {
+ ss.firstId = mAdapter.getItemId(position);
+ }
+ if (getChildCount() > 0) {
+ ss.topOffset = getChildAt(0).getTop() - mItemMargin - getPaddingTop();
+ }
+ return ss;
+ }
+
+ @Override
+ public void onRestoreInstanceState(Parcelable state) {
+ SavedState ss = (SavedState) state;
+ super.onRestoreInstanceState(ss.getSuperState());
+ mDataChanged = true;
+ mFirstPosition = ss.position;
+ mRestoreOffset = ss.topOffset;
+ requestLayout();
+ }
+
+ public static class LayoutParams extends ViewGroup.LayoutParams {
+ private static final int[] LAYOUT_ATTRS = new int[] {
+ android.R.attr.layout_span
+ };
+
+ private static final int SPAN_INDEX = 0;
+
+ /**
+ * The number of columns this item should span
+ */
+ public int span = 1;
+
+ /**
+ * Item position this view represents
+ */
+ int position;
+
+ /**
+ * Type of this view as reported by the adapter
+ */
+ int viewType;
+
+ /**
+ * The column this view is occupying
+ */
+ int column;
+
+ /**
+ * The stable ID of the item this view displays
+ */
+ long id = -1;
+
+ public LayoutParams(int height) {
+ super(FILL_PARENT, height);
+
+ if (this.height == FILL_PARENT) {
+ Log.w(TAG, "Constructing LayoutParams with height FILL_PARENT - " +
+ "impossible! Falling back to WRAP_CONTENT");
+ this.height = WRAP_CONTENT;
+ }
+ }
+
+ public LayoutParams(Context c, AttributeSet attrs) {
+ super(c, attrs);
+
+ if (this.width != FILL_PARENT) {
+ Log.w(TAG, "Inflation setting LayoutParams width to " + this.width +
+ " - must be MATCH_PARENT");
+ this.width = FILL_PARENT;
+ }
+ if (this.height == FILL_PARENT) {
+ Log.w(TAG, "Inflation setting LayoutParams height to MATCH_PARENT - " +
+ "impossible! Falling back to WRAP_CONTENT");
+ this.height = WRAP_CONTENT;
+ }
+
+ TypedArray a = c.obtainStyledAttributes(attrs, LAYOUT_ATTRS);
+ span = a.getInteger(SPAN_INDEX, 1);
+ a.recycle();
+ }
+
+ public LayoutParams(ViewGroup.LayoutParams other) {
+ super(other);
+
+ if (this.width != FILL_PARENT) {
+ Log.w(TAG, "Constructing LayoutParams with width " + this.width +
+ " - must be MATCH_PARENT");
+ this.width = FILL_PARENT;
+ }
+ if (this.height == FILL_PARENT) {
+ Log.w(TAG, "Constructing LayoutParams with height MATCH_PARENT - " +
+ "impossible! Falling back to WRAP_CONTENT");
+ this.height = WRAP_CONTENT;
+ }
+ }
+ }
+
+ private class RecycleBin {
+ private ArrayList<View>[] mScrapViews;
+ private int mViewTypeCount;
+ private int mMaxScrap;
+
+ private SparseArray<View> mTransientStateViews;
+
+ public void setViewTypeCount(int viewTypeCount) {
+ if (viewTypeCount < 1) {
+ throw new IllegalArgumentException("Must have at least one view type (" +
+ viewTypeCount + " types reported)");
+ }
+ if (viewTypeCount == mViewTypeCount) {
+ return;
+ }
+
+ ArrayList<View>[] scrapViews = new ArrayList[viewTypeCount];
+ for (int i = 0; i < viewTypeCount; i++) {
+ scrapViews[i] = new ArrayList<View>();
+ }
+ mViewTypeCount = viewTypeCount;
+ mScrapViews = scrapViews;
+ }
+
+ public void clear() {
+ final int typeCount = mViewTypeCount;
+ for (int i = 0; i < typeCount; i++) {
+ mScrapViews[i].clear();
+ }
+ if (mTransientStateViews != null) {
+ mTransientStateViews.clear();
+ }
+ }
+
+ public void clearTransientViews() {
+ if (mTransientStateViews != null) {
+ mTransientStateViews.clear();
+ }
+ }
+
+ public void addScrap(View v) {
+ final LayoutParams lp = (LayoutParams) v.getLayoutParams();
+ if (ViewCompat.hasTransientState(v)) {
+ if (mTransientStateViews == null) {
+ mTransientStateViews = new SparseArray<View>();
+ }
+ mTransientStateViews.put(lp.position, v);
+ return;
+ }
+
+ final int childCount = getChildCount();
+ if (childCount > mMaxScrap) {
+ mMaxScrap = childCount;
+ }
+
+ ArrayList<View> scrap = mScrapViews[lp.viewType];
+ if (scrap.size() < mMaxScrap) {
+ scrap.add(v);
+ }
+ }
+
+ public View getTransientStateView(int position) {
+ if (mTransientStateViews == null) {
+ return null;
+ }
+
+ final View result = mTransientStateViews.get(position);
+ if (result != null) {
+ mTransientStateViews.remove(position);
+ }
+ return result;
+ }
+
+ public View getScrapView(int type) {
+ ArrayList<View> scrap = mScrapViews[type];
+ if (scrap.isEmpty()) {
+ return null;
+ }
+
+ final int index = scrap.size() - 1;
+ final View result = scrap.get(index);
+ scrap.remove(index);
+ return result;
+ }
+ }
+
+ private class AdapterDataSetObserver extends DataSetObserver {
+ @Override
+ public void onChanged() {
+ mDataChanged = true;
+ mOldItemCount = mItemCount;
+ mItemCount = mAdapter.getCount();
+
+ // TODO: Consider matching these back up if we have stable IDs.
+ mRecycler.clearTransientViews();
+
+ if (!mHasStableIds) {
+ // Clear all layout records and recycle the views
+ mLayoutRecords.clear();
+ recycleAllViews();
+
+ // Reset item bottoms to be equal to item tops
+ final int colCount = mColCount;
+ for (int i = 0; i < colCount; i++) {
+ mItemBottoms[i] = mItemTops[i];
+ }
+ }
+
+ // TODO: consider repopulating in a deferred runnable instead
+ // (so that successive changes may still be batched)
+ requestLayout();
+ }
+
+ @Override
+ public void onInvalidated() {
+ }
+ }
+
+ static class SavedState extends BaseSavedState {
+ long firstId = -1;
+ int position;
+ int topOffset;
+
+ SavedState(Parcelable superState) {
+ super(superState);
+ }
+
+ private SavedState(Parcel in) {
+ super(in);
+ firstId = in.readLong();
+ position = in.readInt();
+ topOffset = in.readInt();
+ }
+
+ @Override
+ public void writeToParcel(Parcel out, int flags) {
+ super.writeToParcel(out, flags);
+ out.writeLong(firstId);
+ out.writeInt(position);
+ out.writeInt(topOffset);
+ }
+
+ @Override
+ public String toString() {
+ return "StaggereGridView.SavedState{"
+ + Integer.toHexString(System.identityHashCode(this))
+ + " firstId=" + firstId
+ + " position=" + position + "}";
+ }
+
+ public static final Parcelable.Creator<SavedState> CREATOR
+ = new Parcelable.Creator<SavedState>() {
+ public SavedState createFromParcel(Parcel in) {
+ return new SavedState(in);
+ }
+
+ public SavedState[] newArray(int size) {
+ return new SavedState[size];
+ }
+ };
+ }
+}