() {
82 |
83 | public ClassLoaderSavedState createFromParcel(Parcel in) {
84 | Parcelable superState = in.readParcelable(null);
85 | if (superState != null) {
86 | throw new IllegalStateException("superState must be null");
87 | }
88 | return EMPTY_STATE;
89 | }
90 |
91 | public ClassLoaderSavedState[] newArray(int size) {
92 | return new ClassLoaderSavedState[size];
93 | }
94 | };
95 | }
96 |
--------------------------------------------------------------------------------
/library/src/main/java/com/etsy/android/grid/HeaderViewListAdapter.java:
--------------------------------------------------------------------------------
1 | /*
2 | * Copyright (C) 2006 The Android Open Source Project
3 | *
4 | * Licensed under the Apache License, Version 2.0 (the "License");
5 | * you may not use this file except in compliance with the License.
6 | * You may obtain a copy of the License at
7 | *
8 | * http://www.apache.org/licenses/LICENSE-2.0
9 | *
10 | * Unless required by applicable law or agreed to in writing, software
11 | * distributed under the License is distributed on an "AS IS" BASIS,
12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 | * See the License for the specific language governing permissions and
14 | * limitations under the License.
15 | */
16 |
17 | package com.etsy.android.grid;
18 |
19 | import android.database.DataSetObserver;
20 | import android.view.View;
21 | import android.view.ViewGroup;
22 | import android.widget.AdapterView;
23 | import android.widget.Filter;
24 | import android.widget.Filterable;
25 | import android.widget.ListAdapter;
26 | import android.widget.WrapperListAdapter;
27 |
28 | import java.util.ArrayList;
29 |
30 | /**
31 | * ListAdapter used when a ListView has header views. This ListAdapter
32 | * wraps another one and also keeps track of the header views and their
33 | * associated data objects.
34 | *This is intended as a base class; you will probably not need to
35 | * use this class directly in your own code.
36 | */
37 | public class HeaderViewListAdapter implements WrapperListAdapter, Filterable {
38 |
39 | private final ListAdapter mAdapter;
40 |
41 | // These two ArrayList are assumed to NOT be null.
42 | // They are indeed created when declared in ListView and then shared.
43 | ArrayList mHeaderViewInfos;
44 | ArrayList mFooterViewInfos;
45 |
46 | // Used as a placeholder in case the provided info views are indeed null.
47 | // Currently only used by some CTS tests, which may be removed.
48 | static final ArrayList EMPTY_INFO_LIST =
49 | new ArrayList();
50 |
51 | boolean mAreAllFixedViewsSelectable;
52 |
53 | private final boolean mIsFilterable;
54 |
55 | public HeaderViewListAdapter(ArrayList headerViewInfos,
56 | ArrayList footerViewInfos,
57 | ListAdapter adapter) {
58 | mAdapter = adapter;
59 | mIsFilterable = adapter instanceof Filterable;
60 |
61 | if (headerViewInfos == null) {
62 | mHeaderViewInfos = EMPTY_INFO_LIST;
63 | } else {
64 | mHeaderViewInfos = headerViewInfos;
65 | }
66 |
67 | if (footerViewInfos == null) {
68 | mFooterViewInfos = EMPTY_INFO_LIST;
69 | } else {
70 | mFooterViewInfos = footerViewInfos;
71 | }
72 |
73 | mAreAllFixedViewsSelectable =
74 | areAllListInfosSelectable(mHeaderViewInfos)
75 | && areAllListInfosSelectable(mFooterViewInfos);
76 | }
77 |
78 | public int getHeadersCount() {
79 | return mHeaderViewInfos.size();
80 | }
81 |
82 | public int getFootersCount() {
83 | return mFooterViewInfos.size();
84 | }
85 |
86 | public boolean isEmpty() {
87 | return mAdapter == null || mAdapter.isEmpty();
88 | }
89 |
90 | private boolean areAllListInfosSelectable(ArrayList infos) {
91 | if (infos != null) {
92 | for (StaggeredGridView.FixedViewInfo info : infos) {
93 | if (!info.isSelectable) {
94 | return false;
95 | }
96 | }
97 | }
98 | return true;
99 | }
100 |
101 | public boolean removeHeader(View v) {
102 | for (int i = 0; i < mHeaderViewInfos.size(); i++) {
103 | StaggeredGridView.FixedViewInfo info = mHeaderViewInfos.get(i);
104 | if (info.view == v) {
105 | mHeaderViewInfos.remove(i);
106 |
107 | mAreAllFixedViewsSelectable =
108 | areAllListInfosSelectable(mHeaderViewInfos)
109 | && areAllListInfosSelectable(mFooterViewInfos);
110 |
111 | return true;
112 | }
113 | }
114 |
115 | return false;
116 | }
117 |
118 | public boolean removeFooter(View v) {
119 | for (int i = 0; i < mFooterViewInfos.size(); i++) {
120 | StaggeredGridView.FixedViewInfo info = mFooterViewInfos.get(i);
121 | if (info.view == v) {
122 | mFooterViewInfos.remove(i);
123 |
124 | mAreAllFixedViewsSelectable =
125 | areAllListInfosSelectable(mHeaderViewInfos)
126 | && areAllListInfosSelectable(mFooterViewInfos);
127 |
128 | return true;
129 | }
130 | }
131 |
132 | return false;
133 | }
134 |
135 | public int getCount() {
136 | if (mAdapter != null) {
137 | return getFootersCount() + getHeadersCount() + mAdapter.getCount();
138 | } else {
139 | return getFootersCount() + getHeadersCount();
140 | }
141 | }
142 |
143 | public boolean areAllItemsEnabled() {
144 | if (mAdapter != null) {
145 | return mAreAllFixedViewsSelectable && mAdapter.areAllItemsEnabled();
146 | } else {
147 | return true;
148 | }
149 | }
150 |
151 | public boolean isEnabled(int position) {
152 | // Header (negative positions will throw an ArrayIndexOutOfBoundsException)
153 | int numHeaders = getHeadersCount();
154 | if (position < numHeaders) {
155 | return mHeaderViewInfos.get(position).isSelectable;
156 | }
157 |
158 | // Adapter
159 | final int adjPosition = position - numHeaders;
160 | int adapterCount = 0;
161 | if (mAdapter != null) {
162 | adapterCount = mAdapter.getCount();
163 | if (adjPosition < adapterCount) {
164 | return mAdapter.isEnabled(adjPosition);
165 | }
166 | }
167 |
168 | // Footer (off-limits positions will throw an ArrayIndexOutOfBoundsException)
169 | return mFooterViewInfos.get(adjPosition - adapterCount).isSelectable;
170 | }
171 |
172 | public Object getItem(int position) {
173 | // Header (negative positions will throw an ArrayIndexOutOfBoundsException)
174 | int numHeaders = getHeadersCount();
175 | if (position < numHeaders) {
176 | return mHeaderViewInfos.get(position).data;
177 | }
178 |
179 | // Adapter
180 | final int adjPosition = position - numHeaders;
181 | int adapterCount = 0;
182 | if (mAdapter != null) {
183 | adapterCount = mAdapter.getCount();
184 | if (adjPosition < adapterCount) {
185 | return mAdapter.getItem(adjPosition);
186 | }
187 | }
188 |
189 | // Footer (off-limits positions will throw an ArrayIndexOutOfBoundsException)
190 | return mFooterViewInfos.get(adjPosition - adapterCount).data;
191 | }
192 |
193 | public long getItemId(int position) {
194 | int numHeaders = getHeadersCount();
195 | if (mAdapter != null && position >= numHeaders) {
196 | int adjPosition = position - numHeaders;
197 | int adapterCount = mAdapter.getCount();
198 | if (adjPosition < adapterCount) {
199 | return mAdapter.getItemId(adjPosition);
200 | }
201 | }
202 | return -1;
203 | }
204 |
205 | public boolean hasStableIds() {
206 | if (mAdapter != null) {
207 | return mAdapter.hasStableIds();
208 | }
209 | return false;
210 | }
211 |
212 | public View getView(int position, View convertView, ViewGroup parent) {
213 | // Header (negative positions will throw an ArrayIndexOutOfBoundsException)
214 | int numHeaders = getHeadersCount();
215 | if (position < numHeaders) {
216 | return mHeaderViewInfos.get(position).view;
217 | }
218 |
219 | // Adapter
220 | final int adjPosition = position - numHeaders;
221 | int adapterCount = 0;
222 | if (mAdapter != null) {
223 | adapterCount = mAdapter.getCount();
224 | if (adjPosition < adapterCount) {
225 | return mAdapter.getView(adjPosition, convertView, parent);
226 | }
227 | }
228 |
229 | // Footer (off-limits positions will throw an ArrayIndexOutOfBoundsException)
230 | return mFooterViewInfos.get(adjPosition - adapterCount).view;
231 | }
232 |
233 | public int getItemViewType(int position) {
234 | int numHeaders = getHeadersCount();
235 | if (mAdapter != null && position >= numHeaders) {
236 | int adjPosition = position - numHeaders;
237 | int adapterCount = mAdapter.getCount();
238 | if (adjPosition < adapterCount) {
239 | return mAdapter.getItemViewType(adjPosition);
240 | }
241 | }
242 |
243 | return AdapterView.ITEM_VIEW_TYPE_HEADER_OR_FOOTER;
244 | }
245 |
246 | public int getViewTypeCount() {
247 | if (mAdapter != null) {
248 | return mAdapter.getViewTypeCount();
249 | }
250 | return 1;
251 | }
252 |
253 | public void registerDataSetObserver(DataSetObserver observer) {
254 | if (mAdapter != null) {
255 | mAdapter.registerDataSetObserver(observer);
256 | }
257 | }
258 |
259 | public void unregisterDataSetObserver(DataSetObserver observer) {
260 | if (mAdapter != null) {
261 | mAdapter.unregisterDataSetObserver(observer);
262 | }
263 | }
264 |
265 | public Filter getFilter() {
266 | if (mIsFilterable) {
267 | return ((Filterable) mAdapter).getFilter();
268 | }
269 | return null;
270 | }
271 |
272 | public ListAdapter getWrappedAdapter() {
273 | return mAdapter;
274 | }
275 | }
--------------------------------------------------------------------------------
/library/src/main/java/com/etsy/android/grid/StaggeredGridView.java:
--------------------------------------------------------------------------------
1 | /*
2 | * Copyright (c) 2013 Etsy
3 | *
4 | * Licensed under the Apache License, Version 2.0 (the "License");
5 | * you may not use this file except in compliance with the License.
6 | * You may obtain a copy of the License at
7 | *
8 | * http://www.apache.org/licenses/LICENSE-2.0
9 | *
10 | * Unless required by applicable law or agreed to in writing, software
11 | * distributed under the License is distributed on an "AS IS" BASIS,
12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 | * See the License for the specific language governing permissions and
14 | * limitations under the License.
15 | */
16 |
17 | package com.etsy.android.grid;
18 |
19 | import android.content.Context;
20 | import android.content.res.Configuration;
21 | import android.content.res.TypedArray;
22 | import android.os.Parcel;
23 | import android.os.Parcelable;
24 | import android.util.AttributeSet;
25 | import android.util.Log;
26 | import android.util.SparseArray;
27 | import android.view.View;
28 | import android.view.ViewGroup;
29 |
30 | import java.util.Arrays;
31 |
32 | /**
33 | * A staggered grid view which supports multiple columns with rows of varying sizes.
34 | *
35 | * Builds multiple columns on top of {@link ExtendableListView}
36 | *
37 | * Partly inspired by - https://github.com/huewu/PinterestLikeAdapterView
38 | */
39 | public class StaggeredGridView extends ExtendableListView {
40 |
41 | private static final String TAG = "StaggeredGridView";
42 | private static final boolean DBG = false;
43 |
44 | private static final int DEFAULT_COLUMNS_PORTRAIT = 2;
45 | private static final int DEFAULT_COLUMNS_LANDSCAPE = 3;
46 |
47 | private int mColumnCount;
48 | private int mItemMargin;
49 | private int mColumnWidth;
50 | private boolean mNeedSync;
51 |
52 | private int mColumnCountPortrait = DEFAULT_COLUMNS_PORTRAIT;
53 | private int mColumnCountLandscape = DEFAULT_COLUMNS_LANDSCAPE;
54 |
55 | /**
56 | * A key-value collection where the key is the position and the
57 | * {@link GridItemRecord} with some info about that position
58 | * so we can maintain it's position - and reorg on orientation change.
59 | */
60 | private SparseArray mPositionData;
61 | private int mGridPaddingLeft;
62 | private int mGridPaddingRight;
63 | private int mGridPaddingTop;
64 | private int mGridPaddingBottom;
65 |
66 | /***
67 | * Our grid item state record with {@link Parcelable} implementation
68 | * so we can persist them across the SGV lifecycle.
69 | */
70 | static class GridItemRecord implements Parcelable {
71 | int column;
72 | double heightRatio;
73 | boolean isHeaderFooter;
74 |
75 | GridItemRecord() { }
76 |
77 | /**
78 | * Constructor called from {@link #CREATOR}
79 | */
80 | private GridItemRecord(Parcel in) {
81 | column = in.readInt();
82 | heightRatio = in.readDouble();
83 | isHeaderFooter = in.readByte() == 1;
84 | }
85 |
86 | @Override
87 | public int describeContents() {
88 | return 0;
89 | }
90 |
91 | @Override
92 | public void writeToParcel(Parcel out, int flags) {
93 | out.writeInt(column);
94 | out.writeDouble(heightRatio);
95 | out.writeByte((byte) (isHeaderFooter ? 1 : 0));
96 | }
97 |
98 | @Override
99 | public String toString() {
100 | return "GridItemRecord.ListSavedState{"
101 | + Integer.toHexString(System.identityHashCode(this))
102 | + " column:" + column
103 | + " heightRatio:" + heightRatio
104 | + " isHeaderFooter:" + isHeaderFooter
105 | + "}";
106 | }
107 |
108 | public static final Parcelable.Creator CREATOR
109 | = new Parcelable.Creator() {
110 | public GridItemRecord createFromParcel(Parcel in) {
111 | return new GridItemRecord(in);
112 | }
113 |
114 | public GridItemRecord[] newArray(int size) {
115 | return new GridItemRecord[size];
116 | }
117 | };
118 | }
119 |
120 | /**
121 | * The location of the top of each top item added in each column.
122 | */
123 | private int[] mColumnTops;
124 |
125 | /**
126 | * The location of the bottom of each bottom item added in each column.
127 | */
128 | private int[] mColumnBottoms;
129 |
130 | /**
131 | * The left location to put items for each column
132 | */
133 | private int[] mColumnLefts;
134 |
135 | /***
136 | * Tells us the distance we've offset from the top.
137 | * Can be slightly off on orientation change - TESTING
138 | */
139 | private int mDistanceToTop;
140 |
141 | public StaggeredGridView(final Context context) {
142 | this(context, null);
143 | }
144 |
145 | public StaggeredGridView(final Context context, final AttributeSet attrs) {
146 | this(context, attrs, 0);
147 | }
148 |
149 | public StaggeredGridView(final Context context, final AttributeSet attrs, final int defStyle) {
150 | super(context, attrs, defStyle);
151 |
152 | if (attrs != null) {
153 | // get the number of columns in portrait and landscape
154 | TypedArray typedArray = context.obtainStyledAttributes(attrs, R.styleable.StaggeredGridView, defStyle, 0);
155 |
156 | mColumnCount = typedArray.getInteger(
157 | R.styleable.StaggeredGridView_column_count, 0);
158 |
159 | if (mColumnCount > 0) {
160 | mColumnCountPortrait = mColumnCount;
161 | mColumnCountLandscape = mColumnCount;
162 | }
163 | else {
164 | mColumnCountPortrait = typedArray.getInteger(
165 | R.styleable.StaggeredGridView_column_count_portrait,
166 | DEFAULT_COLUMNS_PORTRAIT);
167 | mColumnCountLandscape = typedArray.getInteger(
168 | R.styleable.StaggeredGridView_column_count_landscape,
169 | DEFAULT_COLUMNS_LANDSCAPE);
170 | }
171 |
172 | mItemMargin = typedArray.getDimensionPixelSize(
173 | R.styleable.StaggeredGridView_item_margin, 0);
174 | mGridPaddingLeft = typedArray.getDimensionPixelSize(
175 | R.styleable.StaggeredGridView_grid_paddingLeft, 0);
176 | mGridPaddingRight = typedArray.getDimensionPixelSize(
177 | R.styleable.StaggeredGridView_grid_paddingRight, 0);
178 | mGridPaddingTop = typedArray.getDimensionPixelSize(
179 | R.styleable.StaggeredGridView_grid_paddingTop, 0);
180 | mGridPaddingBottom = typedArray.getDimensionPixelSize(
181 | R.styleable.StaggeredGridView_grid_paddingBottom, 0);
182 |
183 | typedArray.recycle();
184 | }
185 |
186 | mColumnCount = 0; // determined onMeasure
187 | // Creating these empty arrays to avoid saving null states
188 | mColumnTops = new int[0];
189 | mColumnBottoms = new int[0];
190 | mColumnLefts = new int[0];
191 | mPositionData = new SparseArray();
192 | }
193 |
194 | // //////////////////////////////////////////////////////////////////////////////////////////
195 | // PROPERTIES
196 | //
197 |
198 | // Grid padding is applied to the list item rows but not the header and footer
199 | public int getRowPaddingLeft() {
200 | return getListPaddingLeft() + mGridPaddingLeft;
201 | }
202 |
203 | public int getRowPaddingRight() {
204 | return getListPaddingRight() + mGridPaddingRight;
205 | }
206 |
207 | public int getRowPaddingTop() {
208 | return getListPaddingTop() + mGridPaddingTop;
209 | }
210 |
211 | public int getRowPaddingBottom() {
212 | return getListPaddingBottom() + mGridPaddingBottom;
213 | }
214 |
215 | public void setGridPadding(int left, int top, int right, int bottom) {
216 | mGridPaddingLeft = left;
217 | mGridPaddingTop = top;
218 | mGridPaddingRight = right;
219 | mGridPaddingBottom = bottom;
220 | }
221 |
222 | public void setColumnCountPortrait(int columnCountPortrait) {
223 | mColumnCountPortrait = columnCountPortrait;
224 | onSizeChanged(getWidth(), getHeight());
225 | requestLayoutChildren();
226 | }
227 |
228 | public void setColumnCountLandscape(int columnCountLandscape) {
229 | mColumnCountLandscape = columnCountLandscape;
230 | onSizeChanged(getWidth(), getHeight());
231 | requestLayoutChildren();
232 | }
233 |
234 | public void setColumnCount(int columnCount) {
235 | mColumnCountPortrait = columnCount;
236 | mColumnCountLandscape = columnCount;
237 | // mColumnCount set onSizeChanged();
238 | onSizeChanged(getWidth(), getHeight());
239 | requestLayoutChildren();
240 | }
241 |
242 | // //////////////////////////////////////////////////////////////////////////////////////////
243 | // MEASUREMENT
244 | //
245 | private boolean isLandscape() {
246 | return getResources().getConfiguration().orientation == Configuration.ORIENTATION_LANDSCAPE;
247 | }
248 |
249 | @Override
250 | protected void onMeasure(final int widthMeasureSpec, final int heightMeasureSpec) {
251 | super.onMeasure(widthMeasureSpec, heightMeasureSpec);
252 |
253 | if (mColumnCount <= 0) {
254 | boolean isLandscape = isLandscape();
255 | mColumnCount = isLandscape ? mColumnCountLandscape : mColumnCountPortrait;
256 | }
257 |
258 | // our column width is the width of the listview
259 | // minus it's padding
260 | // minus the total items margin
261 | // divided by the number of columns
262 | mColumnWidth = calculateColumnWidth(getMeasuredWidth());
263 |
264 | if (mColumnTops == null || mColumnTops.length != mColumnCount) {
265 | mColumnTops = new int[mColumnCount];
266 | initColumnTops();
267 | }
268 | if (mColumnBottoms == null || mColumnBottoms.length != mColumnCount) {
269 | mColumnBottoms = new int[mColumnCount];
270 | initColumnBottoms();
271 | }
272 | if (mColumnLefts == null || mColumnLefts.length != mColumnCount) {
273 | mColumnLefts = new int[mColumnCount];
274 | initColumnLefts();
275 | }
276 | }
277 |
278 | @Override
279 | protected void onMeasureChild(final View child, final LayoutParams layoutParams) {
280 | final int viewType = layoutParams.viewType;
281 | final int position = layoutParams.position;
282 |
283 | if (viewType == ITEM_VIEW_TYPE_HEADER_OR_FOOTER ||
284 | viewType == ITEM_VIEW_TYPE_IGNORE) {
285 | // for headers and weird ignored views
286 | super.onMeasureChild(child, layoutParams);
287 | }
288 | else {
289 | if (DBG) Log.d(TAG, "onMeasureChild BEFORE position:" + position +
290 | " h:" + getMeasuredHeight());
291 | // measure it to the width of our column.
292 | int childWidthSpec = MeasureSpec.makeMeasureSpec(mColumnWidth, MeasureSpec.EXACTLY);
293 | int childHeightSpec;
294 | if (layoutParams.height > 0) {
295 | childHeightSpec = MeasureSpec.makeMeasureSpec(layoutParams.height, MeasureSpec.EXACTLY);
296 | }
297 | else {
298 | childHeightSpec = MeasureSpec.makeMeasureSpec(LayoutParams.WRAP_CONTENT, MeasureSpec.UNSPECIFIED);
299 | }
300 | child.measure(childWidthSpec, childHeightSpec);
301 | }
302 |
303 | final int childHeight = getChildHeight(child);
304 | setPositionHeightRatio(position, childHeight);
305 |
306 | if (DBG) Log.d(TAG, "onMeasureChild AFTER position:" + position +
307 | " h:" + childHeight);
308 | }
309 |
310 | public int getColumnWidth() {
311 | return mColumnWidth;
312 | }
313 |
314 | public void resetToTop() {
315 | if (mColumnCount > 0) {
316 |
317 | if (mColumnTops == null) {
318 | mColumnTops = new int[mColumnCount];
319 | }
320 | if (mColumnBottoms == null) {
321 | mColumnBottoms = new int[mColumnCount];
322 | }
323 | initColumnTopsAndBottoms();
324 |
325 | mPositionData.clear();
326 | mNeedSync = false;
327 | mDistanceToTop = 0;
328 | setSelection(0);
329 | }
330 | }
331 |
332 | // //////////////////////////////////////////////////////////////////////////////////////////
333 | // POSITIONING
334 | //
335 |
336 | @Override
337 | protected void onChildCreated(final int position, final boolean flowDown) {
338 | super.onChildCreated(position, flowDown);
339 | if (!isHeaderOrFooter(position)) {
340 | // do we already have a column for this position?
341 | final int column = getChildColumn(position, flowDown);
342 | setPositionColumn(position, column);
343 | if (DBG) Log.d(TAG, "onChildCreated position:" + position +
344 | " is in column:" + column);
345 | }
346 | else {
347 | setPositionIsHeaderFooter(position);
348 | }
349 | }
350 |
351 | private void requestLayoutChildren() {
352 | final int count = getChildCount();
353 | for (int i = 0; i < count; i++) {
354 | final View v = getChildAt(i);
355 | if (v != null) v.requestLayout();
356 | }
357 | }
358 |
359 | @Override
360 | protected void layoutChildren() {
361 | preLayoutChildren();
362 | super.layoutChildren();
363 | }
364 |
365 | private void preLayoutChildren() {
366 | // on a major re-layout reset for our next layout pass
367 | if (!mNeedSync) {
368 | Arrays.fill(mColumnBottoms, 0);
369 | }
370 | else {
371 | mNeedSync = false;
372 | }
373 | // copy the tops into the bottom
374 | // since we're going to redo a layout pass that will draw down from
375 | // the top
376 | System.arraycopy(mColumnTops, 0, mColumnBottoms, 0, mColumnCount);
377 | }
378 |
379 | // NOTE : Views will either be layout out via onLayoutChild
380 | // OR
381 | // Views will be offset if they are active but offscreen so that we can recycle!
382 | // Both onLayoutChild() and onOffsetChild are called after we measure our view
383 | // see ExtensibleListView.setupChild();
384 |
385 | @Override
386 | protected void onLayoutChild(final View child,
387 | final int position,
388 | final boolean flowDown,
389 | final int childrenLeft, final int childTop,
390 | final int childRight, final int childBottom) {
391 | if (isHeaderOrFooter(position)) {
392 | layoutGridHeaderFooter(child, position, flowDown, childrenLeft, childTop, childRight, childBottom);
393 | }
394 | else {
395 | layoutGridChild(child, position, flowDown, childrenLeft, childRight);
396 | }
397 | }
398 |
399 | private void layoutGridHeaderFooter(final View child, final int position, final boolean flowDown, final int childrenLeft, final int childTop, final int childRight, final int childBottom) {
400 | // offset the top and bottom of all our columns
401 | // if it's the footer we want it below the lowest child bottom
402 | int gridChildTop;
403 | int gridChildBottom;
404 |
405 | if (flowDown) {
406 | gridChildTop = getLowestPositionedBottom();
407 | gridChildBottom = gridChildTop + getChildHeight(child);
408 | }
409 | else {
410 | gridChildBottom = getHighestPositionedTop();
411 | gridChildTop = gridChildBottom - getChildHeight(child);
412 | }
413 |
414 | for (int i = 0; i < mColumnCount; i++) {
415 | updateColumnTopIfNeeded(i, gridChildTop);
416 | updateColumnBottomIfNeeded(i, gridChildBottom);
417 | }
418 |
419 | super.onLayoutChild(child, position, flowDown,
420 | childrenLeft, gridChildTop, childRight, gridChildBottom);
421 | }
422 |
423 | private void layoutGridChild(final View child, final int position,
424 | final boolean flowDown,
425 | final int childrenLeft, final int childRight) {
426 | // stash the bottom and the top if it's higher positioned
427 | int column = getPositionColumn(position);
428 |
429 | int gridChildTop;
430 | int gridChildBottom;
431 |
432 | int childTopMargin = getChildTopMargin(position);
433 | int childBottomMargin = getChildBottomMargin();
434 | int verticalMargins = childTopMargin + childBottomMargin;
435 |
436 | if (flowDown) {
437 | gridChildTop = mColumnBottoms[column]; // the next items top is the last items bottom
438 | gridChildBottom = gridChildTop + (getChildHeight(child) + verticalMargins);
439 | }
440 | else {
441 | gridChildBottom = mColumnTops[column]; // the bottom of the next column up is our top
442 | gridChildTop = gridChildBottom - (getChildHeight(child) + verticalMargins);
443 | }
444 |
445 | if (DBG) Log.d(TAG, "onLayoutChild position:" + position +
446 | " column:" + column +
447 | " gridChildTop:" + gridChildTop +
448 | " gridChildBottom:" + gridChildBottom);
449 |
450 | // we also know the column of this view so let's stash it in the
451 | // view's layout params
452 | GridLayoutParams layoutParams = (GridLayoutParams) child.getLayoutParams();
453 | layoutParams.column = column;
454 |
455 | updateColumnBottomIfNeeded(column, gridChildBottom);
456 | updateColumnTopIfNeeded(column, gridChildTop);
457 |
458 | // subtract the margins before layout
459 | gridChildTop += childTopMargin;
460 | gridChildBottom -= childBottomMargin;
461 |
462 | child.layout(childrenLeft, gridChildTop, childRight, gridChildBottom);
463 | }
464 |
465 | @Override
466 | protected void onOffsetChild(final View child, final int position,
467 | final boolean flowDown, final int childrenLeft, final int childTop) {
468 | // if the child is recycled and is just offset
469 | // we still want to add its deets into our store
470 | if (isHeaderOrFooter(position)) {
471 |
472 | offsetGridHeaderFooter(child, position, flowDown, childrenLeft, childTop);
473 | }
474 | else {
475 | offsetGridChild(child, position, flowDown, childrenLeft, childTop);
476 | }
477 | }
478 |
479 | private void offsetGridHeaderFooter(final View child, final int position, final boolean flowDown, final int childrenLeft, final int childTop) {
480 | // offset the top and bottom of all our columns
481 | // if it's the footer we want it below the lowest child bottom
482 | int gridChildTop;
483 | int gridChildBottom;
484 |
485 | if (flowDown) {
486 | gridChildTop = getLowestPositionedBottom();
487 | gridChildBottom = gridChildTop + getChildHeight(child);
488 | }
489 | else {
490 | gridChildBottom = getHighestPositionedTop();
491 | gridChildTop = gridChildBottom - getChildHeight(child);
492 | }
493 |
494 | for (int i = 0; i < mColumnCount; i++) {
495 | updateColumnTopIfNeeded(i, gridChildTop);
496 | updateColumnBottomIfNeeded(i, gridChildBottom);
497 | }
498 |
499 | super.onOffsetChild(child, position, flowDown, childrenLeft, gridChildTop);
500 | }
501 |
502 | private void offsetGridChild(final View child, final int position, final boolean flowDown, final int childrenLeft, final int childTop) {
503 | // stash the bottom and the top if it's higher positioned
504 | int column = getPositionColumn(position);
505 |
506 | int gridChildTop;
507 | int gridChildBottom;
508 |
509 | int childTopMargin = getChildTopMargin(position);
510 | int childBottomMargin = getChildBottomMargin();
511 | int verticalMargins = childTopMargin + childBottomMargin;
512 |
513 | if (flowDown) {
514 | gridChildTop = mColumnBottoms[column]; // the next items top is the last items bottom
515 | gridChildBottom = gridChildTop + (getChildHeight(child) + verticalMargins);
516 | }
517 | else {
518 | gridChildBottom = mColumnTops[column]; // the bottom of the next column up is our top
519 | gridChildTop = gridChildBottom - (getChildHeight(child) + verticalMargins);
520 | }
521 |
522 | if (DBG) Log.d(TAG, "onOffsetChild position:" + position +
523 | " column:" + column +
524 | " childTop:" + childTop +
525 | " gridChildTop:" + gridChildTop +
526 | " gridChildBottom:" + gridChildBottom);
527 |
528 | // we also know the column of this view so let's stash it in the
529 | // view's layout params
530 | GridLayoutParams layoutParams = (GridLayoutParams) child.getLayoutParams();
531 | layoutParams.column = column;
532 |
533 | updateColumnBottomIfNeeded(column, gridChildBottom);
534 | updateColumnTopIfNeeded(column, gridChildTop);
535 |
536 | super.onOffsetChild(child, position, flowDown, childrenLeft, gridChildTop + childTopMargin);
537 | }
538 |
539 | private int getChildHeight(final View child) {
540 | return child.getMeasuredHeight();
541 | }
542 |
543 | private int getChildTopMargin(final int position) {
544 | boolean isFirstRow = position < (getHeaderViewsCount() + mColumnCount);
545 | return isFirstRow ? mItemMargin : 0;
546 | }
547 |
548 | private int getChildBottomMargin() {
549 | return mItemMargin;
550 | }
551 |
552 | @Override
553 | protected LayoutParams generateChildLayoutParams(final View child) {
554 | GridLayoutParams layoutParams = null;
555 |
556 | final ViewGroup.LayoutParams childParams = child.getLayoutParams();
557 | if (childParams != null) {
558 | if (childParams instanceof GridLayoutParams) {
559 | layoutParams = (GridLayoutParams) childParams;
560 | }
561 | else {
562 | layoutParams = new GridLayoutParams(childParams);
563 | }
564 | }
565 | if (layoutParams == null) {
566 | layoutParams = new GridLayoutParams(
567 | mColumnWidth, ViewGroup.LayoutParams.WRAP_CONTENT);
568 | }
569 |
570 | return layoutParams;
571 | }
572 |
573 | private void updateColumnTopIfNeeded(int column, int childTop) {
574 | if (childTop < mColumnTops[column]) {
575 | mColumnTops[column] = childTop;
576 | }
577 | }
578 |
579 | private void updateColumnBottomIfNeeded(int column, int childBottom) {
580 | if (childBottom > mColumnBottoms[column]) {
581 | mColumnBottoms[column] = childBottom;
582 | }
583 | }
584 |
585 | @Override
586 | protected int getChildLeft(final int position) {
587 | if (isHeaderOrFooter(position)) {
588 | return super.getChildLeft(position);
589 | }
590 | else {
591 | final int column = getPositionColumn(position);
592 | return mColumnLefts[column];
593 | }
594 | }
595 |
596 | @Override
597 | protected int getChildTop(final int position) {
598 | if (isHeaderOrFooter(position)) {
599 | return super.getChildTop(position);
600 | }
601 | else {
602 | final int column = getPositionColumn(position);
603 | if (column == -1) {
604 | return getHighestPositionedBottom();
605 | }
606 | return mColumnBottoms[column];
607 | }
608 | }
609 |
610 | /**
611 | * Get the top for the next child down in our view
612 | * (maybe a column across) so we can fill down.
613 | */
614 | @Override
615 | protected int getNextChildDownsTop(final int position) {
616 | if (isHeaderOrFooter(position)) {
617 | return super.getNextChildDownsTop(position);
618 | }
619 | else {
620 | return getHighestPositionedBottom();
621 | }
622 | }
623 |
624 | @Override
625 | protected int getChildBottom(final int position) {
626 | if (isHeaderOrFooter(position)) {
627 | return super.getChildBottom(position);
628 | }
629 | else {
630 | final int column = getPositionColumn(position);
631 | if (column == -1) {
632 | return getLowestPositionedTop();
633 | }
634 | return mColumnTops[column];
635 | }
636 | }
637 |
638 | /**
639 | * Get the bottom for the next child up in our view
640 | * (maybe a column across) so we can fill up.
641 | */
642 | @Override
643 | protected int getNextChildUpsBottom(final int position) {
644 | if (isHeaderOrFooter(position)) {
645 | return super.getNextChildUpsBottom(position);
646 | }
647 | else {
648 | return getLowestPositionedTop();
649 | }
650 | }
651 |
652 | @Override
653 | protected int getLastChildBottom() {
654 | final int lastPosition = mFirstPosition + (getChildCount() - 1);
655 | if (isHeaderOrFooter(lastPosition)) {
656 | return super.getLastChildBottom();
657 | }
658 | return getHighestPositionedBottom();
659 | }
660 |
661 | @Override
662 | protected int getFirstChildTop() {
663 | if (isHeaderOrFooter(mFirstPosition)) {
664 | return super.getFirstChildTop();
665 | }
666 | return getLowestPositionedTop();
667 | }
668 |
669 | @Override
670 | protected int getHighestChildTop() {
671 | if (isHeaderOrFooter(mFirstPosition)) {
672 | return super.getHighestChildTop();
673 | }
674 | return getHighestPositionedTop();
675 | }
676 |
677 | @Override
678 | protected int getLowestChildBottom() {
679 | final int lastPosition = mFirstPosition + (getChildCount() - 1);
680 | if (isHeaderOrFooter(lastPosition)) {
681 | return super.getLowestChildBottom();
682 | }
683 | return getLowestPositionedBottom();
684 | }
685 |
686 | @Override
687 | protected void offsetChildrenTopAndBottom(final int offset) {
688 | super.offsetChildrenTopAndBottom(offset);
689 | offsetAllColumnsTopAndBottom(offset);
690 | offsetDistanceToTop(offset);
691 | }
692 |
693 | protected void offsetChildrenTopAndBottom(final int offset, final int column) {
694 | if (DBG) Log.d(TAG, "offsetChildrenTopAndBottom: " + offset + " column:" + column);
695 | final int count = getChildCount();
696 | for (int i = 0; i < count; i++) {
697 | final View v = getChildAt(i);
698 | if (v != null &&
699 | v.getLayoutParams() != null &&
700 | v.getLayoutParams() instanceof GridLayoutParams) {
701 | GridLayoutParams lp = (GridLayoutParams) v.getLayoutParams();
702 | if (lp.column == column) {
703 | v.offsetTopAndBottom(offset);
704 | }
705 | }
706 | }
707 | offsetColumnTopAndBottom(offset, column);
708 | }
709 |
710 | private void offsetDistanceToTop(final int offset) {
711 | mDistanceToTop += offset;
712 | if (DBG) Log.d(TAG, "offset mDistanceToTop:" + mDistanceToTop);
713 | }
714 |
715 | public int getDistanceToTop() {
716 | return mDistanceToTop;
717 | }
718 |
719 | private void offsetAllColumnsTopAndBottom(final int offset) {
720 | if (offset != 0) {
721 | for (int i = 0; i < mColumnCount; i++) {
722 | offsetColumnTopAndBottom(offset, i);
723 | }
724 | }
725 | }
726 |
727 | private void offsetColumnTopAndBottom(final int offset, final int column) {
728 | if (offset != 0) {
729 | mColumnTops[column] += offset;
730 | mColumnBottoms[column] += offset;
731 | }
732 | }
733 |
734 | @Override
735 | protected void adjustViewsAfterFillGap(final boolean down) {
736 | super.adjustViewsAfterFillGap(down);
737 | // fix vertical gaps when hitting the top after a rotate
738 | // only when scrolling back up!
739 | if (!down) {
740 | alignTops();
741 | }
742 | }
743 |
744 | private void alignTops() {
745 | if (mFirstPosition == getHeaderViewsCount()) {
746 | // we're showing all the views before the header views
747 | int[] nonHeaderTops = getHighestNonHeaderTops();
748 | // we should now have our non header tops
749 | // align them
750 | boolean isAligned = true;
751 | int highestColumn = -1;
752 | int highestTop = Integer.MAX_VALUE;
753 | for (int i = 0; i < nonHeaderTops.length; i++) {
754 | // are they all aligned
755 | if (isAligned && i > 0 && nonHeaderTops[i] != highestTop) {
756 | isAligned = false; // not all the tops are aligned
757 | }
758 | // what's the highest
759 | if (nonHeaderTops[i] < highestTop) {
760 | highestTop = nonHeaderTops[i];
761 | highestColumn = i;
762 | }
763 | }
764 |
765 | // skip the rest.
766 | if (isAligned) return;
767 |
768 | // we've got the highest column - lets align the others
769 | for (int i = 0; i < nonHeaderTops.length; i++) {
770 | if (i != highestColumn) {
771 | // there's a gap in this column
772 | int offset = highestTop - nonHeaderTops[i];
773 | offsetChildrenTopAndBottom(offset, i);
774 | }
775 | }
776 | invalidate();
777 | }
778 | }
779 |
780 | private int[] getHighestNonHeaderTops() {
781 | int[] nonHeaderTops = new int[mColumnCount];
782 | int childCount = getChildCount();
783 | if (childCount > 0) {
784 | for (int i = 0; i < childCount; i++) {
785 | View child = getChildAt(i);
786 | if (child != null &&
787 | child.getLayoutParams() != null &&
788 | child.getLayoutParams() instanceof GridLayoutParams) {
789 | // is this child's top the highest non
790 | GridLayoutParams lp = (GridLayoutParams) child.getLayoutParams();
791 | // is it a child that isn't a header
792 | if (lp.viewType != ITEM_VIEW_TYPE_HEADER_OR_FOOTER &&
793 | child.getTop() < nonHeaderTops[lp.column]) {
794 | nonHeaderTops[lp.column] = child.getTop();
795 | }
796 | }
797 | }
798 | }
799 | return nonHeaderTops;
800 | }
801 |
802 | @Override
803 | protected void onChildrenDetached(final int start, final int count) {
804 | super.onChildrenDetached(start, count);
805 | // go through our remaining views and sync the top and bottom stash.
806 |
807 | // Repair the top and bottom column boundaries from the views we still have
808 | Arrays.fill(mColumnTops, Integer.MAX_VALUE);
809 | Arrays.fill(mColumnBottoms, 0);
810 |
811 | for (int i = 0; i < getChildCount(); i++) {
812 | final View child = getChildAt(i);
813 | if (child != null) {
814 | final LayoutParams childParams = (LayoutParams) child.getLayoutParams();
815 | if (childParams.viewType != ITEM_VIEW_TYPE_HEADER_OR_FOOTER &&
816 | childParams instanceof GridLayoutParams) {
817 | GridLayoutParams layoutParams = (GridLayoutParams) childParams;
818 | int column = layoutParams.column;
819 | int position = layoutParams.position;
820 | final int childTop = child.getTop();
821 | if (childTop < mColumnTops[column]) {
822 | mColumnTops[column] = childTop - getChildTopMargin(position);
823 | }
824 | final int childBottom = child.getBottom();
825 | if (childBottom > mColumnBottoms[column]) {
826 | mColumnBottoms[column] = childBottom + getChildBottomMargin();
827 | }
828 | }
829 | else {
830 | // the header and footer here
831 | final int childTop = child.getTop();
832 | final int childBottom = child.getBottom();
833 |
834 | for (int col = 0; col < mColumnCount; col++) {
835 | if (childTop < mColumnTops[col]) {
836 | mColumnTops[col] = childTop;
837 | }
838 | if (childBottom > mColumnBottoms[col]) {
839 | mColumnBottoms[col] = childBottom;
840 | }
841 | }
842 |
843 | }
844 | }
845 | }
846 | }
847 |
848 | @Override
849 | protected boolean hasSpaceUp() {
850 | int end = mClipToPadding ? getRowPaddingTop() : 0;
851 | return getLowestPositionedTop() > end;
852 | }
853 |
854 | // //////////////////////////////////////////////////////////////////////////////////////////
855 | // SYNCING ACROSS ROTATION
856 | //
857 |
858 | @Override
859 | protected void onSizeChanged(final int w, final int h, final int oldw, final int oldh) {
860 | super.onSizeChanged(w, h, oldw, oldh);
861 | onSizeChanged(w, h);
862 | }
863 |
864 | @Override
865 | protected void onSizeChanged(int w, int h) {
866 | super.onSizeChanged(w, h);
867 | boolean isLandscape = isLandscape();
868 | int newColumnCount = isLandscape ? mColumnCountLandscape : mColumnCountPortrait;
869 | if (mColumnCount != newColumnCount) {
870 | mColumnCount = newColumnCount;
871 |
872 | mColumnWidth = calculateColumnWidth(w);
873 |
874 | mColumnTops = new int[mColumnCount];
875 | mColumnBottoms = new int[mColumnCount];
876 | mColumnLefts = new int[mColumnCount];
877 |
878 | mDistanceToTop = 0;
879 |
880 | // rebuild the columns
881 | initColumnTopsAndBottoms();
882 | initColumnLefts();
883 |
884 | // if we have data
885 | if (getCount() > 0 && mPositionData.size() > 0) {
886 | onColumnSync();
887 | }
888 |
889 | requestLayout();
890 | }
891 | }
892 |
893 | private int calculateColumnWidth(final int gridWidth) {
894 | final int listPadding = getRowPaddingLeft() + getRowPaddingRight();
895 | return (gridWidth - listPadding - mItemMargin * (mColumnCount + 1)) / mColumnCount;
896 | }
897 |
898 | private int calculateColumnLeft(final int colIndex) {
899 | return getRowPaddingLeft() + mItemMargin + ((mItemMargin + mColumnWidth) * colIndex);
900 | }
901 |
902 | /***
903 | * Our mColumnTops and mColumnBottoms need to be re-built up to the
904 | * mSyncPosition - the following layout request will then
905 | * layout the that position and then fillUp and fillDown appropriately.
906 | */
907 | private void onColumnSync() {
908 | // re-calc tops for new column count!
909 | int syncPosition = Math.min(mSyncPosition, getCount() - 1);
910 |
911 | SparseArray positionHeightRatios = new SparseArray(syncPosition);
912 | for (int pos = 0; pos < syncPosition; pos++) {
913 | // check for weirdness
914 | final GridItemRecord rec = mPositionData.get(pos);
915 | if (rec == null) break;
916 |
917 | Log.d(TAG, "onColumnSync:" + pos + " ratio:" + rec.heightRatio);
918 | positionHeightRatios.append(pos, rec.heightRatio);
919 | }
920 |
921 | mPositionData.clear();
922 |
923 | // re-calc our relative position while at the same time
924 | // rebuilding our GridItemRecord collection
925 |
926 | if (DBG) Log.d(TAG, "onColumnSync column width:" + mColumnWidth);
927 |
928 | for (int pos = 0; pos < syncPosition; pos++) {
929 | //Check for weirdness again
930 | final Double heightRatio = positionHeightRatios.get(pos);
931 | if(heightRatio == null){
932 | break;
933 | }
934 |
935 | final GridItemRecord rec = getOrCreateRecord(pos);
936 | final int height = (int) (mColumnWidth * heightRatio);
937 | rec.heightRatio = heightRatio;
938 |
939 | int top;
940 | int bottom;
941 | // check for headers
942 | if (isHeaderOrFooter(pos)) {
943 | // the next top is the bottom for that column
944 | top = getLowestPositionedBottom();
945 | bottom = top + height;
946 |
947 | for (int i = 0; i < mColumnCount; i++) {
948 | mColumnTops[i] = top;
949 | mColumnBottoms[i] = bottom;
950 | }
951 | }
952 | else {
953 | // what's the next column down ?
954 | final int column = getHighestPositionedBottomColumn();
955 | // the next top is the bottom for that column
956 | top = mColumnBottoms[column];
957 | bottom = top + height + getChildTopMargin(pos) + getChildBottomMargin();
958 |
959 | mColumnTops[column] = top;
960 | mColumnBottoms[column] = bottom;
961 |
962 | rec.column = column;
963 | }
964 |
965 |
966 | if (DBG) Log.d(TAG, "onColumnSync position:" + pos +
967 | " top:" + top +
968 | " bottom:" + bottom +
969 | " height:" + height +
970 | " heightRatio:" + heightRatio);
971 | }
972 |
973 | // our sync position will be displayed in this column
974 | final int syncColumn = getHighestPositionedBottomColumn();
975 | setPositionColumn(syncPosition, syncColumn);
976 |
977 | // we want to offset from height of the sync position
978 | // minus the offset
979 | int syncToBottom = mColumnBottoms[syncColumn];
980 | int offset = -syncToBottom + mSpecificTop;
981 | // offset all columns by
982 | offsetAllColumnsTopAndBottom(offset);
983 |
984 | // sync the distance to top
985 | mDistanceToTop = -syncToBottom;
986 |
987 | // stash our bottoms in our tops - though these will be copied back to the bottoms
988 | System.arraycopy(mColumnBottoms, 0, mColumnTops, 0, mColumnCount);
989 | }
990 |
991 |
992 | // //////////////////////////////////////////////////////////////////////////////////////////
993 | // GridItemRecord UTILS
994 | //
995 |
996 | private void setPositionColumn(final int position, final int column) {
997 | GridItemRecord rec = getOrCreateRecord(position);
998 | rec.column = column;
999 | }
1000 |
1001 | private void setPositionHeightRatio(final int position, final int height) {
1002 | GridItemRecord rec = getOrCreateRecord(position);
1003 | rec.heightRatio = (double) height / (double) mColumnWidth;
1004 | if (DBG) Log.d(TAG, "position:" + position +
1005 | " width:" + mColumnWidth +
1006 | " height:" + height +
1007 | " heightRatio:" + rec.heightRatio);
1008 | }
1009 |
1010 | private void setPositionIsHeaderFooter(final int position) {
1011 | GridItemRecord rec = getOrCreateRecord(position);
1012 | rec.isHeaderFooter = true;
1013 | }
1014 |
1015 | private GridItemRecord getOrCreateRecord(final int position) {
1016 | GridItemRecord rec = mPositionData.get(position, null);
1017 | if (rec == null) {
1018 | rec = new GridItemRecord();
1019 | mPositionData.append(position, rec);
1020 | }
1021 | return rec;
1022 | }
1023 |
1024 | private int getPositionColumn(final int position) {
1025 | GridItemRecord rec = mPositionData.get(position, null);
1026 | return rec != null ? rec.column : -1;
1027 | }
1028 |
1029 |
1030 | // //////////////////////////////////////////////////////////////////////////////////////////
1031 | // HELPERS
1032 | //
1033 |
1034 | private boolean isHeaderOrFooter(final int position) {
1035 | final int viewType = mAdapter.getItemViewType(position);
1036 | return viewType == ITEM_VIEW_TYPE_HEADER_OR_FOOTER;
1037 | }
1038 |
1039 | private int getChildColumn(final int position, final boolean flowDown) {
1040 |
1041 | // do we already have a column for this child position?
1042 | int column = getPositionColumn(position);
1043 | // we don't have the column or it no longer fits in our grid
1044 | final int columnCount = mColumnCount;
1045 | if (column < 0 || column >= columnCount) {
1046 | // if we're going down -
1047 | // get the highest positioned (lowest value)
1048 | // column bottom
1049 | if (flowDown) {
1050 | column = getHighestPositionedBottomColumn();
1051 | }
1052 | else {
1053 | column = getLowestPositionedTopColumn();
1054 |
1055 | }
1056 | }
1057 | return column;
1058 | }
1059 |
1060 | private void initColumnTopsAndBottoms() {
1061 | initColumnTops();
1062 | initColumnBottoms();
1063 | }
1064 |
1065 | private void initColumnTops() {
1066 | Arrays.fill(mColumnTops, getPaddingTop() + mGridPaddingTop);
1067 | }
1068 |
1069 | private void initColumnBottoms() {
1070 | Arrays.fill(mColumnBottoms, getPaddingTop() + mGridPaddingTop);
1071 | }
1072 |
1073 | private void initColumnLefts() {
1074 | for (int i = 0; i < mColumnCount; i++) {
1075 | mColumnLefts[i] = calculateColumnLeft(i);
1076 | }
1077 | }
1078 |
1079 |
1080 | // //////////////////////////////////////////////////////////////////////////////////////////
1081 | // BOTTOM
1082 | //
1083 |
1084 | private int getHighestPositionedBottom() {
1085 | final int column = getHighestPositionedBottomColumn();
1086 | return mColumnBottoms[column];
1087 | }
1088 |
1089 | private int getHighestPositionedBottomColumn() {
1090 | int columnFound = 0;
1091 | int highestPositionedBottom = Integer.MAX_VALUE;
1092 | // the highest positioned bottom is the one with the lowest value :D
1093 | for (int i = 0; i < mColumnCount; i++) {
1094 | int bottom = mColumnBottoms[i];
1095 | if (bottom < highestPositionedBottom) {
1096 | highestPositionedBottom = bottom;
1097 | columnFound = i;
1098 | }
1099 | }
1100 | return columnFound;
1101 | }
1102 |
1103 | private int getLowestPositionedBottom() {
1104 | final int column = getLowestPositionedBottomColumn();
1105 | return mColumnBottoms[column];
1106 | }
1107 |
1108 | private int getLowestPositionedBottomColumn() {
1109 | int columnFound = 0;
1110 | int lowestPositionedBottom = Integer.MIN_VALUE;
1111 | // the lowest positioned bottom is the one with the highest value :D
1112 | for (int i = 0; i < mColumnCount; i++) {
1113 | int bottom = mColumnBottoms[i];
1114 | if (bottom > lowestPositionedBottom) {
1115 | lowestPositionedBottom = bottom;
1116 | columnFound = i;
1117 | }
1118 | }
1119 | return columnFound;
1120 | }
1121 |
1122 | // //////////////////////////////////////////////////////////////////////////////////////////
1123 | // TOP
1124 | //
1125 |
1126 | private int getLowestPositionedTop() {
1127 | final int column = getLowestPositionedTopColumn();
1128 | return mColumnTops[column];
1129 | }
1130 |
1131 | private int getLowestPositionedTopColumn() {
1132 | int columnFound = 0;
1133 | // we'll go backwards through since the right most
1134 | // will likely be the lowest positioned Top
1135 | int lowestPositionedTop = Integer.MIN_VALUE;
1136 | // the lowest positioned top is the one with the highest value :D
1137 | for (int i = 0; i < mColumnCount; i++) {
1138 | int top = mColumnTops[i];
1139 | if (top > lowestPositionedTop) {
1140 | lowestPositionedTop = top;
1141 | columnFound = i;
1142 | }
1143 | }
1144 | return columnFound;
1145 | }
1146 |
1147 | private int getHighestPositionedTop() {
1148 | final int column = getHighestPositionedTopColumn();
1149 | return mColumnTops[column];
1150 | }
1151 |
1152 | private int getHighestPositionedTopColumn() {
1153 | int columnFound = 0;
1154 | int highestPositionedTop = Integer.MAX_VALUE;
1155 | // the highest positioned top is the one with the lowest value :D
1156 | for (int i = 0; i < mColumnCount; i++) {
1157 | int top = mColumnTops[i];
1158 | if (top < highestPositionedTop) {
1159 | highestPositionedTop = top;
1160 | columnFound = i;
1161 | }
1162 | }
1163 | return columnFound;
1164 | }
1165 |
1166 | // //////////////////////////////////////////////////////////////////////////////////////////
1167 | // LAYOUT PARAMS
1168 | //
1169 |
1170 | /**
1171 | * Extended LayoutParams to column position and anything else we may been for the grid
1172 | */
1173 | public static class GridLayoutParams extends LayoutParams {
1174 |
1175 | // The column the view is displayed in
1176 | int column;
1177 |
1178 | public GridLayoutParams(Context c, AttributeSet attrs) {
1179 | super(c, attrs);
1180 | enforceStaggeredLayout();
1181 | }
1182 |
1183 | public GridLayoutParams(int w, int h) {
1184 | super(w, h);
1185 | enforceStaggeredLayout();
1186 | }
1187 |
1188 | public GridLayoutParams(int w, int h, int viewType) {
1189 | super(w, h);
1190 | enforceStaggeredLayout();
1191 | }
1192 |
1193 | public GridLayoutParams(ViewGroup.LayoutParams source) {
1194 | super(source);
1195 | enforceStaggeredLayout();
1196 | }
1197 |
1198 | /**
1199 | * Here we're making sure that all grid view items
1200 | * are width MATCH_PARENT and height WRAP_CONTENT.
1201 | * That's what this grid is designed for
1202 | */
1203 | private void enforceStaggeredLayout() {
1204 | if (width != MATCH_PARENT) {
1205 | width = MATCH_PARENT;
1206 | }
1207 | if (height == MATCH_PARENT) {
1208 | height = WRAP_CONTENT;
1209 | }
1210 | }
1211 | }
1212 |
1213 | // //////////////////////////////////////////////////////////////////////////////////////////
1214 | // SAVED STATE
1215 |
1216 |
1217 | public static class GridListSavedState extends ListSavedState {
1218 | int columnCount;
1219 | int[] columnTops;
1220 | SparseArray positionData;
1221 |
1222 | public GridListSavedState(Parcelable superState) {
1223 | super(superState);
1224 | }
1225 |
1226 | /**
1227 | * Constructor called from {@link #CREATOR}
1228 | */
1229 | public GridListSavedState(Parcel in) {
1230 | super(in);
1231 | columnCount = in.readInt();
1232 | columnTops = new int[columnCount >= 0 ? columnCount : 0];
1233 | in.readIntArray(columnTops);
1234 | positionData = in.readSparseArray(GridItemRecord.class.getClassLoader());
1235 | }
1236 |
1237 | @Override
1238 | public void writeToParcel(Parcel out, int flags) {
1239 | super.writeToParcel(out, flags);
1240 | out.writeInt(columnCount);
1241 | out.writeIntArray(columnTops);
1242 | out.writeSparseArray(positionData);
1243 | }
1244 |
1245 | @Override
1246 | public String toString() {
1247 | return "StaggeredGridView.GridListSavedState{"
1248 | + Integer.toHexString(System.identityHashCode(this)) + "}";
1249 | }
1250 |
1251 | public static final Creator CREATOR
1252 | = new Creator() {
1253 | public GridListSavedState createFromParcel(Parcel in) {
1254 | return new GridListSavedState(in);
1255 | }
1256 |
1257 | public GridListSavedState[] newArray(int size) {
1258 | return new GridListSavedState[size];
1259 | }
1260 | };
1261 | }
1262 |
1263 |
1264 | @Override
1265 | public Parcelable onSaveInstanceState() {
1266 | ListSavedState listState = (ListSavedState) super.onSaveInstanceState();
1267 | GridListSavedState ss = new GridListSavedState(listState.getSuperState());
1268 |
1269 | // from the list state
1270 | ss.selectedId = listState.selectedId;
1271 | ss.firstId = listState.firstId;
1272 | ss.viewTop = listState.viewTop;
1273 | ss.position = listState.position;
1274 | ss.height = listState.height;
1275 |
1276 | // our state
1277 |
1278 | boolean haveChildren = getChildCount() > 0 && getCount() > 0;
1279 |
1280 | if (haveChildren && mFirstPosition > 0) {
1281 | ss.columnCount = mColumnCount;
1282 | ss.columnTops = mColumnTops;
1283 | ss.positionData = mPositionData;
1284 | }
1285 | else {
1286 | ss.columnCount = mColumnCount >= 0 ? mColumnCount : 0;
1287 | ss.columnTops = new int[ss.columnCount];
1288 | ss.positionData = new SparseArray