├── .gitignore
├── README.md
└── demo
├── AndroidManifest.xml
├── libs
└── android-support-v4.jar
├── proguard-project.txt
├── project.properties
├── res
├── drawable-hdpi
│ └── ic_launcher.png
├── drawable-mdpi
│ └── ic_launcher.png
├── drawable-xhdpi
│ └── ic_launcher.png
├── drawable
│ └── bg_white_box.xml
├── layout
│ ├── activity_main.xml
│ ├── element_header.xml
│ ├── element_item.xml
│ ├── element_item_large.xml
│ └── element_item_small.xml
├── menu
│ └── activity_main.xml
├── values-v11
│ └── styles.xml
├── values-v14
│ └── styles.xml
└── values
│ ├── strings.xml
│ └── styles.xml
└── src
├── android
└── support
│ └── v4
│ └── widget
│ └── StaggeredGridView.java
└── com
└── mino
└── staggeredgridview
└── MainActivity.java
/.gitignore:
--------------------------------------------------------------------------------
1 | # built application files
2 | *.apk
3 | *.ap_
4 |
5 | # files for the dex VM
6 | *.dex
7 |
8 | # Java class files
9 | *.class
10 |
11 | # generated files
12 | bin/
13 | gen/
14 |
15 | # Local configuration file (sdk path, etc)
16 | local.properties
17 |
18 | # Eclipse project files
19 | .classpath
20 | .project
21 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | StaggeredGridView
2 | =================
3 |
4 | ## This is just a demo
5 |
6 | If you want a better implimentation of this, either wait for google to release it.
7 |
8 | Or try https://github.com/maurycyw/StaggeredGridView
9 |
10 |
11 | ### About
12 | Based of the google staggeredgridview that has been hidden from the ACL, this is an example based off of that and supporting scroll listener.
13 |
--------------------------------------------------------------------------------
/demo/AndroidManifest.xml:
--------------------------------------------------------------------------------
1 |
2 |
6 |
7 |
10 |
11 |
16 |
19 |
20 |
21 |
22 |
23 |
24 |
25 |
26 |
27 |
--------------------------------------------------------------------------------
/demo/libs/android-support-v4.jar:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/chrisjenx/StaggeredGridView/1016867787ff374b93edd85d3e17257e39244eec/demo/libs/android-support-v4.jar
--------------------------------------------------------------------------------
/demo/proguard-project.txt:
--------------------------------------------------------------------------------
1 | # To enable ProGuard in your project, edit project.properties
2 | # to define the proguard.config property as described in that file.
3 | #
4 | # Add project specific ProGuard rules here.
5 | # By default, the flags in this file are appended to flags specified
6 | # in ${sdk.dir}/tools/proguard/proguard-android.txt
7 | # You can edit the include path and order by changing the ProGuard
8 | # include property in project.properties.
9 | #
10 | # For more details, see
11 | # http://developer.android.com/guide/developing/tools/proguard.html
12 |
13 | # Add any project specific keep options here:
14 |
15 | # If your project uses WebView with JS, uncomment the following
16 | # and specify the fully qualified class name to the JavaScript interface
17 | # class:
18 | #-keepclassmembers class fqcn.of.javascript.interface.for.webview {
19 | # public *;
20 | #}
21 |
--------------------------------------------------------------------------------
/demo/project.properties:
--------------------------------------------------------------------------------
1 | # This file is automatically generated by Android Tools.
2 | # Do not modify this file -- YOUR CHANGES WILL BE ERASED!
3 | #
4 | # This file must be checked in Version Control Systems.
5 | #
6 | # To customize properties used by the Ant build system edit
7 | # "ant.properties", and override values to adapt the script to your
8 | # project structure.
9 | #
10 | # To enable ProGuard to shrink and obfuscate your code, uncomment this (available properties: sdk.dir, user.home):
11 | #proguard.config=${sdk.dir}/tools/proguard/proguard-android.txt:proguard-project.txt
12 |
13 | # Project target.
14 | target=android-17
15 |
--------------------------------------------------------------------------------
/demo/res/drawable-hdpi/ic_launcher.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/chrisjenx/StaggeredGridView/1016867787ff374b93edd85d3e17257e39244eec/demo/res/drawable-hdpi/ic_launcher.png
--------------------------------------------------------------------------------
/demo/res/drawable-mdpi/ic_launcher.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/chrisjenx/StaggeredGridView/1016867787ff374b93edd85d3e17257e39244eec/demo/res/drawable-mdpi/ic_launcher.png
--------------------------------------------------------------------------------
/demo/res/drawable-xhdpi/ic_launcher.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/chrisjenx/StaggeredGridView/1016867787ff374b93edd85d3e17257e39244eec/demo/res/drawable-xhdpi/ic_launcher.png
--------------------------------------------------------------------------------
/demo/res/drawable/bg_white_box.xml:
--------------------------------------------------------------------------------
1 |
2 |
4 |
5 |
6 |
7 |
10 |
11 |
--------------------------------------------------------------------------------
/demo/res/layout/activity_main.xml:
--------------------------------------------------------------------------------
1 |
6 |
7 |
11 |
12 |
--------------------------------------------------------------------------------
/demo/res/layout/element_header.xml:
--------------------------------------------------------------------------------
1 |
2 |
8 |
9 |
15 |
16 |
21 |
22 |
--------------------------------------------------------------------------------
/demo/res/layout/element_item.xml:
--------------------------------------------------------------------------------
1 |
2 |
8 |
9 |
14 |
15 |
25 |
26 |
--------------------------------------------------------------------------------
/demo/res/layout/element_item_large.xml:
--------------------------------------------------------------------------------
1 |
2 |
8 |
9 |
14 |
15 |
25 |
26 |
--------------------------------------------------------------------------------
/demo/res/layout/element_item_small.xml:
--------------------------------------------------------------------------------
1 |
2 |
8 |
9 |
14 |
15 |
25 |
26 |
--------------------------------------------------------------------------------
/demo/res/menu/activity_main.xml:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/demo/res/values-v11/styles.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
7 |
10 |
11 |
--------------------------------------------------------------------------------
/demo/res/values-v14/styles.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
8 |
11 |
12 |
--------------------------------------------------------------------------------
/demo/res/values/strings.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | StaggeredGridView
5 | Hello world!
6 | Settings
7 |
8 |
--------------------------------------------------------------------------------
/demo/res/values/styles.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
7 |
14 |
15 |
16 |
19 |
20 |
--------------------------------------------------------------------------------
/demo/src/android/support/v4/widget/StaggeredGridView.java:
--------------------------------------------------------------------------------
1 | /*
2 | * Copyright (C) 2012 The Android Open Source Project
3 | *
4 | * Licensed under the Apache License, Version 2.0 (the "License");
5 | * you may not use this file except in compliance with the License.
6 | * You may obtain a copy of the License at
7 | *
8 | * http://www.apache.org/licenses/LICENSE-2.0
9 | *
10 | * Unless required by applicable law or agreed to in writing, software
11 | * distributed under the License is distributed on an "AS IS" BASIS,
12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 | * See the License for the specific language governing permissions and
14 | * limitations under the License.
15 | */
16 |
17 | package android.support.v4.widget;
18 |
19 | import java.util.ArrayList;
20 | import java.util.Arrays;
21 |
22 | import android.content.Context;
23 | import android.content.res.TypedArray;
24 | import android.database.DataSetObserver;
25 | import android.graphics.Canvas;
26 | import android.os.Parcel;
27 | import android.os.Parcelable;
28 | import android.support.v4.util.SparseArrayCompat;
29 | import android.support.v4.view.MotionEventCompat;
30 | import android.support.v4.view.VelocityTrackerCompat;
31 | import android.support.v4.view.ViewCompat;
32 | import android.util.AttributeSet;
33 | import android.util.Log;
34 | import android.util.SparseArray;
35 | import android.view.MotionEvent;
36 | import android.view.VelocityTracker;
37 | import android.view.View;
38 | import android.view.ViewConfiguration;
39 | import android.view.ViewGroup;
40 | import android.widget.ListAdapter;
41 |
42 | /**
43 | * ListView and GridView just not complex enough? Try StaggeredGridView!
44 | *
45 | *
46 | * StaggeredGridView presents a multi-column grid with consistent column sizes
47 | * but varying row sizes between the columns. Each successive item from a
48 | * {@link android.widget.ListAdapter ListAdapter} will be arranged from top to
49 | * bottom, left to right. The largest vertical gap is always filled first.
50 | *
51 | *
52 | *
53 | * Item views may span multiple columns as specified by their
54 | * {@link LayoutParams}. The attribute android:layout_span may be
55 | * used when inflating item views from xml.
56 | *
57 | */
58 | public class StaggeredGridView extends ViewGroup
59 | {
60 | private static final String TAG = "StaggeredGridView";
61 | private static final boolean DEBUG = true;
62 |
63 | /**
64 | * There are a few things you should know if you're going to make
65 | * modifications to StaggeredGridView.
66 | *
67 | * Like ListView, SGV populates from an adapter and recycles views that fall
68 | * out of the visible boundaries of the grid. A few invariants always hold:
69 | *
70 | * - mFirstPosition is the adapter position of the View returned by
71 | * getChildAt(0).
72 | * - Any child index can be translated to an adapter position by adding
73 | * mFirstPosition.
74 | * - Any adapter position can be translated to a child index by subtracting
75 | * mFirstPosition.
76 | * - Views for items in the range [mFirstPosition, mFirstPosition +
77 | * getChildCount()) are currently attached to the grid as children. All
78 | * other adapter positions do not have active views.
79 | *
80 | * This means a few things thanks to the staggered grid's nature. Some views
81 | * may stay attached long after they have scrolled offscreen if removing and
82 | * recycling them would result in breaking one of the invariants above.
83 | *
84 | * LayoutRecords are used to track data about a particular item's layout
85 | * after the associated view has been removed. These let positioning and the
86 | * choice of column for an item remain consistent even though the rules for
87 | * filling content up vs. filling down vary.
88 | *
89 | * Whenever layout parameters for a known LayoutRecord change, other
90 | * LayoutRecords before or after it may need to be invalidated. e.g. if the
91 | * item's height or the number of columns it spans changes, all bets for
92 | * other items in the same direction are off since the cached information no
93 | * longer applies.
94 | */
95 |
96 | private ListAdapter mAdapter;
97 |
98 | public static final int COLUMN_COUNT_AUTO = -1;
99 |
100 | private int mColCountSetting = 2;
101 | private int mColCount = 2;
102 | private int mMinColWidth = 0;
103 | private int mItemMargin;
104 |
105 | private int[] mItemTops;
106 | private int[] mItemBottoms;
107 |
108 | private boolean mFastChildLayout;
109 | private boolean mPopulating;
110 | private boolean mForcePopulateOnLayout;
111 | private boolean mInLayout;
112 | private int mRestoreOffset;
113 |
114 | private final RecycleBin mRecycler = new RecycleBin();
115 |
116 | private final AdapterDataSetObserver mObserver = new AdapterDataSetObserver();
117 |
118 | private boolean mDataChanged;
119 | private int mOldItemCount;
120 | private int mItemCount;
121 | private boolean mHasStableIds;
122 |
123 | private int mFirstPosition;
124 |
125 | private final int mTouchSlop;
126 | private final int mMaximumVelocity;
127 | private final int mFlingVelocity;
128 | private float mLastTouchY;
129 | private float mTouchRemainderY;
130 | private int mActivePointerId;
131 |
132 | private static final int TOUCH_MODE_IDLE = 0;
133 | private static final int TOUCH_MODE_DRAGGING = 1;
134 | private static final int TOUCH_MODE_FLINGING = 2;
135 |
136 | private int mTouchMode;
137 | private final VelocityTracker mVelocityTracker = VelocityTracker.obtain();
138 | private final ScrollerCompat mScroller;
139 |
140 | private final EdgeEffectCompat mTopEdge;
141 | private final EdgeEffectCompat mBottomEdge;
142 | private OnScrollListener mOnScrollListener;
143 |
144 | private static final class LayoutRecord
145 | {
146 | public int column;
147 | public long id = -1;
148 | public int height;
149 | public int span;
150 | private int[] mMargins;
151 |
152 | private final void ensureMargins()
153 | {
154 | if (mMargins == null)
155 | {
156 | // Don't need to confirm length;
157 | // all layoutrecords are purged when column count changes.
158 | mMargins = new int[span * 2];
159 | }
160 | }
161 |
162 | public final int getMarginAbove(int col)
163 | {
164 | if (mMargins == null)
165 | {
166 | return 0;
167 | }
168 | return mMargins[col * 2];
169 | }
170 |
171 | public final int getMarginBelow(int col)
172 | {
173 | if (mMargins == null)
174 | {
175 | return 0;
176 | }
177 | return mMargins[col * 2 + 1];
178 | }
179 |
180 | public final void setMarginAbove(int col, int margin)
181 | {
182 | if (mMargins == null && margin == 0)
183 | {
184 | return;
185 | }
186 | ensureMargins();
187 | mMargins[col * 2] = margin;
188 | }
189 |
190 | public final void setMarginBelow(int col, int margin)
191 | {
192 | if (mMargins == null && margin == 0)
193 | {
194 | return;
195 | }
196 | ensureMargins();
197 | mMargins[col * 2 + 1] = margin;
198 | }
199 |
200 | @Override
201 | public String toString()
202 | {
203 | String result = "LayoutRecord{c=" + column + ", id=" + id + " h=" + height + " s="
204 | + span;
205 | if (mMargins != null)
206 | {
207 | result += " margins[above, below](";
208 | for (int i = 0; i < mMargins.length; i += 2)
209 | {
210 | result += "[" + mMargins[i] + ", " + mMargins[i + 1] + "]";
211 | }
212 | result += ")";
213 | }
214 | return result + "}";
215 | }
216 | }
217 |
218 | private final SparseArrayCompat mLayoutRecords = new SparseArrayCompat();
219 |
220 | public StaggeredGridView(Context context)
221 | {
222 | this(context, null);
223 | }
224 |
225 | public StaggeredGridView(Context context, AttributeSet attrs)
226 | {
227 | this(context, attrs, 0);
228 | }
229 |
230 | public StaggeredGridView(Context context, AttributeSet attrs, int defStyle)
231 | {
232 | super(context, attrs, defStyle);
233 |
234 | final ViewConfiguration vc = ViewConfiguration.get(context);
235 | mTouchSlop = vc.getScaledTouchSlop();
236 | mMaximumVelocity = vc.getScaledMaximumFlingVelocity();
237 | mFlingVelocity = vc.getScaledMinimumFlingVelocity();
238 | mScroller = ScrollerCompat.from(context);
239 |
240 | mTopEdge = new EdgeEffectCompat(context);
241 | mBottomEdge = new EdgeEffectCompat(context);
242 | setWillNotDraw(false);
243 | setClipToPadding(false);
244 | }
245 |
246 | /**
247 | * Set a fixed number of columns for this grid. Space will be divided evenly
248 | * among all columns, respecting the item margin between columns. The
249 | * default is 2. (If it were 1, perhaps you should be using a
250 | * {@link android.widget.ListView ListView}.)
251 | *
252 | * @param colCount
253 | * Number of columns to display.
254 | * @see #setMinColumnWidth(int)
255 | */
256 | public void setColumnCount(int colCount)
257 | {
258 | if (colCount < 1 && colCount != COLUMN_COUNT_AUTO)
259 | {
260 | throw new IllegalArgumentException("Column count must be at least 1 - received "
261 | + colCount);
262 | }
263 | final boolean needsPopulate = colCount != mColCount;
264 | mColCount = mColCountSetting = colCount;
265 | if (needsPopulate)
266 | {
267 | populate();
268 | }
269 | }
270 |
271 | public int getColumnCount()
272 | {
273 | return mColCount;
274 | }
275 |
276 | /**
277 | * Set a minimum column width for
278 | *
279 | * @param minColWidth
280 | */
281 | public void setMinColumnWidth(int minColWidth)
282 | {
283 | mMinColWidth = minColWidth;
284 | setColumnCount(COLUMN_COUNT_AUTO);
285 | }
286 |
287 | /**
288 | * Set the margin between items in pixels. This margin is applied both
289 | * vertically and horizontally.
290 | *
291 | * @param marginPixels
292 | * Spacing between items in pixels
293 | */
294 | public void setItemMargin(int marginPixels)
295 | {
296 | final boolean needsPopulate = marginPixels != mItemMargin;
297 | mItemMargin = marginPixels;
298 | if (needsPopulate)
299 | {
300 | populate();
301 | }
302 | }
303 |
304 | /**
305 | * Return the first adapter position with a view currently attached as a
306 | * child view of this grid.
307 | *
308 | * @return the adapter position represented by the view at getChildAt(0).
309 | */
310 | public int getFirstPosition()
311 | {
312 | return mFirstPosition;
313 | }
314 |
315 | /**
316 | * @return the total number of items in the grid displayed or not
317 | */
318 | public int getItemCount()
319 | {
320 | return mItemCount;
321 | }
322 |
323 | @Override
324 | public boolean onInterceptTouchEvent(MotionEvent ev)
325 | {
326 | mVelocityTracker.addMovement(ev);
327 | final int action = ev.getAction() & MotionEventCompat.ACTION_MASK;
328 | switch (action)
329 | {
330 | case MotionEvent.ACTION_DOWN:
331 | mVelocityTracker.clear();
332 | mScroller.abortAnimation();
333 | mLastTouchY = ev.getY();
334 | mActivePointerId = MotionEventCompat.getPointerId(ev, 0);
335 | mTouchRemainderY = 0;
336 | if (mTouchMode == TOUCH_MODE_FLINGING)
337 | {
338 | // Catch!
339 | // Stopped a fling. It is a scroll.
340 | setTouchMode(TOUCH_MODE_DRAGGING);
341 |
342 | return true;
343 | }
344 | break;
345 |
346 | case MotionEvent.ACTION_MOVE:
347 | {
348 | final int index = MotionEventCompat.findPointerIndex(ev, mActivePointerId);
349 | if (index < 0)
350 | {
351 | Log.e(TAG, "onInterceptTouchEvent could not find pointer with id "
352 | + mActivePointerId
353 | + " - did StaggeredGridView receive an inconsistent " + "event stream?");
354 | return false;
355 | }
356 | final float y = MotionEventCompat.getY(ev, index);
357 | final float dy = y - mLastTouchY + mTouchRemainderY;
358 | final int deltaY = (int) dy;
359 | mTouchRemainderY = dy - deltaY;
360 |
361 | if (Math.abs(dy) > mTouchSlop)
362 | {
363 | setTouchMode(TOUCH_MODE_DRAGGING);
364 | return true;
365 | }
366 | }
367 | }
368 |
369 | return false;
370 | }
371 |
372 | public void setTouchMode(int newState){
373 | // mTouchMode = newState;
374 | switch (newState) {
375 | case TOUCH_MODE_DRAGGING:
376 | reportScrollStateChange(OnScrollListener.SCROLL_STATE_TOUCH_SCROLL);
377 | break;
378 | case TOUCH_MODE_FLINGING:
379 | reportScrollStateChange(OnScrollListener.SCROLL_STATE_FLING);
380 | break;
381 | case TOUCH_MODE_IDLE:
382 | reportScrollStateChange(OnScrollListener.SCROLL_STATE_IDLE);
383 | break;
384 | }
385 | }
386 |
387 | @Override
388 | public boolean onTouchEvent(MotionEvent ev)
389 | {
390 | mVelocityTracker.addMovement(ev);
391 | final int action = ev.getAction() & MotionEventCompat.ACTION_MASK;
392 | switch (action)
393 | {
394 | case MotionEvent.ACTION_DOWN:
395 | mVelocityTracker.clear();
396 | mScroller.abortAnimation();
397 | mLastTouchY = ev.getY();
398 | mActivePointerId = MotionEventCompat.getPointerId(ev, 0);
399 | mTouchRemainderY = 0;
400 | break;
401 |
402 | case MotionEvent.ACTION_MOVE:
403 | {
404 | final int index = MotionEventCompat.findPointerIndex(ev, mActivePointerId);
405 | if (index < 0)
406 | {
407 | Log.e(TAG, "onInterceptTouchEvent could not find pointer with id "
408 | + mActivePointerId
409 | + " - did StaggeredGridView receive an inconsistent " + "event stream?");
410 | return false;
411 | }
412 | final float y = MotionEventCompat.getY(ev, index);
413 | final float dy = y - mLastTouchY + mTouchRemainderY;
414 | final int deltaY = (int) dy;
415 | mTouchRemainderY = dy - deltaY;
416 |
417 | if (Math.abs(dy) > mTouchSlop)
418 | {
419 | setTouchMode(TOUCH_MODE_DRAGGING);
420 | }
421 |
422 | if (mTouchMode == TOUCH_MODE_DRAGGING)
423 | {
424 | mLastTouchY = y;
425 |
426 | if (!trackMotionScroll(deltaY, true))
427 | {
428 | // Break fling velocity if we impacted an edge.
429 | mVelocityTracker.clear();
430 | }
431 | }
432 | }
433 | break;
434 |
435 | case MotionEvent.ACTION_CANCEL:
436 | setTouchMode(TOUCH_MODE_IDLE);
437 | break;
438 |
439 | case MotionEvent.ACTION_UP:
440 | {
441 | mVelocityTracker.computeCurrentVelocity(1000, mMaximumVelocity);
442 | final float velocity = VelocityTrackerCompat.getYVelocity(mVelocityTracker,
443 | mActivePointerId);
444 | if (Math.abs(velocity) > mFlingVelocity)
445 | { // TODO
446 | setTouchMode(TOUCH_MODE_FLINGING);
447 | mScroller.fling(0, 0, 0, (int) velocity, 0, 0, Integer.MIN_VALUE, Integer.MAX_VALUE);
448 | mLastTouchY = 0;
449 | ViewCompat.postInvalidateOnAnimation(this);
450 | }
451 | else
452 | {
453 | setTouchMode(TOUCH_MODE_IDLE);
454 | }
455 |
456 | }
457 | break;
458 | }
459 | return true;
460 | }
461 |
462 | /**
463 | *
464 | * @param deltaY
465 | * Pixels that content should move by
466 | * @return true if the movement completed, false if it was stopped
467 | * prematurely.
468 | */
469 | private boolean trackMotionScroll(int deltaY, boolean allowOverScroll)
470 | {
471 | final boolean contentFits = contentFits();
472 | final int allowOverhang = Math.abs(deltaY);
473 |
474 | final int overScrolledBy;
475 | final int movedBy;
476 | if (!contentFits)
477 | {
478 | final int overhang;
479 | final boolean up;
480 | mPopulating = true;
481 | if (deltaY > 0)
482 | {
483 | overhang = fillUp(mFirstPosition - 1, allowOverhang);
484 | up = true;
485 | }
486 | else
487 | {
488 | overhang = fillDown(mFirstPosition + getChildCount(), allowOverhang) + mItemMargin;
489 | up = false;
490 | }
491 | movedBy = Math.min(overhang, allowOverhang);
492 | offsetChildren(up ? movedBy : -movedBy);
493 | recycleOffscreenViews();
494 | mPopulating = false;
495 | overScrolledBy = allowOverhang - overhang;
496 | }
497 | else
498 | {
499 | overScrolledBy = allowOverhang;
500 | movedBy = 0;
501 | }
502 |
503 | if (allowOverScroll)
504 | {
505 | final int overScrollMode = ViewCompat.getOverScrollMode(this);
506 |
507 | if (overScrollMode == ViewCompat.OVER_SCROLL_ALWAYS
508 | || (overScrollMode == ViewCompat.OVER_SCROLL_IF_CONTENT_SCROLLS && !contentFits))
509 | {
510 |
511 | if (overScrolledBy > 0)
512 | {
513 | EdgeEffectCompat edge = deltaY > 0 ? mTopEdge : mBottomEdge;
514 | edge.onPull((float) Math.abs(deltaY) / getHeight());
515 | ViewCompat.postInvalidateOnAnimation(this);
516 | }
517 | }
518 | }
519 |
520 | invokeOnItemScrollListener();
521 |
522 | return deltaY == 0 || movedBy != 0;
523 | }
524 |
525 | private final boolean contentFits()
526 | {
527 | if (mFirstPosition != 0 || getChildCount() != mItemCount)
528 | {
529 | return false;
530 | }
531 |
532 | int topmost = Integer.MAX_VALUE;
533 | int bottommost = Integer.MIN_VALUE;
534 | for (int i = 0; i < mColCount; i++)
535 | {
536 | if (mItemTops[i] < topmost)
537 | {
538 | topmost = mItemTops[i];
539 | }
540 | if (mItemBottoms[i] > bottommost)
541 | {
542 | bottommost = mItemBottoms[i];
543 | }
544 | }
545 |
546 | return topmost >= getPaddingTop() && bottommost <= getHeight() - getPaddingBottom();
547 | }
548 |
549 | private void recycleAllViews()
550 | {
551 | for (int i = 0; i < getChildCount(); i++)
552 | {
553 | mRecycler.addScrap(getChildAt(i));
554 | }
555 |
556 | if (mInLayout)
557 | {
558 | removeAllViewsInLayout();
559 | }
560 | else
561 | {
562 | removeAllViews();
563 | }
564 | }
565 |
566 | /**
567 | * Important: this method will leave offscreen views attached if they are
568 | * required to maintain the invariant that child view with index i is always
569 | * the view corresponding to position mFirstPosition + i.
570 | */
571 | private void recycleOffscreenViews()
572 | {
573 | final int height = getHeight();
574 | final int clearAbove = -mItemMargin;
575 | final int clearBelow = height + mItemMargin;
576 | for (int i = getChildCount() - 1; i >= 0; i--)
577 | {
578 | final View child = getChildAt(i);
579 | if (child.getTop() <= clearBelow)
580 | {
581 | // There may be other offscreen views, but we need to maintain
582 | // the invariant documented above.
583 | break;
584 | }
585 |
586 | if (mInLayout)
587 | {
588 | removeViewsInLayout(i, 1);
589 | }
590 | else
591 | {
592 | removeViewAt(i);
593 | }
594 |
595 | mRecycler.addScrap(child);
596 | }
597 |
598 | while (getChildCount() > 0)
599 | {
600 | final View child = getChildAt(0);
601 | if (child.getBottom() >= clearAbove)
602 | {
603 | // There may be other offscreen views, but we need to maintain
604 | // the invariant documented above.
605 | break;
606 | }
607 |
608 | if (mInLayout)
609 | {
610 | removeViewsInLayout(0, 1);
611 | }
612 | else
613 | {
614 | removeViewAt(0);
615 | }
616 |
617 | mRecycler.addScrap(child);
618 | mFirstPosition++;
619 | }
620 |
621 | final int childCount = getChildCount();
622 | if (childCount > 0)
623 | {
624 | // Repair the top and bottom column boundaries from the views we
625 | // still have
626 | Arrays.fill(mItemTops, Integer.MAX_VALUE);
627 | Arrays.fill(mItemBottoms, Integer.MIN_VALUE);
628 |
629 | for (int i = 0; i < childCount; i++)
630 | {
631 | final View child = getChildAt(i);
632 | final LayoutParams lp = (LayoutParams) child.getLayoutParams();
633 | final int top = child.getTop() - mItemMargin;
634 | final int bottom = child.getBottom();
635 | final LayoutRecord rec = mLayoutRecords.get(mFirstPosition + i);
636 |
637 | final int colEnd = lp.column + Math.min(mColCount, lp.span);
638 | for (int col = lp.column; col < colEnd; col++)
639 | {
640 | final int colTop = top - rec.getMarginAbove(col - lp.column);
641 | final int colBottom = bottom + rec.getMarginBelow(col - lp.column);
642 | if (colTop < mItemTops[col])
643 | {
644 | mItemTops[col] = colTop;
645 | }
646 | if (colBottom > mItemBottoms[col])
647 | {
648 | mItemBottoms[col] = colBottom;
649 | }
650 | }
651 | }
652 |
653 | for (int col = 0; col < mColCount; col++)
654 | {
655 | if (mItemTops[col] == Integer.MAX_VALUE)
656 | {
657 | // If one was untouched, both were.
658 | mItemTops[col] = 0;
659 | mItemBottoms[col] = 0;
660 | }
661 | }
662 | }
663 | }
664 |
665 | @Override
666 | public void computeScroll()
667 | {
668 | if (mScroller.computeScrollOffset())
669 | {
670 | final int y = mScroller.getCurrY();
671 | final int dy = (int) (y - mLastTouchY);
672 | mLastTouchY = y;
673 | final boolean stopped = !trackMotionScroll(dy, false);
674 |
675 | if (!stopped && !mScroller.isFinished())
676 | {
677 | invokeOnItemScrollListener();
678 | ViewCompat.postInvalidateOnAnimation(this);
679 | }
680 | else
681 | {
682 | if (stopped)
683 | {
684 | final int overScrollMode = ViewCompat.getOverScrollMode(this);
685 | if (overScrollMode != ViewCompat.OVER_SCROLL_NEVER)
686 | {
687 | final EdgeEffectCompat edge;
688 | if (dy > 0)
689 | {
690 | edge = mTopEdge;
691 | }
692 | else
693 | {
694 | edge = mBottomEdge;
695 | }
696 | edge.onAbsorb(Math.abs((int) mScroller.getCurrVelocity()));
697 | ViewCompat.postInvalidateOnAnimation(this);
698 | }
699 | mScroller.abortAnimation();
700 | }
701 | setTouchMode(TOUCH_MODE_IDLE);
702 | }
703 | }
704 | }
705 |
706 | @Override
707 | public void draw(Canvas canvas)
708 | {
709 | super.draw(canvas);
710 |
711 | if (mTopEdge != null)
712 | {
713 | boolean needsInvalidate = false;
714 | if (!mTopEdge.isFinished())
715 | {
716 | mTopEdge.draw(canvas);
717 | needsInvalidate = true;
718 | }
719 | if (!mBottomEdge.isFinished())
720 | {
721 | final int restoreCount = canvas.save();
722 | final int width = getWidth();
723 | canvas.translate(-width, getHeight());
724 | canvas.rotate(180, width, 0);
725 | mBottomEdge.draw(canvas);
726 | canvas.restoreToCount(restoreCount);
727 | needsInvalidate = true;
728 | }
729 |
730 | if (needsInvalidate)
731 | {
732 | ViewCompat.postInvalidateOnAnimation(this);
733 | }
734 | }
735 | }
736 |
737 | public void beginFastChildLayout()
738 | {
739 | mFastChildLayout = true;
740 | }
741 |
742 | public void endFastChildLayout()
743 | {
744 | mFastChildLayout = false;
745 | populate();
746 | }
747 |
748 | @Override
749 | public void requestLayout()
750 | {
751 | if (!mPopulating && !mFastChildLayout)
752 | {
753 | super.requestLayout();
754 | }
755 | }
756 |
757 | @Override
758 | protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec)
759 | {
760 | int widthMode = MeasureSpec.getMode(widthMeasureSpec);
761 | int heightMode = MeasureSpec.getMode(heightMeasureSpec);
762 | int widthSize = MeasureSpec.getSize(widthMeasureSpec);
763 | int heightSize = MeasureSpec.getSize(heightMeasureSpec);
764 |
765 | if (widthMode != MeasureSpec.EXACTLY)
766 | {
767 | Log.e(TAG, "onMeasure: must have an exact width or match_parent! "
768 | + "Using fallback spec of EXACTLY " + widthSize);
769 | widthMode = MeasureSpec.EXACTLY;
770 | }
771 | if (heightMode != MeasureSpec.EXACTLY)
772 | {
773 | Log.e(TAG, "onMeasure: must have an exact height or match_parent! "
774 | + "Using fallback spec of EXACTLY " + heightSize);
775 | heightMode = MeasureSpec.EXACTLY;
776 | }
777 |
778 | setMeasuredDimension(widthSize, heightSize);
779 |
780 | if (mColCountSetting == COLUMN_COUNT_AUTO)
781 | {
782 | final int colCount = widthSize / mMinColWidth;
783 | if (colCount != mColCount)
784 | {
785 | mColCount = colCount;
786 | mForcePopulateOnLayout = true;
787 | }
788 | }
789 | }
790 |
791 | @Override
792 | protected void onLayout(boolean changed, int l, int t, int r, int b)
793 | {
794 | mInLayout = true;
795 | populate();
796 | mInLayout = false;
797 | mForcePopulateOnLayout = false;
798 |
799 | final int width = r - l;
800 | final int height = b - t;
801 | mTopEdge.setSize(width, height);
802 | mBottomEdge.setSize(width, height);
803 | }
804 |
805 | private void populate()
806 | {
807 | if (getWidth() == 0 || getHeight() == 0)
808 | {
809 | return;
810 | }
811 |
812 | if (mColCount == COLUMN_COUNT_AUTO)
813 | {
814 | final int colCount = getWidth() / mMinColWidth;
815 | if (colCount != mColCount)
816 | {
817 | mColCount = colCount;
818 | }
819 | }
820 |
821 | final int colCount = mColCount;
822 | if (mItemTops == null || mItemTops.length != colCount)
823 | {
824 | mItemTops = new int[colCount];
825 | mItemBottoms = new int[colCount];
826 | final int top = getPaddingTop();
827 | final int offset = top + Math.min(mRestoreOffset, 0);
828 | Arrays.fill(mItemTops, offset);
829 | Arrays.fill(mItemBottoms, offset);
830 | mLayoutRecords.clear();
831 | if (mInLayout)
832 | {
833 | removeAllViewsInLayout();
834 | }
835 | else
836 | {
837 | removeAllViews();
838 | }
839 | mRestoreOffset = 0;
840 | }
841 |
842 | mPopulating = true;
843 | layoutChildren(mDataChanged);
844 | fillDown(mFirstPosition + getChildCount(), 0);
845 | fillUp(mFirstPosition - 1, 0);
846 | mPopulating = false;
847 | mDataChanged = false;
848 | }
849 |
850 | private void dumpItemPositions()
851 | {
852 | final int childCount = getChildCount();
853 | Log.d(TAG, "dumpItemPositions:");
854 | Log.d(TAG, " => Tops:");
855 | for (int i = 0; i < mColCount; i++)
856 | {
857 | Log.d(TAG, " => " + mItemTops[i]);
858 | boolean found = false;
859 | for (int j = 0; j < childCount; j++)
860 | {
861 | final View child = getChildAt(j);
862 | if (mItemTops[i] == child.getTop() - mItemMargin)
863 | {
864 | found = true;
865 | }
866 | }
867 | if (!found)
868 | {
869 | Log.d(TAG, "!!! No top item found for column " + i + " value " + mItemTops[i]);
870 | }
871 | }
872 | Log.d(TAG, " => Bottoms:");
873 | for (int i = 0; i < mColCount; i++)
874 | {
875 | Log.d(TAG, " => " + mItemBottoms[i]);
876 | boolean found = false;
877 | for (int j = 0; j < childCount; j++)
878 | {
879 | final View child = getChildAt(j);
880 | if (mItemBottoms[i] == child.getBottom())
881 | {
882 | found = true;
883 | }
884 | }
885 | if (!found)
886 | {
887 | Log.d(TAG, "!!! No bottom item found for column " + i + " value " + mItemBottoms[i]);
888 | }
889 | }
890 | }
891 |
892 | final void offsetChildren(int offset)
893 | {
894 | final int childCount = getChildCount();
895 | for (int i = 0; i < childCount; i++)
896 | {
897 | final View child = getChildAt(i);
898 | child.layout(child.getLeft(), child.getTop() + offset, child.getRight(),
899 | child.getBottom() + offset);
900 | }
901 |
902 | final int colCount = mColCount;
903 | for (int i = 0; i < colCount; i++)
904 | {
905 | mItemTops[i] += offset;
906 | mItemBottoms[i] += offset;
907 | }
908 | }
909 |
910 | /**
911 | * Measure and layout all currently visible children.
912 | *
913 | * @param queryAdapter
914 | * true to requery the adapter for view data
915 | */
916 | final void layoutChildren(boolean queryAdapter)
917 | {
918 | final int paddingLeft = getPaddingLeft();
919 | final int paddingRight = getPaddingRight();
920 | final int itemMargin = mItemMargin;
921 | final int colWidth = (getWidth() - paddingLeft - paddingRight - itemMargin
922 | * (mColCount - 1))
923 | / mColCount;
924 | int rebuildLayoutRecordsBefore = -1;
925 | int rebuildLayoutRecordsAfter = -1;
926 |
927 | Arrays.fill(mItemBottoms, Integer.MIN_VALUE);
928 |
929 | final int childCount = getChildCount();
930 | for (int i = 0; i < childCount; i++)
931 | {
932 | View child = getChildAt(i);
933 | LayoutParams lp = (LayoutParams) child.getLayoutParams();
934 | final int col = lp.column;
935 | final int position = mFirstPosition + i;
936 | final boolean needsLayout = queryAdapter || child.isLayoutRequested();
937 |
938 | if (queryAdapter)
939 | {
940 | View newView = obtainView(position, child);
941 | if (newView != child)
942 | {
943 | removeViewAt(i);
944 | addView(newView, i);
945 | child = newView;
946 | }
947 | lp = (LayoutParams) child.getLayoutParams(); // Might have
948 | // changed
949 | }
950 |
951 | final int span = Math.min(mColCount, lp.span);
952 | final int widthSize = colWidth * span + itemMargin * (span - 1);
953 |
954 | if (needsLayout)
955 | {
956 | final int widthSpec = MeasureSpec.makeMeasureSpec(widthSize, MeasureSpec.EXACTLY);
957 |
958 | final int heightSpec;
959 | if (lp.height == LayoutParams.WRAP_CONTENT)
960 | {
961 | heightSpec = MeasureSpec.makeMeasureSpec(0, MeasureSpec.UNSPECIFIED);
962 | }
963 | else
964 | {
965 | heightSpec = MeasureSpec.makeMeasureSpec(lp.height, MeasureSpec.EXACTLY);
966 | }
967 |
968 | child.measure(widthSpec, heightSpec);
969 | }
970 |
971 | int childTop = mItemBottoms[col] > Integer.MIN_VALUE ? mItemBottoms[col] + mItemMargin
972 | : child.getTop();
973 | if (span > 1)
974 | {
975 | int lowest = childTop;
976 | for (int j = col + 1; j < col + span; j++)
977 | {
978 | final int bottom = mItemBottoms[j] + mItemMargin;
979 | if (bottom > lowest)
980 | {
981 | lowest = bottom;
982 | }
983 | }
984 | childTop = lowest;
985 | }
986 | final int childHeight = child.getMeasuredHeight();
987 | final int childBottom = childTop + childHeight;
988 | final int childLeft = paddingLeft + col * (colWidth + itemMargin);
989 | final int childRight = childLeft + child.getMeasuredWidth();
990 | child.layout(childLeft, childTop, childRight, childBottom);
991 |
992 | for (int j = col; j < col + span; j++)
993 | {
994 | mItemBottoms[j] = childBottom;
995 | }
996 |
997 | final LayoutRecord rec = mLayoutRecords.get(position);
998 | if (rec != null && rec.height != childHeight)
999 | {
1000 | // Invalidate our layout records for everything before this.
1001 | rec.height = childHeight;
1002 | rebuildLayoutRecordsBefore = position;
1003 | }
1004 |
1005 | if (rec != null && rec.span != span)
1006 | {
1007 | // Invalidate our layout records for everything after this.
1008 | rec.span = span;
1009 | rebuildLayoutRecordsAfter = position;
1010 | }
1011 | }
1012 |
1013 | // Update mItemBottoms for any empty columns
1014 | for (int i = 0; i < mColCount; i++)
1015 | {
1016 | if (mItemBottoms[i] == Integer.MIN_VALUE)
1017 | {
1018 | mItemBottoms[i] = mItemTops[i];
1019 | }
1020 | }
1021 |
1022 | if (rebuildLayoutRecordsBefore >= 0 || rebuildLayoutRecordsAfter >= 0)
1023 | {
1024 | if (rebuildLayoutRecordsBefore >= 0)
1025 | {
1026 | invalidateLayoutRecordsBeforePosition(rebuildLayoutRecordsBefore);
1027 | }
1028 | if (rebuildLayoutRecordsAfter >= 0)
1029 | {
1030 | invalidateLayoutRecordsAfterPosition(rebuildLayoutRecordsAfter);
1031 | }
1032 | for (int i = 0; i < childCount; i++)
1033 | {
1034 | final int position = mFirstPosition + i;
1035 | final View child = getChildAt(i);
1036 | final LayoutParams lp = (LayoutParams) child.getLayoutParams();
1037 | LayoutRecord rec = mLayoutRecords.get(position);
1038 | if (rec == null)
1039 | {
1040 | rec = new LayoutRecord();
1041 | mLayoutRecords.put(position, rec);
1042 | }
1043 | rec.column = lp.column;
1044 | rec.height = child.getHeight();
1045 | rec.id = lp.id;
1046 | rec.span = Math.min(mColCount, lp.span);
1047 | }
1048 | }
1049 | }
1050 |
1051 | final void invalidateLayoutRecordsBeforePosition(int position)
1052 | {
1053 | int endAt = 0;
1054 | while (endAt < mLayoutRecords.size() && mLayoutRecords.keyAt(endAt) < position)
1055 | {
1056 | endAt++;
1057 | }
1058 | mLayoutRecords.removeAtRange(0, endAt);
1059 | }
1060 |
1061 | final void invalidateLayoutRecordsAfterPosition(int position)
1062 | {
1063 | int beginAt = mLayoutRecords.size() - 1;
1064 | while (beginAt >= 0 && mLayoutRecords.keyAt(beginAt) > position)
1065 | {
1066 | beginAt--;
1067 | }
1068 | beginAt++;
1069 | mLayoutRecords.removeAtRange(beginAt + 1, mLayoutRecords.size() - beginAt);
1070 | }
1071 |
1072 | /**
1073 | * Should be called with mPopulating set to true
1074 | *
1075 | * @param fromPosition
1076 | * Position to start filling from
1077 | * @param overhang
1078 | * the number of extra pixels to fill beyond the current top edge
1079 | * @return the max overhang beyond the beginning of the view of any added
1080 | * items at the top
1081 | */
1082 | final int fillUp(int fromPosition, int overhang)
1083 | {
1084 | final int paddingLeft = getPaddingLeft();
1085 | final int paddingRight = getPaddingRight();
1086 | final int itemMargin = mItemMargin;
1087 | final int colWidth = (getWidth() - paddingLeft - paddingRight - itemMargin
1088 | * (mColCount - 1))
1089 | / mColCount;
1090 | final int gridTop = getPaddingTop();
1091 | final int fillTo = gridTop - overhang;
1092 | int nextCol = getNextColumnUp();
1093 | int position = fromPosition;
1094 |
1095 | while (nextCol >= 0 && mItemTops[nextCol] > fillTo && position >= 0)
1096 | {
1097 | final View child = obtainView(position, null);
1098 | LayoutParams lp = (LayoutParams) child.getLayoutParams();
1099 |
1100 | if (child.getParent() != this)
1101 | {
1102 | if (mInLayout)
1103 | {
1104 | addViewInLayout(child, 0, lp);
1105 | }
1106 | else
1107 | {
1108 | addView(child, 0);
1109 | }
1110 | }
1111 |
1112 | final int span = Math.min(mColCount, lp.span);
1113 | final int widthSize = colWidth * span + itemMargin * (span - 1);
1114 | final int widthSpec = MeasureSpec.makeMeasureSpec(widthSize, MeasureSpec.EXACTLY);
1115 |
1116 | LayoutRecord rec;
1117 | if (span > 1)
1118 | {
1119 | rec = getNextRecordUp(position, span);
1120 | nextCol = rec.column;
1121 | }
1122 | else
1123 | {
1124 | rec = mLayoutRecords.get(position);
1125 | }
1126 |
1127 | boolean invalidateBefore = false;
1128 | if (rec == null)
1129 | {
1130 | rec = new LayoutRecord();
1131 | mLayoutRecords.put(position, rec);
1132 | rec.column = nextCol;
1133 | rec.span = span;
1134 | }
1135 | else if (span != rec.span)
1136 | {
1137 | rec.span = span;
1138 | rec.column = nextCol;
1139 | invalidateBefore = true;
1140 | }
1141 | else
1142 | {
1143 | nextCol = rec.column;
1144 | }
1145 |
1146 | if (mHasStableIds)
1147 | {
1148 | final long id = mAdapter.getItemId(position);
1149 | rec.id = id;
1150 | lp.id = id;
1151 | }
1152 |
1153 | lp.column = nextCol;
1154 |
1155 | final int heightSpec;
1156 | if (lp.height == LayoutParams.WRAP_CONTENT)
1157 | {
1158 | heightSpec = MeasureSpec.makeMeasureSpec(0, MeasureSpec.UNSPECIFIED);
1159 | }
1160 | else
1161 | {
1162 | heightSpec = MeasureSpec.makeMeasureSpec(lp.height, MeasureSpec.EXACTLY);
1163 | }
1164 | child.measure(widthSpec, heightSpec);
1165 |
1166 | final int childHeight = child.getMeasuredHeight();
1167 | if (invalidateBefore || (childHeight != rec.height && rec.height > 0))
1168 | {
1169 | invalidateLayoutRecordsBeforePosition(position);
1170 | }
1171 | rec.height = childHeight;
1172 |
1173 | final int startFrom;
1174 | if (span > 1)
1175 | {
1176 | int highest = mItemTops[nextCol];
1177 | for (int i = nextCol + 1; i < nextCol + span; i++)
1178 | {
1179 | final int top = mItemTops[i];
1180 | if (top < highest)
1181 | {
1182 | highest = top;
1183 | }
1184 | }
1185 | startFrom = highest;
1186 | }
1187 | else
1188 | {
1189 | startFrom = mItemTops[nextCol];
1190 | }
1191 | final int childBottom = startFrom;
1192 | final int childTop = childBottom - childHeight;
1193 | final int childLeft = paddingLeft + nextCol * (colWidth + itemMargin);
1194 | final int childRight = childLeft + child.getMeasuredWidth();
1195 | child.layout(childLeft, childTop, childRight, childBottom);
1196 |
1197 | for (int i = nextCol; i < nextCol + span; i++)
1198 | {
1199 | mItemTops[i] = childTop - rec.getMarginAbove(i - nextCol) - itemMargin;
1200 | }
1201 |
1202 | nextCol = getNextColumnUp();
1203 | mFirstPosition = position--;
1204 | }
1205 |
1206 | int highestView = getHeight();
1207 | for (int i = 0; i < mColCount; i++)
1208 | {
1209 | if (mItemTops[i] < highestView)
1210 | {
1211 | highestView = mItemTops[i];
1212 | }
1213 | }
1214 | return gridTop - highestView;
1215 | }
1216 |
1217 | /**
1218 | * Should be called with mPopulating set to true
1219 | *
1220 | * @param fromPosition
1221 | * Position to start filling from
1222 | * @param overhang
1223 | * the number of extra pixels to fill beyond the current bottom
1224 | * edge
1225 | * @return the max overhang beyond the end of the view of any added items at
1226 | * the bottom
1227 | */
1228 | final int fillDown(int fromPosition, int overhang)
1229 | {
1230 | final int paddingLeft = getPaddingLeft();
1231 | final int paddingRight = getPaddingRight();
1232 | final int itemMargin = mItemMargin;
1233 | final int colWidth = (getWidth() - paddingLeft - paddingRight - itemMargin
1234 | * (mColCount - 1))
1235 | / mColCount;
1236 | final int gridBottom = getHeight() - getPaddingBottom();
1237 | final int fillTo = gridBottom + overhang;
1238 | int nextCol = getNextColumnDown();
1239 | int position = fromPosition;
1240 |
1241 | while (nextCol >= 0 && mItemBottoms[nextCol] < fillTo && position < mItemCount)
1242 | {
1243 | final View child = obtainView(position, null);
1244 | LayoutParams lp = (LayoutParams) child.getLayoutParams();
1245 |
1246 | if (child.getParent() != this)
1247 | {
1248 | if (mInLayout)
1249 | {
1250 | addViewInLayout(child, -1, lp);
1251 | }
1252 | else
1253 | {
1254 | addView(child);
1255 | }
1256 | }
1257 |
1258 | final int span = Math.min(mColCount, lp.span);
1259 | final int widthSize = colWidth * span + itemMargin * (span - 1);
1260 | final int widthSpec = MeasureSpec.makeMeasureSpec(widthSize, MeasureSpec.EXACTLY);
1261 |
1262 | LayoutRecord rec;
1263 | if (span > 1)
1264 | {
1265 | rec = getNextRecordDown(position, span);
1266 | nextCol = rec.column;
1267 | }
1268 | else
1269 | {
1270 | rec = mLayoutRecords.get(position);
1271 | }
1272 |
1273 | boolean invalidateAfter = false;
1274 | if (rec == null)
1275 | {
1276 | rec = new LayoutRecord();
1277 | mLayoutRecords.put(position, rec);
1278 | rec.column = nextCol;
1279 | rec.span = span;
1280 | }
1281 | else if (span != rec.span)
1282 | {
1283 | rec.span = span;
1284 | rec.column = nextCol;
1285 | invalidateAfter = true;
1286 | }
1287 | else
1288 | {
1289 | nextCol = rec.column;
1290 | }
1291 |
1292 | if (mHasStableIds)
1293 | {
1294 | final long id = mAdapter.getItemId(position);
1295 | rec.id = id;
1296 | lp.id = id;
1297 | }
1298 |
1299 | lp.column = nextCol;
1300 |
1301 | final int heightSpec;
1302 | if (lp.height == LayoutParams.WRAP_CONTENT)
1303 | {
1304 | heightSpec = MeasureSpec.makeMeasureSpec(0, MeasureSpec.UNSPECIFIED);
1305 | }
1306 | else
1307 | {
1308 | heightSpec = MeasureSpec.makeMeasureSpec(lp.height, MeasureSpec.EXACTLY);
1309 | }
1310 | child.measure(widthSpec, heightSpec);
1311 |
1312 | final int childHeight = child.getMeasuredHeight();
1313 | if (invalidateAfter || (childHeight != rec.height && rec.height > 0))
1314 | {
1315 | invalidateLayoutRecordsAfterPosition(position);
1316 | }
1317 | rec.height = childHeight;
1318 |
1319 | final int startFrom;
1320 | if (span > 1)
1321 | {
1322 | int lowest = mItemBottoms[nextCol];
1323 | for (int i = nextCol + 1; i < nextCol + span; i++)
1324 | {
1325 | final int bottom = mItemBottoms[i];
1326 | if (bottom > lowest)
1327 | {
1328 | lowest = bottom;
1329 | }
1330 | }
1331 | startFrom = lowest;
1332 | }
1333 | else
1334 | {
1335 | startFrom = mItemBottoms[nextCol];
1336 | }
1337 | final int childTop = startFrom + itemMargin;
1338 | final int childBottom = childTop + childHeight;
1339 | final int childLeft = paddingLeft + nextCol * (colWidth + itemMargin);
1340 | final int childRight = childLeft + child.getMeasuredWidth();
1341 | child.layout(childLeft, childTop, childRight, childBottom);
1342 |
1343 | for (int i = nextCol; i < nextCol + span; i++)
1344 | {
1345 | mItemBottoms[i] = childBottom + rec.getMarginBelow(i - nextCol);
1346 | }
1347 |
1348 | nextCol = getNextColumnDown();
1349 | position++;
1350 | }
1351 |
1352 | int lowestView = 0;
1353 | for (int i = 0; i < mColCount; i++)
1354 | {
1355 | if (mItemBottoms[i] > lowestView)
1356 | {
1357 | lowestView = mItemBottoms[i];
1358 | }
1359 | }
1360 | return lowestView - gridBottom;
1361 | }
1362 |
1363 | /**
1364 | * @return column that the next view filling upwards should occupy. This is
1365 | * the bottom-most position available for a single-column item.
1366 | */
1367 | final int getNextColumnUp()
1368 | {
1369 | int result = -1;
1370 | int bottomMost = Integer.MIN_VALUE;
1371 |
1372 | final int colCount = mColCount;
1373 | for (int i = colCount - 1; i >= 0; i--)
1374 | {
1375 | final int top = mItemTops[i];
1376 | if (top > bottomMost)
1377 | {
1378 | bottomMost = top;
1379 | result = i;
1380 | }
1381 | }
1382 | return result;
1383 | }
1384 |
1385 | /**
1386 | * Return a LayoutRecord for the given position
1387 | *
1388 | * @param position
1389 | * @param span
1390 | * @return
1391 | */
1392 | final LayoutRecord getNextRecordUp(int position, int span)
1393 | {
1394 | LayoutRecord rec = mLayoutRecords.get(position);
1395 | if (rec == null)
1396 | {
1397 | rec = new LayoutRecord();
1398 | rec.span = span;
1399 | mLayoutRecords.put(position, rec);
1400 | }
1401 | else if (rec.span != span)
1402 | {
1403 | throw new IllegalStateException("Invalid LayoutRecord! Record had span=" + rec.span
1404 | + " but caller requested span=" + span + " for position=" + position);
1405 | }
1406 | int targetCol = -1;
1407 | int bottomMost = Integer.MIN_VALUE;
1408 |
1409 | final int colCount = mColCount;
1410 | for (int i = colCount - span; i >= 0; i--)
1411 | {
1412 | int top = Integer.MAX_VALUE;
1413 | for (int j = i; j < i + span; j++)
1414 | {
1415 | final int singleTop = mItemTops[j];
1416 | if (singleTop < top)
1417 | {
1418 | top = singleTop;
1419 | }
1420 | }
1421 | if (top > bottomMost)
1422 | {
1423 | bottomMost = top;
1424 | targetCol = i;
1425 | }
1426 | }
1427 |
1428 | rec.column = targetCol;
1429 |
1430 | for (int i = 0; i < span; i++)
1431 | {
1432 | rec.setMarginBelow(i, mItemTops[i + targetCol] - bottomMost);
1433 | }
1434 |
1435 | return rec;
1436 | }
1437 |
1438 | /**
1439 | * @return column that the next view filling downwards should occupy. This
1440 | * is the top-most position available.
1441 | */
1442 | final int getNextColumnDown()
1443 | {
1444 | int result = -1;
1445 | int topMost = Integer.MAX_VALUE;
1446 |
1447 | final int colCount = mColCount;
1448 | for (int i = 0; i < colCount; i++)
1449 | {
1450 | final int bottom = mItemBottoms[i];
1451 | if (bottom < topMost)
1452 | {
1453 | topMost = bottom;
1454 | result = i;
1455 | }
1456 | }
1457 | return result;
1458 | }
1459 |
1460 | final LayoutRecord getNextRecordDown(int position, int span)
1461 | {
1462 | LayoutRecord rec = mLayoutRecords.get(position);
1463 | if (rec == null)
1464 | {
1465 | rec = new LayoutRecord();
1466 | rec.span = span;
1467 | mLayoutRecords.put(position, rec);
1468 | }
1469 | else if (rec.span != span)
1470 | {
1471 | throw new IllegalStateException("Invalid LayoutRecord! Record had span=" + rec.span
1472 | + " but caller requested span=" + span + " for position=" + position);
1473 | }
1474 | int targetCol = -1;
1475 | int topMost = Integer.MAX_VALUE;
1476 |
1477 | final int colCount = mColCount;
1478 | for (int i = 0; i <= colCount - span; i++)
1479 | {
1480 | int bottom = Integer.MIN_VALUE;
1481 | for (int j = i; j < i + span; j++)
1482 | {
1483 | final int singleBottom = mItemBottoms[j];
1484 | if (singleBottom > bottom)
1485 | {
1486 | bottom = singleBottom;
1487 | }
1488 | }
1489 | if (bottom < topMost)
1490 | {
1491 | topMost = bottom;
1492 | targetCol = i;
1493 | }
1494 | }
1495 |
1496 | rec.column = targetCol;
1497 |
1498 | for (int i = 0; i < span; i++)
1499 | {
1500 | rec.setMarginAbove(i, topMost - mItemBottoms[i + targetCol]);
1501 | }
1502 |
1503 | return rec;
1504 | }
1505 |
1506 | /**
1507 | * Obtain a populated view from the adapter. If optScrap is non-null and is
1508 | * not reused it will be placed in the recycle bin.
1509 | *
1510 | * @param position
1511 | * position to get view for
1512 | * @param optScrap
1513 | * Optional scrap view; will be reused if possible
1514 | * @return A new view, a recycled view from mRecycler, or optScrap
1515 | */
1516 | final View obtainView(int position, View optScrap)
1517 | {
1518 | View view = mRecycler.getTransientStateView(position);
1519 | if (view != null)
1520 | {
1521 | return view;
1522 | }
1523 |
1524 | // Reuse optScrap if it's of the right type (and not null)
1525 | final int optType = optScrap != null ? ((LayoutParams) optScrap.getLayoutParams()).viewType
1526 | : -1;
1527 | final int positionViewType = mAdapter.getItemViewType(position);
1528 | final View scrap = optType == positionViewType ? optScrap : mRecycler
1529 | .getScrapView(positionViewType);
1530 |
1531 | view = mAdapter.getView(position, scrap, this);
1532 |
1533 | if (view != scrap && scrap != null)
1534 | {
1535 | // The adapter didn't use it; put it back.
1536 | mRecycler.addScrap(scrap);
1537 | }
1538 |
1539 | ViewGroup.LayoutParams lp = view.getLayoutParams();
1540 |
1541 | if (view.getParent() != this)
1542 | {
1543 | if (lp == null)
1544 | {
1545 | lp = generateDefaultLayoutParams();
1546 | }
1547 | else if (!checkLayoutParams(lp))
1548 | {
1549 | lp = generateLayoutParams(lp);
1550 | }
1551 | }
1552 |
1553 | final LayoutParams sglp = (LayoutParams) lp;
1554 | sglp.position = position;
1555 | sglp.viewType = positionViewType;
1556 |
1557 | return view;
1558 | }
1559 |
1560 | public ListAdapter getAdapter()
1561 | {
1562 | return mAdapter;
1563 | }
1564 |
1565 | public void setAdapter(ListAdapter adapter)
1566 | {
1567 | if (mAdapter != null)
1568 | {
1569 | mAdapter.unregisterDataSetObserver(mObserver);
1570 | }
1571 | // TODO: If the new adapter says that there are stable IDs, remove
1572 | // certain layout records
1573 | // and onscreen views if they have changed instead of removing all of
1574 | // the state here.
1575 | clearAllState();
1576 | mAdapter = adapter;
1577 | mDataChanged = true;
1578 | mOldItemCount = mItemCount = adapter != null ? adapter.getCount() : 0;
1579 | if (adapter != null)
1580 | {
1581 | adapter.registerDataSetObserver(mObserver);
1582 | mRecycler.setViewTypeCount(adapter.getViewTypeCount());
1583 | mHasStableIds = adapter.hasStableIds();
1584 | }
1585 | else
1586 | {
1587 | mHasStableIds = false;
1588 | }
1589 | populate();
1590 | }
1591 |
1592 | /**
1593 | * Clear all state because the grid will be used for a completely different
1594 | * set of data.
1595 | */
1596 | private void clearAllState()
1597 | {
1598 | // Clear all layout records and views
1599 | mLayoutRecords.clear();
1600 | removeAllViews();
1601 |
1602 | // Reset to the top of the grid
1603 | resetStateForGridTop();
1604 |
1605 | // Clear recycler because there could be different view types now
1606 | mRecycler.clear();
1607 | }
1608 |
1609 | /**
1610 | * Reset all internal state to be at the top of the grid.
1611 | */
1612 | private void resetStateForGridTop()
1613 | {
1614 | // Reset mItemTops and mItemBottoms
1615 | final int colCount = mColCount;
1616 | if (mItemTops == null || mItemTops.length != colCount)
1617 | {
1618 | mItemTops = new int[colCount];
1619 | mItemBottoms = new int[colCount];
1620 | }
1621 | final int top = getPaddingTop();
1622 | Arrays.fill(mItemTops, top);
1623 | Arrays.fill(mItemBottoms, top);
1624 |
1625 | // Reset the first visible position in the grid to be item 0
1626 | mFirstPosition = 0;
1627 | mRestoreOffset = 0;
1628 | }
1629 |
1630 | /**
1631 | * Scroll the list so the first visible position in the grid is the first
1632 | * item in the adapter.
1633 | */
1634 | public void setSelectionToTop()
1635 | {
1636 | // Clear out the views (but don't clear out the layout records or
1637 | // recycler because the data
1638 | // has not changed)
1639 | removeAllViews();
1640 |
1641 | // Reset to top of grid
1642 | resetStateForGridTop();
1643 |
1644 | // Start populating again
1645 | populate();
1646 | }
1647 |
1648 | @Override
1649 | protected LayoutParams generateDefaultLayoutParams()
1650 | {
1651 | return new LayoutParams(LayoutParams.WRAP_CONTENT);
1652 | }
1653 |
1654 | @Override
1655 | protected LayoutParams generateLayoutParams(ViewGroup.LayoutParams lp)
1656 | {
1657 | return new LayoutParams(lp);
1658 | }
1659 |
1660 | @Override
1661 | protected boolean checkLayoutParams(ViewGroup.LayoutParams lp)
1662 | {
1663 | return lp instanceof LayoutParams;
1664 | }
1665 |
1666 | @Override
1667 | public ViewGroup.LayoutParams generateLayoutParams(AttributeSet attrs)
1668 | {
1669 | return new LayoutParams(getContext(), attrs);
1670 | }
1671 |
1672 | @Override
1673 | public Parcelable onSaveInstanceState()
1674 | {
1675 | final Parcelable superState = super.onSaveInstanceState();
1676 | final SavedState ss = new SavedState(superState);
1677 | final int position = mFirstPosition;
1678 | ss.position = position;
1679 | if (position >= 0 && mAdapter != null && position < mAdapter.getCount())
1680 | {
1681 | ss.firstId = mAdapter.getItemId(position);
1682 | }
1683 | if (getChildCount() > 0)
1684 | {
1685 | ss.topOffset = getChildAt(0).getTop() - mItemMargin - getPaddingTop();
1686 | }
1687 | return ss;
1688 | }
1689 |
1690 | @Override
1691 | public void onRestoreInstanceState(Parcelable state)
1692 | {
1693 | SavedState ss = (SavedState) state;
1694 | super.onRestoreInstanceState(ss.getSuperState());
1695 | mDataChanged = true;
1696 | mFirstPosition = ss.position;
1697 | mRestoreOffset = ss.topOffset;
1698 | requestLayout();
1699 | }
1700 |
1701 | public static class LayoutParams extends ViewGroup.LayoutParams
1702 | {
1703 | private static final int[] LAYOUT_ATTRS = new int[] { android.R.attr.layout_span };
1704 |
1705 | private static final int SPAN_INDEX = 0;
1706 |
1707 | /**
1708 | * The number of columns this item should span
1709 | */
1710 | public int span = 1;
1711 |
1712 | /**
1713 | * Item position this view represents
1714 | */
1715 | int position;
1716 |
1717 | /**
1718 | * Type of this view as reported by the adapter
1719 | */
1720 | int viewType;
1721 |
1722 | /**
1723 | * The column this view is occupying
1724 | */
1725 | int column;
1726 |
1727 | /**
1728 | * The stable ID of the item this view displays
1729 | */
1730 | long id = -1;
1731 |
1732 | public LayoutParams(int height)
1733 | {
1734 | super(MATCH_PARENT, height);
1735 |
1736 | if (this.height == MATCH_PARENT)
1737 | {
1738 | Log.w(TAG, "Constructing LayoutParams with height FILL_PARENT - "
1739 | + "impossible! Falling back to WRAP_CONTENT");
1740 | this.height = WRAP_CONTENT;
1741 | }
1742 | }
1743 |
1744 | public LayoutParams(Context c, AttributeSet attrs)
1745 | {
1746 | super(c, attrs);
1747 |
1748 | if (this.width != MATCH_PARENT)
1749 | {
1750 | Log.w(TAG, "Inflation setting LayoutParams width to " + this.width
1751 | + " - must be MATCH_PARENT");
1752 | this.width = MATCH_PARENT;
1753 | }
1754 | if (this.height == MATCH_PARENT)
1755 | {
1756 | Log.w(TAG, "Inflation setting LayoutParams height to MATCH_PARENT - "
1757 | + "impossible! Falling back to WRAP_CONTENT");
1758 | this.height = WRAP_CONTENT;
1759 | }
1760 |
1761 | TypedArray a = c.obtainStyledAttributes(attrs, LAYOUT_ATTRS);
1762 | span = a.getInteger(SPAN_INDEX, 1);
1763 | a.recycle();
1764 | }
1765 |
1766 | public LayoutParams(ViewGroup.LayoutParams other)
1767 | {
1768 | super(other);
1769 |
1770 | if (this.width != MATCH_PARENT)
1771 | {
1772 | Log.w(TAG, "Constructing LayoutParams with width " + this.width
1773 | + " - must be MATCH_PARENT");
1774 | this.width = MATCH_PARENT;
1775 | }
1776 | if (this.height == MATCH_PARENT)
1777 | {
1778 | Log.w(TAG, "Constructing LayoutParams with height MATCH_PARENT - "
1779 | + "impossible! Falling back to WRAP_CONTENT");
1780 | this.height = WRAP_CONTENT;
1781 | }
1782 | }
1783 | }
1784 |
1785 | private class RecycleBin
1786 | {
1787 | private ArrayList[] mScrapViews;
1788 | private int mViewTypeCount;
1789 | private int mMaxScrap;
1790 |
1791 | private SparseArray mTransientStateViews;
1792 |
1793 | public void setViewTypeCount(int viewTypeCount)
1794 | {
1795 | if (viewTypeCount < 1)
1796 | {
1797 | throw new IllegalArgumentException("Must have at least one view type ("
1798 | + viewTypeCount + " types reported)");
1799 | }
1800 | if (viewTypeCount == mViewTypeCount)
1801 | {
1802 | return;
1803 | }
1804 |
1805 | ArrayList[] scrapViews = new ArrayList[viewTypeCount];
1806 | for (int i = 0; i < viewTypeCount; i++)
1807 | {
1808 | scrapViews[i] = new ArrayList();
1809 | }
1810 | mViewTypeCount = viewTypeCount;
1811 | mScrapViews = scrapViews;
1812 | }
1813 |
1814 | public void clear()
1815 | {
1816 | final int typeCount = mViewTypeCount;
1817 | for (int i = 0; i < typeCount; i++)
1818 | {
1819 | mScrapViews[i].clear();
1820 | }
1821 | if (mTransientStateViews != null)
1822 | {
1823 | mTransientStateViews.clear();
1824 | }
1825 | }
1826 |
1827 | public void clearTransientViews()
1828 | {
1829 | if (mTransientStateViews != null)
1830 | {
1831 | mTransientStateViews.clear();
1832 | }
1833 | }
1834 |
1835 | public void addScrap(View v)
1836 | {
1837 | final LayoutParams lp = (LayoutParams) v.getLayoutParams();
1838 | if (ViewCompat.hasTransientState(v))
1839 | {
1840 | if (mTransientStateViews == null)
1841 | {
1842 | mTransientStateViews = new SparseArray();
1843 | }
1844 | mTransientStateViews.put(lp.position, v);
1845 | return;
1846 | }
1847 |
1848 | final int childCount = getChildCount();
1849 | if (childCount > mMaxScrap)
1850 | {
1851 | mMaxScrap = childCount;
1852 | }
1853 |
1854 | ArrayList scrap = mScrapViews[lp.viewType];
1855 | if (scrap.size() < mMaxScrap)
1856 | {
1857 | scrap.add(v);
1858 | }
1859 | }
1860 |
1861 | public View getTransientStateView(int position)
1862 | {
1863 | if (mTransientStateViews == null)
1864 | {
1865 | return null;
1866 | }
1867 |
1868 | final View result = mTransientStateViews.get(position);
1869 | if (result != null)
1870 | {
1871 | mTransientStateViews.remove(position);
1872 | }
1873 | return result;
1874 | }
1875 |
1876 | public View getScrapView(int type)
1877 | {
1878 | ArrayList scrap = mScrapViews[type];
1879 | if (scrap.isEmpty())
1880 | {
1881 | return null;
1882 | }
1883 |
1884 | final int index = scrap.size() - 1;
1885 | final View result = scrap.get(index);
1886 | scrap.remove(index);
1887 | return result;
1888 | }
1889 | }
1890 |
1891 | private class AdapterDataSetObserver extends DataSetObserver
1892 | {
1893 | @Override
1894 | public void onChanged()
1895 | {
1896 | mDataChanged = true;
1897 | mOldItemCount = mItemCount;
1898 | mItemCount = mAdapter.getCount();
1899 |
1900 | // TODO: Consider matching these back up if we have stable IDs.
1901 | mRecycler.clearTransientViews();
1902 |
1903 | if (!mHasStableIds)
1904 | {
1905 | // Clear all layout records and recycle the views
1906 | mLayoutRecords.clear();
1907 | recycleAllViews();
1908 |
1909 | // Reset item bottoms to be equal to item tops
1910 | final int colCount = mColCount;
1911 | for (int i = 0; i < colCount; i++)
1912 | {
1913 | mItemBottoms[i] = mItemTops[i];
1914 | }
1915 | }
1916 |
1917 | // TODO: consider repopulating in a deferred runnable instead
1918 | // (so that successive changes may still be batched)
1919 | requestLayout();
1920 | }
1921 |
1922 | @Override
1923 | public void onInvalidated()
1924 | {
1925 | }
1926 | }
1927 |
1928 | static class SavedState extends BaseSavedState
1929 | {
1930 | long firstId = -1;
1931 | int position;
1932 | int topOffset;
1933 |
1934 | SavedState(Parcelable superState)
1935 | {
1936 | super(superState);
1937 | }
1938 |
1939 | private SavedState(Parcel in)
1940 | {
1941 | super(in);
1942 | firstId = in.readLong();
1943 | position = in.readInt();
1944 | topOffset = in.readInt();
1945 | }
1946 |
1947 | @Override
1948 | public void writeToParcel(Parcel out, int flags)
1949 | {
1950 | super.writeToParcel(out, flags);
1951 | out.writeLong(firstId);
1952 | out.writeInt(position);
1953 | out.writeInt(topOffset);
1954 | }
1955 |
1956 | @Override
1957 | public String toString()
1958 | {
1959 | return "StaggereGridView.SavedState{"
1960 | + Integer.toHexString(System.identityHashCode(this)) + " firstId=" + firstId
1961 | + " position=" + position + "}";
1962 | }
1963 |
1964 | public static final Parcelable.Creator CREATOR = new Parcelable.Creator()
1965 | {
1966 | @Override
1967 | public SavedState createFromParcel(Parcel in)
1968 | {
1969 | return new SavedState(in);
1970 | }
1971 |
1972 | @Override
1973 | public SavedState[] newArray(int size)
1974 | {
1975 | return new SavedState[size];
1976 | }
1977 | };
1978 | }
1979 |
1980 | public void setOnScrollListener(OnScrollListener scrollListener) {
1981 | mOnScrollListener = scrollListener;
1982 | invokeOnItemScrollListener();
1983 | }
1984 |
1985 | void reportScrollStateChange(int newState) {
1986 | if (newState != mTouchMode) {
1987 | mTouchMode = newState;
1988 | if (mOnScrollListener != null) {
1989 | mOnScrollListener.onScrollStateChanged(this, newState);
1990 | }
1991 | }
1992 | }
1993 |
1994 | void invokeOnItemScrollListener() {
1995 |
1996 | if (mOnScrollListener != null) {
1997 | mOnScrollListener.onScroll(this, mFirstPosition, getChildCount(), mItemCount);
1998 | }
1999 |
2000 | }
2001 |
2002 | /**
2003 | * Interface definition for a callback to be invoked when the list or grid
2004 | * has been scrolled.
2005 | */
2006 | public interface OnScrollListener {
2007 |
2008 | /**
2009 | * The view is not scrolling. Note navigating the list using the trackball counts as
2010 | * being in the idle state since these transitions are not animated.
2011 | */
2012 | public static int SCROLL_STATE_IDLE = 0;
2013 |
2014 | /**
2015 | * The user is scrolling using touch, and their finger is still on the screen
2016 | */
2017 | public static int SCROLL_STATE_TOUCH_SCROLL = 1;
2018 |
2019 | /**
2020 | * The user had previously been scrolling using touch and had performed a fling. The
2021 | * animation is now coasting to a stop
2022 | */
2023 | public static int SCROLL_STATE_FLING = 2;
2024 |
2025 | /**
2026 | * Callback method to be invoked while the list view or grid view is being scrolled. If the
2027 | * view is being scrolled, this method will be called before the next frame of the scroll is
2028 | * rendered. In particular, it will be called before any calls to
2029 | * {@link Adapter#getView(int, View, ViewGroup)}.
2030 | *
2031 | * @param view The view whose scroll state is being reported
2032 | *
2033 | * @param scrollState The current scroll state. One of {@link #SCROLL_STATE_IDLE},
2034 | * {@link #SCROLL_STATE_TOUCH_SCROLL} or {@link #SCROLL_STATE_IDLE}.
2035 | */
2036 | public void onScrollStateChanged(ViewGroup view, int scrollState);
2037 |
2038 | /**
2039 | * Callback method to be invoked when the list or grid has been scrolled. This will be
2040 | * called after the scroll has completed
2041 | * @param view The view whose scroll state is being reported
2042 | * @param firstVisibleItem the index of the first visible cell (ignore if
2043 | * visibleItemCount == 0)
2044 | * @param visibleItemCount the number of visible cells
2045 | * @param totalItemCount the number of items in the list adaptor
2046 | */
2047 | public void onScroll(ViewGroup view, int firstVisibleItem, int visibleItemCount, int totalItemCount);
2048 | }
2049 |
2050 |
2051 |
2052 | }
--------------------------------------------------------------------------------
/demo/src/com/mino/staggeredgridview/MainActivity.java:
--------------------------------------------------------------------------------
1 | package com.mino.staggeredgridview;
2 |
3 | import java.util.Random;
4 |
5 | import android.app.Activity;
6 | import android.content.Context;
7 | import android.os.Bundle;
8 | import android.support.v4.widget.StaggeredGridView;
9 | import android.support.v4.widget.StaggeredGridView.LayoutParams;
10 | import android.support.v4.widget.StaggeredGridView.OnScrollListener;
11 | import android.util.Log;
12 | import android.view.LayoutInflater;
13 | import android.view.Menu;
14 | import android.view.View;
15 | import android.view.ViewGroup;
16 | import android.widget.BaseAdapter;
17 |
18 | public class MainActivity extends Activity
19 | {
20 | private final static String TAG = MainActivity.class.getSimpleName();
21 | private StaggeredGridView mSGV;
22 | private SGVAdapter mAdapter;
23 |
24 | private OnScrollListener mScrollListener = new OnScrollListener() {
25 |
26 | @Override
27 | public void onScrollStateChanged(ViewGroup view, int scrollState) {
28 | Log.d(TAG, "[onScrollStateChanged] scrollState:" + scrollState);
29 | switch (scrollState) {
30 | case SCROLL_STATE_IDLE:
31 | setTitle("SCROLL_STATE_IDLE");
32 | break;
33 |
34 | case SCROLL_STATE_FLING:
35 | setTitle("SCROLL_STATE_FLING");
36 | break;
37 |
38 | case SCROLL_STATE_TOUCH_SCROLL:
39 | setTitle("SCROLL_STATE_TOUCH_SCROLL");
40 | break;
41 |
42 | default:
43 | break;
44 | }
45 |
46 | }
47 |
48 | @Override
49 | public void onScroll(ViewGroup view, int firstVisibleItem, int visibleItemCount, int totalItemCount) {
50 | Log.d(TAG, "[onScroll] firstVisibleItem:" + firstVisibleItem + " visibleItemCount:"+visibleItemCount + " totalItemCount:" + totalItemCount);
51 |
52 | }
53 | };
54 |
55 | @Override
56 | protected void onCreate(Bundle savedInstanceState)
57 | {
58 | super.onCreate(savedInstanceState);
59 | setContentView(R.layout.activity_main);
60 |
61 | mAdapter = new SGVAdapter(this);
62 | mSGV = (StaggeredGridView) findViewById(R.id.grid);
63 | mSGV.setColumnCount(4);
64 | mSGV.setAdapter(mAdapter);
65 | mSGV.setOnScrollListener(mScrollListener);
66 | }
67 |
68 | @Override
69 | public boolean onCreateOptionsMenu(Menu menu)
70 | {
71 | // Inflate the menu; this adds items to the action bar if it is present.
72 | // getMenuInflater().inflate(R.menu.activity_main, menu);
73 | return true;
74 | }
75 |
76 | private final class SGVAdapter extends BaseAdapter
77 | {
78 |
79 | LayoutInflater mInflater;
80 |
81 | public SGVAdapter(Context ctx)
82 | {
83 | mInflater = LayoutInflater.from(ctx);
84 | }
85 |
86 | @Override
87 | public int getCount()
88 | {
89 | return 30;
90 | }
91 |
92 | @Override
93 | public Object getItem(int position)
94 | {
95 | return null;
96 | }
97 |
98 | @Override
99 | public long getItemId(int position)
100 | {
101 | return 0;
102 | }
103 |
104 | Random r = new Random();
105 |
106 | @Override
107 | public View getView(int position, View convertView, ViewGroup parent)
108 | {
109 | final LayoutParams lp;
110 | final View v;
111 | switch (position)
112 | {
113 | case 0:
114 | case 29:
115 | v = mInflater.inflate(R.layout.element_header, parent, false);
116 | lp = new LayoutParams(v.getLayoutParams());
117 | lp.span = mSGV.getColumnCount();
118 | break;
119 | case 8:
120 | case 9:
121 | case 18:
122 | case 19:
123 | v = mInflater.inflate(R.layout.element_item_small, parent, false);
124 | lp = new LayoutParams(v.getLayoutParams());
125 | lp.span = 1;
126 | break;
127 | case 10:
128 | case 20:
129 | v = mInflater.inflate(R.layout.element_item_large, parent, false);
130 | lp = new LayoutParams(v.getLayoutParams());
131 | lp.span = 4;
132 | break;
133 | default:
134 | v = mInflater.inflate(R.layout.element_item, parent, false);
135 | lp = new LayoutParams(v.getLayoutParams());
136 | lp.span = 2;
137 | break;
138 | }
139 | v.setLayoutParams(lp);
140 | return v;
141 | }
142 | }
143 |
144 | }
145 |
--------------------------------------------------------------------------------