blob: 6b6b9387247e582cc82d47febf4edd7ce8c24532 [file] [log] [blame]
/*
* 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];
}
};
}
}