/* * Copyright (c) 2013 Etsy * * 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.marshalchen.common.uimodule.staggeredgridview; import android.content.Context; import android.content.res.Configuration; import android.content.res.TypedArray; import android.os.Parcel; import android.os.Parcelable; import android.util.AttributeSet; import android.util.Log; import android.util.SparseArray; import android.view.View; import android.view.ViewGroup; import com.marshalchen.common.uimodule.R; import java.util.Arrays; /** * A staggered grid view which supports multiple columns with rows of varying sizes. * <p/> * Builds multiple columns on top of {@link ExtendableListView} * <p/> * Partly inspired by - https://github.com/huewu/PinterestLikeAdapterView */ public class StaggeredGridView extends ExtendableListView { private static final String TAG = "StaggeredGridView"; private static final boolean DBG = false; private static final int DEFAULT_COLUMNS_PORTRAIT = 2; private static final int DEFAULT_COLUMNS_LANDSCAPE = 3; private int mColumnCount; private int mItemMargin; private int mColumnWidth; private boolean mNeedSync; private int mColumnCountPortrait = DEFAULT_COLUMNS_PORTRAIT; private int mColumnCountLandscape = DEFAULT_COLUMNS_LANDSCAPE; /** * A key-value collection where the key is the position and the * {@link com.marshalchen.common.uimodule.staggeredgridview.StaggeredGridView.GridItemRecord} with some info about that position * so we can maintain it's position - and reorg on orientation change. */ private SparseArray<GridItemRecord> mPositionData; private int mGridPaddingLeft; private int mGridPaddingRight; private int mGridPaddingTop; private int mGridPaddingBottom; /*** * Our grid item state record with {@link android.os.Parcelable} implementation * so we can persist them across the SGV lifecycle. */ static class GridItemRecord implements Parcelable { int column; double heightRatio; boolean isHeaderFooter; GridItemRecord() { } /** * Constructor called from {@link #CREATOR} */ private GridItemRecord(Parcel in) { column = in.readInt(); heightRatio = in.readDouble(); isHeaderFooter = in.readByte() == 1; } @Override public int describeContents() { return 0; } @Override public void writeToParcel(Parcel out, int flags) { out.writeInt(column); out.writeDouble(heightRatio); out.writeByte((byte) (isHeaderFooter ? 1 : 0)); } @Override public String toString() { return "GridItemRecord.ListSavedState{" + Integer.toHexString(System.identityHashCode(this)) + " column:" + column + " heightRatio:" + heightRatio + " isHeaderFooter:" + isHeaderFooter + "}"; } public static final Creator<GridItemRecord> CREATOR = new Creator<GridItemRecord>() { public GridItemRecord createFromParcel(Parcel in) { return new GridItemRecord(in); } public GridItemRecord[] newArray(int size) { return new GridItemRecord[size]; } }; } /** * The location of the top of each top item added in each column. */ private int[] mColumnTops; /** * The location of the bottom of each bottom item added in each column. */ private int[] mColumnBottoms; /** * The left location to put items for each column */ private int[] mColumnLefts; /*** * Tells us the distance we've offset from the top. * Can be slightly off on orientation change - TESTING */ private int mDistanceToTop; public StaggeredGridView(final Context context) { this(context, null); } public StaggeredGridView(final Context context, final AttributeSet attrs) { this(context, attrs, 0); } public StaggeredGridView(final Context context, final AttributeSet attrs, final int defStyle) { super(context, attrs, defStyle); if (attrs != null) { // get the number of columns in portrait and landscape TypedArray typedArray = context.obtainStyledAttributes(attrs, R.styleable.StaggeredGridView, defStyle, 0); mColumnCount = typedArray.getInteger( R.styleable.StaggeredGridView_column_count, 0); if (mColumnCount > 0) { mColumnCountPortrait = mColumnCount; mColumnCountLandscape = mColumnCount; } else { mColumnCountPortrait = typedArray.getInteger( R.styleable.StaggeredGridView_column_count_portrait, DEFAULT_COLUMNS_PORTRAIT); mColumnCountLandscape = typedArray.getInteger( R.styleable.StaggeredGridView_column_count_landscape, DEFAULT_COLUMNS_LANDSCAPE); } mItemMargin = typedArray.getDimensionPixelSize( R.styleable.StaggeredGridView_item_margin, 0); mGridPaddingLeft = typedArray.getDimensionPixelSize( R.styleable.StaggeredGridView_grid_paddingLeft, 0); mGridPaddingRight = typedArray.getDimensionPixelSize( R.styleable.StaggeredGridView_grid_paddingRight, 0); mGridPaddingTop = typedArray.getDimensionPixelSize( R.styleable.StaggeredGridView_grid_paddingTop, 0); mGridPaddingBottom = typedArray.getDimensionPixelSize( R.styleable.StaggeredGridView_grid_paddingBottom, 0); typedArray.recycle(); } mColumnCount = 0; // determined onMeasure // Creating these empty arrays to avoid saving null states mColumnTops = new int[0]; mColumnBottoms = new int[0]; mColumnLefts = new int[0]; mPositionData = new SparseArray<GridItemRecord>(); } // ////////////////////////////////////////////////////////////////////////////////////////// // PROPERTIES // // Grid padding is applied to the list item rows but not the header and footer public int getRowPaddingLeft() { return getListPaddingLeft() + mGridPaddingLeft; } public int getRowPaddingRight() { return getListPaddingRight() + mGridPaddingRight; } public int getRowPaddingTop() { return getListPaddingTop() + mGridPaddingTop; } public int getRowPaddingBottom() { return getListPaddingBottom() + mGridPaddingBottom; } public void setGridPadding(int left, int top, int right, int bottom) { mGridPaddingLeft = left; mGridPaddingTop = top; mGridPaddingRight = right; mGridPaddingBottom = bottom; } public void setColumnCountPortrait(int columnCountPortrait) { mColumnCountPortrait = columnCountPortrait; onSizeChanged(getWidth(), getHeight()); requestLayoutChildren(); } public void setColumnCountLandscape(int columnCountLandscape) { mColumnCountLandscape = columnCountLandscape; onSizeChanged(getWidth(), getHeight()); requestLayoutChildren(); } public void setColumnCount(int columnCount) { mColumnCountPortrait = columnCount; mColumnCountLandscape = columnCount; // mColumnCount set onSizeChanged(); onSizeChanged(getWidth(), getHeight()); requestLayoutChildren(); } // ////////////////////////////////////////////////////////////////////////////////////////// // MEASUREMENT // private boolean isLandscape() { return getResources().getConfiguration().orientation == Configuration.ORIENTATION_LANDSCAPE; } @Override protected void onMeasure(final int widthMeasureSpec, final int heightMeasureSpec) { super.onMeasure(widthMeasureSpec, heightMeasureSpec); if (mColumnCount <= 0) { boolean isLandscape = isLandscape(); mColumnCount = isLandscape ? mColumnCountLandscape : mColumnCountPortrait; } // our column width is the width of the listview // minus it's padding // minus the total items margin // divided by the number of columns mColumnWidth = calculateColumnWidth(getMeasuredWidth()); if (mColumnTops == null || mColumnTops.length != mColumnCount) { mColumnTops = new int[mColumnCount]; initColumnTops(); } if (mColumnBottoms == null || mColumnBottoms.length != mColumnCount) { mColumnBottoms = new int[mColumnCount]; initColumnBottoms(); } if (mColumnLefts == null || mColumnLefts.length != mColumnCount) { mColumnLefts = new int[mColumnCount]; initColumnLefts(); } } @Override protected void onMeasureChild(final View child, final LayoutParams layoutParams) { final int viewType = layoutParams.viewType; final int position = layoutParams.position; if (viewType == ITEM_VIEW_TYPE_HEADER_OR_FOOTER || viewType == ITEM_VIEW_TYPE_IGNORE) { // for headers and weird ignored views super.onMeasureChild(child, layoutParams); } else { if (DBG) Log.d(TAG, "onMeasureChild BEFORE position:" + position + " h:" + getMeasuredHeight()); // measure it to the width of our column. int childWidthSpec = MeasureSpec.makeMeasureSpec(mColumnWidth, MeasureSpec.EXACTLY); int childHeightSpec; if (layoutParams.height > 0) { childHeightSpec = MeasureSpec.makeMeasureSpec(layoutParams.height, MeasureSpec.EXACTLY); } else { childHeightSpec = MeasureSpec.makeMeasureSpec(LayoutParams.WRAP_CONTENT, MeasureSpec.UNSPECIFIED); } child.measure(childWidthSpec, childHeightSpec); } final int childHeight = getChildHeight(child); setPositionHeightRatio(position, childHeight); if (DBG) Log.d(TAG, "onMeasureChild AFTER position:" + position + " h:" + childHeight); } public int getColumnWidth() { return mColumnWidth; } public void resetToTop() { if (mColumnCount > 0) { if (mColumnTops == null) { mColumnTops = new int[mColumnCount]; } if (mColumnBottoms == null) { mColumnBottoms = new int[mColumnCount]; } initColumnTopsAndBottoms(); mPositionData.clear(); mNeedSync = false; mDistanceToTop = 0; setSelection(0); } } // ////////////////////////////////////////////////////////////////////////////////////////// // POSITIONING // @Override protected void onChildCreated(final int position, final boolean flowDown) { super.onChildCreated(position, flowDown); if (!isHeaderOrFooter(position)) { // do we already have a column for this position? final int column = getChildColumn(position, flowDown); setPositionColumn(position, column); if (DBG) Log.d(TAG, "onChildCreated position:" + position + " is in column:" + column); } else { setPositionIsHeaderFooter(position); } } private void requestLayoutChildren() { final int count = getChildCount(); for (int i = 0; i < count; i++) { final View v = getChildAt(i); if (v != null) v.requestLayout(); } } @Override protected void layoutChildren() { preLayoutChildren(); super.layoutChildren(); } private void preLayoutChildren() { // on a major re-layout reset for our next layout pass if (!mNeedSync) { Arrays.fill(mColumnBottoms, 0); } else { mNeedSync = false; } // copy the tops into the bottom // since we're going to redo a layout pass that will draw down from // the top System.arraycopy(mColumnTops, 0, mColumnBottoms, 0, mColumnCount); } // NOTE : Views will either be layout out via onLayoutChild // OR // Views will be offset if they are active but offscreen so that we can recycle! // Both onLayoutChild() and onOffsetChild are called after we measure our view // see ExtensibleListView.setupChild(); @Override protected void onLayoutChild(final View child, final int position, final boolean flowDown, final int childrenLeft, final int childTop, final int childRight, final int childBottom) { if (isHeaderOrFooter(position)) { layoutGridHeaderFooter(child, position, flowDown, childrenLeft, childTop, childRight, childBottom); } else { layoutGridChild(child, position, flowDown, childrenLeft, childRight); } } private void layoutGridHeaderFooter(final View child, final int position, final boolean flowDown, final int childrenLeft, final int childTop, final int childRight, final int childBottom) { // offset the top and bottom of all our columns // if it's the footer we want it below the lowest child bottom int gridChildTop; int gridChildBottom; if (flowDown) { gridChildTop = getLowestPositionedBottom(); gridChildBottom = gridChildTop + getChildHeight(child); } else { gridChildBottom = getHighestPositionedTop(); gridChildTop = gridChildBottom - getChildHeight(child); } for (int i = 0; i < mColumnCount; i++) { updateColumnTopIfNeeded(i, gridChildTop); updateColumnBottomIfNeeded(i, gridChildBottom); } super.onLayoutChild(child, position, flowDown, childrenLeft, gridChildTop, childRight, gridChildBottom); } private void layoutGridChild(final View child, final int position, final boolean flowDown, final int childrenLeft, final int childRight) { // stash the bottom and the top if it's higher positioned int column = getPositionColumn(position); int gridChildTop; int gridChildBottom; int childTopMargin = getChildTopMargin(position); int childBottomMargin = getChildBottomMargin(); int verticalMargins = childTopMargin + childBottomMargin; if (flowDown) { gridChildTop = mColumnBottoms[column]; // the next items top is the last items bottom gridChildBottom = gridChildTop + (getChildHeight(child) + verticalMargins); } else { gridChildBottom = mColumnTops[column]; // the bottom of the next column up is our top gridChildTop = gridChildBottom - (getChildHeight(child) + verticalMargins); } if (DBG) Log.d(TAG, "onLayoutChild position:" + position + " column:" + column + " gridChildTop:" + gridChildTop + " gridChildBottom:" + gridChildBottom); // we also know the column of this view so let's stash it in the // view's layout params GridLayoutParams layoutParams = (GridLayoutParams) child.getLayoutParams(); layoutParams.column = column; updateColumnBottomIfNeeded(column, gridChildBottom); updateColumnTopIfNeeded(column, gridChildTop); // subtract the margins before layout gridChildTop += childTopMargin; gridChildBottom -= childBottomMargin; child.layout(childrenLeft, gridChildTop, childRight, gridChildBottom); } @Override protected void onOffsetChild(final View child, final int position, final boolean flowDown, final int childrenLeft, final int childTop) { // if the child is recycled and is just offset // we still want to add its deets into our store if (isHeaderOrFooter(position)) { offsetGridHeaderFooter(child, position, flowDown, childrenLeft, childTop); } else { offsetGridChild(child, position, flowDown, childrenLeft, childTop); } } private void offsetGridHeaderFooter(final View child, final int position, final boolean flowDown, final int childrenLeft, final int childTop) { // offset the top and bottom of all our columns // if it's the footer we want it below the lowest child bottom int gridChildTop; int gridChildBottom; if (flowDown) { gridChildTop = getLowestPositionedBottom(); gridChildBottom = gridChildTop + getChildHeight(child); } else { gridChildBottom = getHighestPositionedTop(); gridChildTop = gridChildBottom - getChildHeight(child); } for (int i = 0; i < mColumnCount; i++) { updateColumnTopIfNeeded(i, gridChildTop); updateColumnBottomIfNeeded(i, gridChildBottom); } super.onOffsetChild(child, position, flowDown, childrenLeft, gridChildTop); } private void offsetGridChild(final View child, final int position, final boolean flowDown, final int childrenLeft, final int childTop) { // stash the bottom and the top if it's higher positioned int column = getPositionColumn(position); int gridChildTop; int gridChildBottom; int childTopMargin = getChildTopMargin(position); int childBottomMargin = getChildBottomMargin(); int verticalMargins = childTopMargin + childBottomMargin; if (flowDown) { gridChildTop = mColumnBottoms[column]; // the next items top is the last items bottom gridChildBottom = gridChildTop + (getChildHeight(child) + verticalMargins); } else { gridChildBottom = mColumnTops[column]; // the bottom of the next column up is our top gridChildTop = gridChildBottom - (getChildHeight(child) + verticalMargins); } if (DBG) Log.d(TAG, "onOffsetChild position:" + position + " column:" + column + " childTop:" + childTop + " gridChildTop:" + gridChildTop + " gridChildBottom:" + gridChildBottom); // we also know the column of this view so let's stash it in the // view's layout params GridLayoutParams layoutParams = (GridLayoutParams) child.getLayoutParams(); layoutParams.column = column; updateColumnBottomIfNeeded(column, gridChildBottom); updateColumnTopIfNeeded(column, gridChildTop); super.onOffsetChild(child, position, flowDown, childrenLeft, gridChildTop + childTopMargin); } private int getChildHeight(final View child) { return child.getMeasuredHeight(); } private int getChildTopMargin(final int position) { boolean isFirstRow = position < (getHeaderViewsCount() + mColumnCount); return isFirstRow ? mItemMargin : 0; } private int getChildBottomMargin() { return mItemMargin; } @Override protected LayoutParams generateChildLayoutParams(final View child) { GridLayoutParams layoutParams = null; final ViewGroup.LayoutParams childParams = child.getLayoutParams(); if (childParams != null) { if (childParams instanceof GridLayoutParams) { layoutParams = (GridLayoutParams) childParams; } else { layoutParams = new GridLayoutParams(childParams); } } if (layoutParams == null) { layoutParams = new GridLayoutParams( mColumnWidth, ViewGroup.LayoutParams.WRAP_CONTENT); } return layoutParams; } private void updateColumnTopIfNeeded(int column, int childTop) { if (childTop < mColumnTops[column]) { mColumnTops[column] = childTop; } } private void updateColumnBottomIfNeeded(int column, int childBottom) { if (childBottom > mColumnBottoms[column]) { mColumnBottoms[column] = childBottom; } } @Override protected int getChildLeft(final int position) { if (isHeaderOrFooter(position)) { return super.getChildLeft(position); } else { final int column = getPositionColumn(position); return mColumnLefts[column]; } } @Override protected int getChildTop(final int position) { if (isHeaderOrFooter(position)) { return super.getChildTop(position); } else { final int column = getPositionColumn(position); if (column == -1) { return getHighestPositionedBottom(); } return mColumnBottoms[column]; } } /** * Get the top for the next child down in our view * (maybe a column across) so we can fill down. */ @Override protected int getNextChildDownsTop(final int position) { if (isHeaderOrFooter(position)) { return super.getNextChildDownsTop(position); } else { return getHighestPositionedBottom(); } } @Override protected int getChildBottom(final int position) { if (isHeaderOrFooter(position)) { return super.getChildBottom(position); } else { final int column = getPositionColumn(position); if (column == -1) { return getLowestPositionedTop(); } return mColumnTops[column]; } } /** * Get the bottom for the next child up in our view * (maybe a column across) so we can fill up. */ @Override protected int getNextChildUpsBottom(final int position) { if (isHeaderOrFooter(position)) { return super.getNextChildUpsBottom(position); } else { return getLowestPositionedTop(); } } @Override protected int getLastChildBottom() { final int lastPosition = mFirstPosition + (getChildCount() - 1); if (isHeaderOrFooter(lastPosition)) { return super.getLastChildBottom(); } return getHighestPositionedBottom(); } @Override protected int getFirstChildTop() { if (isHeaderOrFooter(mFirstPosition)) { return super.getFirstChildTop(); } return getLowestPositionedTop(); } @Override protected int getHighestChildTop() { if (isHeaderOrFooter(mFirstPosition)) { return super.getHighestChildTop(); } return getHighestPositionedTop(); } @Override protected int getLowestChildBottom() { final int lastPosition = mFirstPosition + (getChildCount() - 1); if (isHeaderOrFooter(lastPosition)) { return super.getLowestChildBottom(); } return getLowestPositionedBottom(); } @Override protected void offsetChildrenTopAndBottom(final int offset) { super.offsetChildrenTopAndBottom(offset); offsetAllColumnsTopAndBottom(offset); offsetDistanceToTop(offset); } protected void offsetChildrenTopAndBottom(final int offset, final int column) { if (DBG) Log.d(TAG, "offsetChildrenTopAndBottom: " + offset + " column:" + column); final int count = getChildCount(); for (int i = 0; i < count; i++) { final View v = getChildAt(i); if (v != null && v.getLayoutParams() != null && v.getLayoutParams() instanceof GridLayoutParams) { GridLayoutParams lp = (GridLayoutParams) v.getLayoutParams(); if (lp.column == column) { v.offsetTopAndBottom(offset); } } } offsetColumnTopAndBottom(offset, column); } private void offsetDistanceToTop(final int offset) { mDistanceToTop += offset; if (DBG) Log.d(TAG, "offset mDistanceToTop:" + mDistanceToTop); } public int getDistanceToTop() { return mDistanceToTop; } private void offsetAllColumnsTopAndBottom(final int offset) { if (offset != 0) { for (int i = 0; i < mColumnCount; i++) { offsetColumnTopAndBottom(offset, i); } } } private void offsetColumnTopAndBottom(final int offset, final int column) { if (offset != 0) { mColumnTops[column] += offset; mColumnBottoms[column] += offset; } } @Override protected void adjustViewsAfterFillGap(final boolean down) { super.adjustViewsAfterFillGap(down); // fix vertical gaps when hitting the top after a rotate // only when scrolling back up! if (!down) { alignTops(); } } private void alignTops() { if (mFirstPosition == getHeaderViewsCount()) { // we're showing all the views before the header views int[] nonHeaderTops = getHighestNonHeaderTops(); // we should now have our non header tops // align them boolean isAligned = true; int highestColumn = -1; int highestTop = Integer.MAX_VALUE; for (int i = 0; i < nonHeaderTops.length; i++) { // are they all aligned if (isAligned && i > 0 && nonHeaderTops[i] != highestTop) { isAligned = false; // not all the tops are aligned } // what's the highest if (nonHeaderTops[i] < highestTop) { highestTop = nonHeaderTops[i]; highestColumn = i; } } // skip the rest. if (isAligned) return; // we've got the highest column - lets align the others for (int i = 0; i < nonHeaderTops.length; i++) { if (i != highestColumn) { // there's a gap in this column int offset = highestTop - nonHeaderTops[i]; offsetChildrenTopAndBottom(offset, i); } } invalidate(); } } private int[] getHighestNonHeaderTops() { int[] nonHeaderTops = new int[mColumnCount]; int childCount = getChildCount(); if (childCount > 0) { for (int i = 0; i < childCount; i++) { View child = getChildAt(i); if (child != null && child.getLayoutParams() != null && child.getLayoutParams() instanceof GridLayoutParams) { // is this child's top the highest non GridLayoutParams lp = (GridLayoutParams) child.getLayoutParams(); // is it a child that isn't a header if (lp.viewType != ITEM_VIEW_TYPE_HEADER_OR_FOOTER && child.getTop() < nonHeaderTops[lp.column]) { nonHeaderTops[lp.column] = child.getTop(); } } } } return nonHeaderTops; } @Override protected void onChildrenDetached(final int start, final int count) { super.onChildrenDetached(start, count); // go through our remaining views and sync the top and bottom stash. // Repair the top and bottom column boundaries from the views we still have Arrays.fill(mColumnTops, Integer.MAX_VALUE); Arrays.fill(mColumnBottoms, 0); for (int i = 0; i < getChildCount(); i++) { final View child = getChildAt(i); if (child != null) { final LayoutParams childParams = (LayoutParams) child.getLayoutParams(); if (childParams.viewType != ITEM_VIEW_TYPE_HEADER_OR_FOOTER && childParams instanceof GridLayoutParams) { GridLayoutParams layoutParams = (GridLayoutParams) childParams; int column = layoutParams.column; int position = layoutParams.position; final int childTop = child.getTop(); if (childTop < mColumnTops[column]) { mColumnTops[column] = childTop - getChildTopMargin(position); } final int childBottom = child.getBottom(); if (childBottom > mColumnBottoms[column]) { mColumnBottoms[column] = childBottom + getChildBottomMargin(); } } else { // the header and footer here final int childTop = child.getTop(); final int childBottom = child.getBottom(); for (int col = 0; col < mColumnCount; col++) { if (childTop < mColumnTops[col]) { mColumnTops[col] = childTop; } if (childBottom > mColumnBottoms[col]) { mColumnBottoms[col] = childBottom; } } } } } } @Override protected boolean hasSpaceUp() { int end = mClipToPadding ? getRowPaddingTop() : 0; return getLowestPositionedTop() > end; } // ////////////////////////////////////////////////////////////////////////////////////////// // SYNCING ACROSS ROTATION // @Override protected void onSizeChanged(final int w, final int h, final int oldw, final int oldh) { super.onSizeChanged(w, h, oldw, oldh); onSizeChanged(w, h); } @Override protected void onSizeChanged(int w, int h) { super.onSizeChanged(w, h); boolean isLandscape = isLandscape(); int newColumnCount = isLandscape ? mColumnCountLandscape : mColumnCountPortrait; if (mColumnCount != newColumnCount) { mColumnCount = newColumnCount; mColumnWidth = calculateColumnWidth(w); mColumnTops = new int[mColumnCount]; mColumnBottoms = new int[mColumnCount]; mColumnLefts = new int[mColumnCount]; mDistanceToTop = 0; // rebuild the columns initColumnTopsAndBottoms(); initColumnLefts(); // if we have data if (getCount() > 0 && mPositionData.size() > 0) { onColumnSync(); } requestLayout(); } } private int calculateColumnWidth(final int gridWidth) { final int listPadding = getRowPaddingLeft() + getRowPaddingRight(); return (gridWidth - listPadding - mItemMargin * (mColumnCount + 1)) / mColumnCount; } private int calculateColumnLeft(final int colIndex) { return getRowPaddingLeft() + mItemMargin + ((mItemMargin + mColumnWidth) * colIndex); } /*** * Our mColumnTops and mColumnBottoms need to be re-built up to the * mSyncPosition - the following layout request will then * layout the that position and then fillUp and fillDown appropriately. */ private void onColumnSync() { // re-calc tops for new column count! int syncPosition = Math.min(mSyncPosition, getCount() - 1); SparseArray<Double> positionHeightRatios = new SparseArray<Double>(syncPosition); for (int pos = 0; pos < syncPosition; pos++) { // check for weirdness final GridItemRecord rec = mPositionData.get(pos); if (rec == null) break; Log.d(TAG, "onColumnSync:" + pos + " ratio:" + rec.heightRatio); positionHeightRatios.append(pos, rec.heightRatio); } mPositionData.clear(); // re-calc our relative position while at the same time // rebuilding our GridItemRecord collection if (DBG) Log.d(TAG, "onColumnSync column width:" + mColumnWidth); for (int pos = 0; pos < syncPosition; pos++) { //Check for weirdness again final Double heightRatio = positionHeightRatios.get(pos); if(heightRatio == null){ break; } final GridItemRecord rec = getOrCreateRecord(pos); final int height = (int) (mColumnWidth * heightRatio); rec.heightRatio = heightRatio; int top; int bottom; // check for headers if (isHeaderOrFooter(pos)) { // the next top is the bottom for that column top = getLowestPositionedBottom(); bottom = top + height; for (int i = 0; i < mColumnCount; i++) { mColumnTops[i] = top; mColumnBottoms[i] = bottom; } } else { // what's the next column down ? final int column = getHighestPositionedBottomColumn(); // the next top is the bottom for that column top = mColumnBottoms[column]; bottom = top + height + getChildTopMargin(pos) + getChildBottomMargin(); mColumnTops[column] = top; mColumnBottoms[column] = bottom; rec.column = column; } if (DBG) Log.d(TAG, "onColumnSync position:" + pos + " top:" + top + " bottom:" + bottom + " height:" + height + " heightRatio:" + heightRatio); } // our sync position will be displayed in this column final int syncColumn = getHighestPositionedBottomColumn(); setPositionColumn(syncPosition, syncColumn); // we want to offset from height of the sync position // minus the offset int syncToBottom = mColumnBottoms[syncColumn]; int offset = -syncToBottom + mSpecificTop; // offset all columns by offsetAllColumnsTopAndBottom(offset); // sync the distance to top mDistanceToTop = -syncToBottom; // stash our bottoms in our tops - though these will be copied back to the bottoms System.arraycopy(mColumnBottoms, 0, mColumnTops, 0, mColumnCount); } // ////////////////////////////////////////////////////////////////////////////////////////// // GridItemRecord UTILS // private void setPositionColumn(final int position, final int column) { GridItemRecord rec = getOrCreateRecord(position); rec.column = column; } private void setPositionHeightRatio(final int position, final int height) { GridItemRecord rec = getOrCreateRecord(position); rec.heightRatio = (double) height / (double) mColumnWidth; if (DBG) Log.d(TAG, "position:" + position + " width:" + mColumnWidth + " height:" + height + " heightRatio:" + rec.heightRatio); } private void setPositionIsHeaderFooter(final int position) { GridItemRecord rec = getOrCreateRecord(position); rec.isHeaderFooter = true; } private GridItemRecord getOrCreateRecord(final int position) { GridItemRecord rec = mPositionData.get(position, null); if (rec == null) { rec = new GridItemRecord(); mPositionData.append(position, rec); } return rec; } private int getPositionColumn(final int position) { GridItemRecord rec = mPositionData.get(position, null); return rec != null ? rec.column : -1; } // ////////////////////////////////////////////////////////////////////////////////////////// // HELPERS // private boolean isHeaderOrFooter(final int position) { final int viewType = mAdapter.getItemViewType(position); return viewType == ITEM_VIEW_TYPE_HEADER_OR_FOOTER; } private int getChildColumn(final int position, final boolean flowDown) { // do we already have a column for this child position? int column = getPositionColumn(position); // we don't have the column or it no longer fits in our grid final int columnCount = mColumnCount; if (column < 0 || column >= columnCount) { // if we're going down - // get the highest positioned (lowest value) // column bottom if (flowDown) { column = getHighestPositionedBottomColumn(); } else { column = getLowestPositionedTopColumn(); } } return column; } private void initColumnTopsAndBottoms() { initColumnTops(); initColumnBottoms(); } private void initColumnTops() { Arrays.fill(mColumnTops, getPaddingTop() + mGridPaddingTop); } private void initColumnBottoms() { Arrays.fill(mColumnBottoms, getPaddingTop() + mGridPaddingTop); } private void initColumnLefts() { for (int i = 0; i < mColumnCount; i++) { mColumnLefts[i] = calculateColumnLeft(i); } } // ////////////////////////////////////////////////////////////////////////////////////////// // BOTTOM // private int getHighestPositionedBottom() { final int column = getHighestPositionedBottomColumn(); return mColumnBottoms[column]; } private int getHighestPositionedBottomColumn() { int columnFound = 0; int highestPositionedBottom = Integer.MAX_VALUE; // the highest positioned bottom is the one with the lowest value :D for (int i = 0; i < mColumnCount; i++) { int bottom = mColumnBottoms[i]; if (bottom < highestPositionedBottom) { highestPositionedBottom = bottom; columnFound = i; } } return columnFound; } private int getLowestPositionedBottom() { final int column = getLowestPositionedBottomColumn(); return mColumnBottoms[column]; } private int getLowestPositionedBottomColumn() { int columnFound = 0; int lowestPositionedBottom = Integer.MIN_VALUE; // the lowest positioned bottom is the one with the highest value :D for (int i = 0; i < mColumnCount; i++) { int bottom = mColumnBottoms[i]; if (bottom > lowestPositionedBottom) { lowestPositionedBottom = bottom; columnFound = i; } } return columnFound; } // ////////////////////////////////////////////////////////////////////////////////////////// // TOP // private int getLowestPositionedTop() { final int column = getLowestPositionedTopColumn(); return mColumnTops[column]; } private int getLowestPositionedTopColumn() { int columnFound = 0; // we'll go backwards through since the right most // will likely be the lowest positioned Top int lowestPositionedTop = Integer.MIN_VALUE; // the lowest positioned top is the one with the highest value :D for (int i = 0; i < mColumnCount; i++) { int top = mColumnTops[i]; if (top > lowestPositionedTop) { lowestPositionedTop = top; columnFound = i; } } return columnFound; } private int getHighestPositionedTop() { final int column = getHighestPositionedTopColumn(); return mColumnTops[column]; } private int getHighestPositionedTopColumn() { int columnFound = 0; int highestPositionedTop = Integer.MAX_VALUE; // the highest positioned top is the one with the lowest value :D for (int i = 0; i < mColumnCount; i++) { int top = mColumnTops[i]; if (top < highestPositionedTop) { highestPositionedTop = top; columnFound = i; } } return columnFound; } // ////////////////////////////////////////////////////////////////////////////////////////// // LAYOUT PARAMS // /** * Extended LayoutParams to column position and anything else we may been for the grid */ public static class GridLayoutParams extends LayoutParams { // The column the view is displayed in int column; public GridLayoutParams(Context c, AttributeSet attrs) { super(c, attrs); enforceStaggeredLayout(); } public GridLayoutParams(int w, int h) { super(w, h); enforceStaggeredLayout(); } public GridLayoutParams(int w, int h, int viewType) { super(w, h); enforceStaggeredLayout(); } public GridLayoutParams(ViewGroup.LayoutParams source) { super(source); enforceStaggeredLayout(); } /** * Here we're making sure that all grid view items * are width MATCH_PARENT and height WRAP_CONTENT. * That's what this grid is designed for */ private void enforceStaggeredLayout() { if (width != MATCH_PARENT) { width = MATCH_PARENT; } if (height == MATCH_PARENT) { height = WRAP_CONTENT; } } } // ////////////////////////////////////////////////////////////////////////////////////////// // SAVED STATE public static class GridListSavedState extends ListSavedState { int columnCount; int[] columnTops; SparseArray positionData; public GridListSavedState(Parcelable superState) { super(superState); } /** * Constructor called from {@link #CREATOR} */ public GridListSavedState(Parcel in) { super(in); columnCount = in.readInt(); columnTops = new int[columnCount >= 0 ? columnCount : 0]; in.readIntArray(columnTops); positionData = in.readSparseArray(GridItemRecord.class.getClassLoader()); } @Override public void writeToParcel(Parcel out, int flags) { super.writeToParcel(out, flags); out.writeInt(columnCount); out.writeIntArray(columnTops); out.writeSparseArray(positionData); } @Override public String toString() { return "StaggeredGridView.GridListSavedState{" + Integer.toHexString(System.identityHashCode(this)) + "}"; } public static final Creator<GridListSavedState> CREATOR = new Creator<GridListSavedState>() { public GridListSavedState createFromParcel(Parcel in) { return new GridListSavedState(in); } public GridListSavedState[] newArray(int size) { return new GridListSavedState[size]; } }; } @Override public Parcelable onSaveInstanceState() { ListSavedState listState = (ListSavedState) super.onSaveInstanceState(); GridListSavedState ss = new GridListSavedState(listState.getSuperState()); // from the list state ss.selectedId = listState.selectedId; ss.firstId = listState.firstId; ss.viewTop = listState.viewTop; ss.position = listState.position; ss.height = listState.height; // our state boolean haveChildren = getChildCount() > 0 && getCount() > 0; if (haveChildren && mFirstPosition > 0) { ss.columnCount = mColumnCount; ss.columnTops = mColumnTops; ss.positionData = mPositionData; } else { ss.columnCount = mColumnCount >= 0 ? mColumnCount : 0; ss.columnTops = new int[ss.columnCount]; ss.positionData = new SparseArray<Object>(); } return ss; } @Override public void onRestoreInstanceState(Parcelable state) { GridListSavedState ss = (GridListSavedState) state; mColumnCount = ss.columnCount; mColumnTops = ss.columnTops; mColumnBottoms = new int[mColumnCount]; mPositionData = ss.positionData; mNeedSync = true; super.onRestoreInstanceState(ss); } }