am f5ecf6ad: Add unit test for creating fake phone entries
* commit 'f5ecf6ad067fb1036199ad5125949aae7ffd9689':
Add unit test for creating fake phone entries
diff --git a/carousel/test/res/values-in/strings.xml b/carousel/test/res/values-in/strings.xml
index 40000e8..06c8812 100644
--- a/carousel/test/res/values-in/strings.xml
+++ b/carousel/test/res/values-in/strings.xml
@@ -21,7 +21,7 @@
xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
<string name="music_demo_activity_label" msgid="4382090808250495841">"KaruselMusik"</string>
<string name="carousel_test_activity_label" msgid="6014624482213318747">"UjiKarusel"</string>
- <string name="carousel_test_activity_description" msgid="1632693812604375483">"Aplikasi untuk menampilkan penggunaan Karusel"</string>
+ <string name="carousel_test_activity_description" msgid="1632693812604375483">"Aplikasi untuk menampilkan penggunaan Korsel"</string>
<string name="task_switcher_activity_label" msgid="714620143340933546">"PengubahTugas"</string>
<string name="recent_tasks_title" msgid="1030287226205477117">"Aplikasi Terbaru"</string>
<string name="no_recent_tasks" msgid="6884096266670555780">"Tidak ada tugas terbaru"</string>
diff --git a/chips/AndroidManifest.xml b/chips/AndroidManifest.xml
index e159fd2..fd7775d 100644
--- a/chips/AndroidManifest.xml
+++ b/chips/AndroidManifest.xml
@@ -1,5 +1,19 @@
<?xml version="1.0" encoding="utf-8"?>
+<!-- Copyright (C) 2011 The Android Open Source Project
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+-->
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
package="com.android.ex.chips"
android:versionCode="1">
-</manifest>
\ No newline at end of file
+</manifest>
diff --git a/chips/res/values-ko/strings.xml b/chips/res/values-ko/strings.xml
index 7423ce5..f7884bd 100644
--- a/chips/res/values-ko/strings.xml
+++ b/chips/res/values-ko/strings.xml
@@ -19,5 +19,5 @@
<string name="more_string" msgid="8495478259330621990">"<xliff:g id="COUNT">%1$s</xliff:g>명 이상"</string>
<string name="copy_email" msgid="7869435992461603532">"이메일 주소 복사"</string>
<string name="copy_number" msgid="530057841276106843">"전화번호 복사"</string>
- <string name="done" msgid="2356320650733788862">"Enter 키"</string>
+ <string name="done" msgid="2356320650733788862">"입력"</string>
</resources>
diff --git a/chips/res/values-pt-rPT/strings.xml b/chips/res/values-pt-rPT/strings.xml
index bfbe1ca..fc991b1 100644
--- a/chips/res/values-pt-rPT/strings.xml
+++ b/chips/res/values-pt-rPT/strings.xml
@@ -19,5 +19,5 @@
<string name="more_string" msgid="8495478259330621990">"+<xliff:g id="COUNT">%1$s</xliff:g>"</string>
<string name="copy_email" msgid="7869435992461603532">"Copiar endereço de email"</string>
<string name="copy_number" msgid="530057841276106843">"Copiar número de telefone"</string>
- <string name="done" msgid="2356320650733788862">"Regressar"</string>
+ <string name="done" msgid="2356320650733788862">"Voltar"</string>
</resources>
diff --git a/chips/src/com/android/ex/chips/RecipientEditTextView.java b/chips/src/com/android/ex/chips/RecipientEditTextView.java
index d7cd7fa..e40edf1 100644
--- a/chips/src/com/android/ex/chips/RecipientEditTextView.java
+++ b/chips/src/com/android/ex/chips/RecipientEditTextView.java
@@ -409,6 +409,44 @@
}
}
+ private int getExcessTopPadding() {
+ if (sExcessTopPadding == -1) {
+ sExcessTopPadding = (int) (mChipHeight + mLineSpacingExtra);
+ }
+ return sExcessTopPadding;
+ }
+
+ public <T extends ListAdapter & Filterable> void setAdapter(T adapter) {
+ super.setAdapter(adapter);
+ ((BaseRecipientAdapter) adapter)
+ .registerUpdateObserver(new BaseRecipientAdapter.EntriesUpdatedObserver() {
+ @Override
+ public void onChanged(List<RecipientEntry> entries) {
+ // Scroll the chips field to the top of the screen so
+ // that the user can see as many results as possible.
+ if (entries != null && entries.size() > 0) {
+ scrollBottomIntoView();
+ }
+ }
+ });
+ }
+
+ private void scrollBottomIntoView() {
+ if (mScrollView != null && mShouldShrink) {
+ int[] location = new int[2];
+ getLocationOnScreen(location);
+ int height = getHeight();
+ int currentPos = location[1] + height;
+ // Desired position shows at least 1 line of chips below the action
+ // bar. We add excess padding to make sure this is always below other
+ // content.
+ int desiredPos = (int) mChipHeight + mActionBarHeight + getExcessTopPadding();
+ if (currentPos > desiredPos) {
+ mScrollView.scrollBy(0, currentPos - desiredPos);
+ }
+ }
+ }
+
@Override
public void performValidation() {
// Do nothing. Chips handles its own validation.
@@ -2240,45 +2278,7 @@
}
}
- @Override
- public <T extends ListAdapter & Filterable> void setAdapter(T adapter) {
- super.setAdapter(adapter);
- ((BaseRecipientAdapter) adapter)
- .registerUpdateObserver(new BaseRecipientAdapter.EntriesUpdatedObserver() {
- @Override
- public void onChanged(List<RecipientEntry> entries) {
- if (entries != null && entries.size() > 0) {
- scrollBottomIntoView();
- }
- }
- });
- }
-
- private void scrollBottomIntoView() {
- if (mScrollView != null && mShouldShrink) {
- int[] location = new int[2];
- getLocationOnScreen(location);
- int height = getHeight();
- int currentPos = location[1] + height;
- // Desired position shows at least 1 line of chips below the action
- // bar.
- // We add excess padding to make sure this is always below other
- // content.
- int desiredPos = (int) mChipHeight + mActionBarHeight + getExcessTopPadding();
- if (currentPos > desiredPos) {
- mScrollView.scrollBy(0, currentPos - desiredPos);
- }
- }
- }
-
- private int getExcessTopPadding() {
- if (sExcessTopPadding == -1) {
- sExcessTopPadding = (int) (mChipHeight + mLineSpacingExtra);
- }
- return sExcessTopPadding;
- }
-
- public boolean lastCharacterIsCommitCharacter(CharSequence s) {
+ public boolean lastCharacterIsCommitCharacter(CharSequence s) {
char last;
int end = getSelectionEnd() == 0 ? 0 : getSelectionEnd() - 1;
int len = length() - 1;
@@ -2357,6 +2357,9 @@
prevTokenStart = tokenStart;
tokenStart = mTokenizer.findTokenStart(text, tokenStart);
findChip = findChip(tokenStart);
+ if (tokenStart == originalTokenStart && findChip == null) {
+ break;
+ }
}
if (tokenStart != originalTokenStart) {
if (findChip != null) {
diff --git a/common/java/com/android/common/OperationScheduler.java b/common/java/com/android/common/OperationScheduler.java
index b8fc7bc..261b15d 100644
--- a/common/java/com/android/common/OperationScheduler.java
+++ b/common/java/com/android/common/OperationScheduler.java
@@ -42,6 +42,9 @@
/** Wait this long times the number of consecutive errors so far before retrying. */
public long backoffIncrementalMillis = 5000;
+ /** Wait this long times 2^(number of consecutive errors so far) before retrying. */
+ public int backoffExponentialMillis = 0;
+
/** Maximum duration of moratorium to honor. Mostly an issue for clock rollbacks. */
public long maxMoratoriumMillis = 24 * 3600 * 1000;
@@ -53,11 +56,20 @@
@Override
public String toString() {
- return String.format(
+ if (backoffExponentialMillis > 0) {
+ return String.format(
+ "OperationScheduler.Options[backoff=%.1f+%.1f+%.1f max=%.1f min=%.1f period=%.1f]",
+ backoffFixedMillis / 1000.0, backoffIncrementalMillis / 1000.0,
+ backoffExponentialMillis / 1000.0,
+ maxMoratoriumMillis / 1000.0, minTriggerMillis / 1000.0,
+ periodicIntervalMillis / 1000.0);
+ } else {
+ return String.format(
"OperationScheduler.Options[backoff=%.1f+%.1f max=%.1f min=%.1f period=%.1f]",
backoffFixedMillis / 1000.0, backoffIncrementalMillis / 1000.0,
maxMoratoriumMillis / 1000.0, minTriggerMillis / 1000.0,
periodicIntervalMillis / 1000.0);
+ }
}
}
@@ -76,7 +88,7 @@
* Parse scheduler options supplied in this string form:
*
* <pre>
- * backoff=(fixed)+(incremental) max=(maxmoratorium) min=(mintrigger) [period=](interval)
+ * backoff=(fixed)+(incremental)[+(exponential)] max=(maxmoratorium) min=(mintrigger) [period=](interval)
* </pre>
*
* All values are times in (possibly fractional) <em>seconds</em> (not milliseconds).
@@ -97,14 +109,18 @@
for (String param : spec.split(" +")) {
if (param.length() == 0) continue;
if (param.startsWith("backoff=")) {
- int plus = param.indexOf('+', 8);
- if (plus < 0) {
- options.backoffFixedMillis = parseSeconds(param.substring(8));
- } else {
- if (plus > 8) {
- options.backoffFixedMillis = parseSeconds(param.substring(8, plus));
- }
- options.backoffIncrementalMillis = parseSeconds(param.substring(plus + 1));
+ String[] pieces = param.substring(8).split("\\+");
+ if (pieces.length > 3) {
+ throw new IllegalArgumentException("bad value for backoff: [" + spec + "]");
+ }
+ if (pieces.length > 0 && pieces[0].length() > 0) {
+ options.backoffFixedMillis = parseSeconds(pieces[0]);
+ }
+ if (pieces.length > 1 && pieces[1].length() > 0) {
+ options.backoffIncrementalMillis = parseSeconds(pieces[1]);
+ }
+ if (pieces.length > 2 && pieces[2].length() > 0) {
+ options.backoffExponentialMillis = (int)parseSeconds(pieces[2]);
}
} else if (param.startsWith("max=")) {
options.maxMoratoriumMillis = parseSeconds(param.substring(4));
@@ -160,8 +176,21 @@
time = Math.max(time, moratoriumTimeMillis);
time = Math.max(time, lastSuccessTimeMillis + options.minTriggerMillis);
if (errorCount > 0) {
- time = Math.max(time, lastErrorTimeMillis + options.backoffFixedMillis +
- options.backoffIncrementalMillis * errorCount);
+ int shift = errorCount-1;
+ // backoffExponentialMillis is an int, so we can safely
+ // double it 30 times without overflowing a long.
+ if (shift > 30) shift = 30;
+ long backoff = options.backoffFixedMillis +
+ (options.backoffIncrementalMillis * errorCount) +
+ (((long)options.backoffExponentialMillis) << shift);
+
+ // Treat backoff like a moratorium: don't let the backoff
+ // time grow too large.
+ if (moratoriumTimeMillis > 0 && backoff > moratoriumTimeMillis) {
+ backoff = moratoriumTimeMillis;
+ }
+
+ time = Math.max(time, lastErrorTimeMillis + backoff);
}
return time;
}
diff --git a/common/tests/src/com/android/common/OperationSchedulerTest.java b/common/tests/src/com/android/common/OperationSchedulerTest.java
index 955508f..87e2cd8 100644
--- a/common/tests/src/com/android/common/OperationSchedulerTest.java
+++ b/common/tests/src/com/android/common/OperationSchedulerTest.java
@@ -119,6 +119,42 @@
assertEquals(beforeSuccess + 1000000, scheduler.getNextTimeMillis(options));
}
+ @MediumTest
+ public void testExponentialBackoff() throws Exception {
+ TimeTravelScheduler scheduler = new TimeTravelScheduler();
+ OperationScheduler.Options options = new OperationScheduler.Options();
+ options.backoffFixedMillis = 100;
+ options.backoffIncrementalMillis = 1000;
+ options.backoffExponentialMillis = 10000;
+ scheduler.setTriggerTimeMillis(0);
+ scheduler.setEnabledState(true);
+
+ // Backoff interval after an error
+ long beforeError = (scheduler.timeMillis += 10);
+ scheduler.onTransientError();
+ assertEquals(0, scheduler.getLastSuccessTimeMillis());
+ assertEquals(beforeError, scheduler.getLastAttemptTimeMillis());
+ assertEquals(beforeError + 11100, scheduler.getNextTimeMillis(options));
+
+ // Second error
+ beforeError = (scheduler.timeMillis += 10);
+ scheduler.onTransientError();
+ assertEquals(beforeError, scheduler.getLastAttemptTimeMillis());
+ assertEquals(beforeError + 22100, scheduler.getNextTimeMillis(options));
+
+ // Third error
+ beforeError = (scheduler.timeMillis += 10);
+ scheduler.onTransientError();
+ assertEquals(beforeError, scheduler.getLastAttemptTimeMillis());
+ assertEquals(beforeError + 43100, scheduler.getNextTimeMillis(options));
+
+ // Fourth error
+ beforeError = (scheduler.timeMillis += 10);
+ scheduler.onTransientError();
+ assertEquals(beforeError, scheduler.getLastAttemptTimeMillis());
+ assertEquals(beforeError + 84100, scheduler.getNextTimeMillis(options));
+ }
+
@SmallTest
public void testParseOptions() throws Exception {
OperationScheduler.Options options = new OperationScheduler.Options();
@@ -138,6 +174,10 @@
assertEquals(
"OperationScheduler.Options[backoff=10.0+2.5 max=12345.6 min=7.0 period=3800.0]",
OperationScheduler.parseOptions("", options).toString());
+
+ assertEquals(
+ "OperationScheduler.Options[backoff=5.0+2.5+10.0 max=12345.6 min=7.0 period=3600.0]",
+ OperationScheduler.parseOptions("backoff=5.0++10.0 3600", options).toString());
}
@SmallTest
diff --git a/photoviewer/.gitignore b/photoviewer/.gitignore
new file mode 100644
index 0000000..ff7ef7d
--- /dev/null
+++ b/photoviewer/.gitignore
@@ -0,0 +1,8 @@
+*~
+*.bak
+*.class
+bin/
+gen/
+*.properties
+.classpath
+.project
diff --git a/variablespeed/jni/variablespeed.cc b/variablespeed/jni/variablespeed.cc
index ea134ec..73ac609 100644
--- a/variablespeed/jni/variablespeed.cc
+++ b/variablespeed/jni/variablespeed.cc
@@ -582,7 +582,7 @@
const size_t playerInterfaceCount = 2;
const SLInterfaceID iids[playerInterfaceCount] = {
SL_IID_ANDROIDSIMPLEBUFFERQUEUE, SL_IID_ANDROIDCONFIGURATION };
- const SLboolean reqs[playerInterfaceCount] = { SL_BOOLEAN_TRUE };
+ const SLboolean reqs[playerInterfaceCount] = { SL_BOOLEAN_TRUE, SL_BOOLEAN_TRUE };
OpenSL(engineInterface, CreateAudioPlayer, &audioPlayer, &playingSrc,
&audioSnk, playerInterfaceCount, iids, reqs);
setAudioStreamType(audioPlayer, audioStreamType);
@@ -619,7 +619,7 @@
SL_IID_ANDROIDSIMPLEBUFFERQUEUE, SL_IID_PREFETCHSTATUS, SL_IID_SEEK,
SL_IID_METADATAEXTRACTION, SL_IID_ANDROIDCONFIGURATION };
const SLboolean decodePlayerRequired[decoderInterfaceCount] = {
- SL_BOOLEAN_TRUE, SL_BOOLEAN_TRUE, SL_BOOLEAN_TRUE, SL_BOOLEAN_TRUE };
+ SL_BOOLEAN_TRUE, SL_BOOLEAN_TRUE, SL_BOOLEAN_TRUE, SL_BOOLEAN_TRUE, SL_BOOLEAN_TRUE };
SLDataSource sourceCopy(audioSrc);
OpenSL(engineInterface, CreateAudioPlayer, &decoder, &sourceCopy, &decDest,
decoderInterfaceCount, decodePlayerInterfaces, decodePlayerRequired);
diff --git a/widget/java/com/android/ex/widget/StaggeredGridView.java b/widget/java/com/android/ex/widget/StaggeredGridView.java
new file mode 100644
index 0000000..6b6b938
--- /dev/null
+++ b/widget/java/com/android/ex/widget/StaggeredGridView.java
@@ -0,0 +1,1621 @@
+/*
+ * Copyright (C) 2012 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.ex.widget;
+
+import android.content.Context;
+import android.content.res.TypedArray;
+import android.database.DataSetObserver;
+import android.graphics.Canvas;
+import android.os.Parcel;
+import android.os.Parcelable;
+import android.support.v4.util.SparseArrayCompat;
+import android.support.v4.view.MotionEventCompat;
+import android.support.v4.view.VelocityTrackerCompat;
+import android.support.v4.view.ViewCompat;
+import android.util.AttributeSet;
+import android.util.Log;
+import android.util.SparseArray;
+import android.view.MotionEvent;
+import android.view.VelocityTracker;
+import android.view.View;
+import android.view.ViewConfiguration;
+import android.view.ViewGroup;
+import android.widget.ListAdapter;
+
+import java.util.ArrayList;
+import java.util.Arrays;
+
+/**
+ * ListView and GridView just not complex enough? Try StaggeredGridView!
+ *
+ * <p>StaggeredGridView presents a multi-column grid with consistent column sizes
+ * but varying row sizes between the columns. Each successive item from a
+ * {@link android.widget.ListAdapter ListAdapter} will be arranged from top to bottom,
+ * left to right. The largest vertical gap is always filled first.</p>
+ *
+ * <p>Item views may span multiple columns as specified by their {@link LayoutParams}.
+ * The attribute <code>android:layout_span</code> may be used when inflating
+ * item views from xml.</p>
+ *
+ * <p>This class is still under development and is not fully functional yet.</p>
+ */
+public class StaggeredGridView extends ViewGroup {
+ private static final String TAG = "StaggeredGridView";
+ private static final boolean DEBUG = false;
+
+ /*
+ * There are a few things you should know if you're going to make modifications
+ * to StaggeredGridView.
+ *
+ * Like ListView, SGV populates from an adapter and recycles views that fall out
+ * of the visible boundaries of the grid. A few invariants always hold:
+ *
+ * - mFirstPosition is the adapter position of the View returned by getChildAt(0).
+ * - Any child index can be translated to an adapter position by adding mFirstPosition.
+ * - Any adapter position can be translated to a child index by subtracting mFirstPosition.
+ * - Views for items in the range [mFirstPosition, mFirstPosition + getChildCount()) are
+ * currently attached to the grid as children. All other adapter positions do not have
+ * active views.
+ *
+ * This means a few things thanks to the staggered grid's nature. Some views may stay attached
+ * long after they have scrolled offscreen if removing and recycling them would result in
+ * breaking one of the invariants above.
+ *
+ * LayoutRecords are used to track data about a particular item's layout after the associated
+ * view has been removed. These let positioning and the choice of column for an item
+ * remain consistent even though the rules for filling content up vs. filling down vary.
+ *
+ * Whenever layout parameters for a known LayoutRecord change, other LayoutRecords before
+ * or after it may need to be invalidated. e.g. if the item's height or the number
+ * of columns it spans changes, all bets for other items in the same direction are off
+ * since the cached information no longer applies.
+ */
+
+ private ListAdapter mAdapter;
+
+ public static final int COLUMN_COUNT_AUTO = -1;
+
+ private int mColCountSetting = 2;
+ private int mColCount = 2;
+ private int mMinColWidth = 0;
+ private int mItemMargin;
+
+ private int[] mItemTops;
+ private int[] mItemBottoms;
+
+ private boolean mFastChildLayout;
+ private boolean mPopulating;
+ private boolean mForcePopulateOnLayout;
+ private boolean mInLayout;
+ private int mRestoreOffset;
+
+ private final RecycleBin mRecycler = new RecycleBin();
+
+ private final AdapterDataSetObserver mObserver = new AdapterDataSetObserver();
+
+ private boolean mDataChanged;
+ private int mOldItemCount;
+ private int mItemCount;
+ private boolean mHasStableIds;
+
+ private int mFirstPosition;
+
+ private int mTouchSlop;
+ private int mMaximumVelocity;
+ private int mFlingVelocity;
+ private float mLastTouchY;
+ private float mTouchRemainderY;
+ private int mActivePointerId;
+
+ private static final int TOUCH_MODE_IDLE = 0;
+ private static final int TOUCH_MODE_DRAGGING = 1;
+ private static final int TOUCH_MODE_FLINGING = 2;
+
+ private int mTouchMode;
+ private final VelocityTracker mVelocityTracker = VelocityTracker.obtain();
+ private final ScrollerCompat mScroller;
+
+ private final EdgeEffectCompat mTopEdge;
+ private final EdgeEffectCompat mBottomEdge;
+
+ private static final class LayoutRecord {
+ public int column;
+ public long id = -1;
+ public int height;
+ public int span;
+ private int[] mMargins;
+
+ private final void ensureMargins() {
+ if (mMargins == null) {
+ // Don't need to confirm length;
+ // all layoutrecords are purged when column count changes.
+ mMargins = new int[span * 2];
+ }
+ }
+
+ public final int getMarginAbove(int col) {
+ if (mMargins == null) {
+ return 0;
+ }
+ return mMargins[col * 2];
+ }
+
+ public final int getMarginBelow(int col) {
+ if (mMargins == null) {
+ return 0;
+ }
+ return mMargins[col * 2 + 1];
+ }
+
+ public final void setMarginAbove(int col, int margin) {
+ if (mMargins == null && margin == 0) {
+ return;
+ }
+ ensureMargins();
+ mMargins[col * 2] = margin;
+ }
+
+ public final void setMarginBelow(int col, int margin) {
+ if (mMargins == null && margin == 0) {
+ return;
+ }
+ ensureMargins();
+ mMargins[col * 2 + 1] = margin;
+ }
+
+ @Override
+ public String toString() {
+ String result = "LayoutRecord{c=" + column + ", id=" + id + " h=" + height +
+ " s=" + span;
+ if (mMargins != null) {
+ result += " margins[above, below](";
+ for (int i = 0; i < mMargins.length; i += 2) {
+ result += "[" + mMargins[i] + ", " + mMargins[i+1] + "]";
+ }
+ result += ")";
+ }
+ return result + "}";
+ }
+ }
+ private final SparseArrayCompat<LayoutRecord> mLayoutRecords =
+ new SparseArrayCompat<LayoutRecord>();
+
+ public StaggeredGridView(Context context) {
+ this(context, null);
+ }
+
+ public StaggeredGridView(Context context, AttributeSet attrs) {
+ this(context, attrs, 0);
+ }
+
+ public StaggeredGridView(Context context, AttributeSet attrs, int defStyle) {
+ super(context, attrs, defStyle);
+
+ final ViewConfiguration vc = ViewConfiguration.get(context);
+ mTouchSlop = vc.getScaledTouchSlop();
+ mMaximumVelocity = vc.getScaledMaximumFlingVelocity();
+ mFlingVelocity = vc.getScaledMinimumFlingVelocity();
+ mScroller = ScrollerCompat.from(context);
+
+ mTopEdge = new EdgeEffectCompat(context);
+ mBottomEdge = new EdgeEffectCompat(context);
+ setWillNotDraw(false);
+ setClipToPadding(false);
+ }
+
+ /**
+ * Set a fixed number of columns for this grid. Space will be divided evenly
+ * among all columns, respecting the item margin between columns.
+ * The default is 2. (If it were 1, perhaps you should be using a
+ * {@link android.widget.ListView ListView}.)
+ *
+ * @param colCount Number of columns to display.
+ * @see #setMinColumnWidth(int)
+ */
+ public void setColumnCount(int colCount) {
+ if (colCount < 1 && colCount != COLUMN_COUNT_AUTO) {
+ throw new IllegalArgumentException("Column count must be at least 1 - received " +
+ colCount);
+ }
+ final boolean needsPopulate = colCount != mColCount;
+ mColCount = mColCountSetting = colCount;
+ if (needsPopulate) {
+ populate();
+ }
+ }
+
+ public int getColumnCount() {
+ return mColCount;
+ }
+
+ /**
+ * Set a minimum column width for
+ * @param minColWidth
+ */
+ public void setMinColumnWidth(int minColWidth) {
+ mMinColWidth = minColWidth;
+ setColumnCount(COLUMN_COUNT_AUTO);
+ }
+
+ /**
+ * Set the margin between items in pixels. This margin is applied
+ * both vertically and horizontally.
+ *
+ * @param marginPixels Spacing between items in pixels
+ */
+ public void setItemMargin(int marginPixels) {
+ final boolean needsPopulate = marginPixels != mItemMargin;
+ mItemMargin = marginPixels;
+ if (needsPopulate) {
+ populate();
+ }
+ }
+
+ /**
+ * Return the first adapter position with a view currently attached as
+ * a child view of this grid.
+ *
+ * @return the adapter position represented by the view at getChildAt(0).
+ */
+ public int getFirstPosition() {
+ return mFirstPosition;
+ }
+
+ @Override
+ public boolean onInterceptTouchEvent(MotionEvent ev) {
+ mVelocityTracker.addMovement(ev);
+ final int action = ev.getAction() & MotionEventCompat.ACTION_MASK;
+ switch (action) {
+ case MotionEvent.ACTION_DOWN:
+ mVelocityTracker.clear();
+ mScroller.abortAnimation();
+ mLastTouchY = ev.getY();
+ mActivePointerId = MotionEventCompat.getPointerId(ev, 0);
+ mTouchRemainderY = 0;
+ if (mTouchMode == TOUCH_MODE_FLINGING) {
+ // Catch!
+ mTouchMode = TOUCH_MODE_DRAGGING;
+ return true;
+ }
+ break;
+
+ case MotionEvent.ACTION_MOVE: {
+ final int index = MotionEventCompat.findPointerIndex(ev, mActivePointerId);
+ if (index < 0) {
+ Log.e(TAG, "onInterceptTouchEvent could not find pointer with id " +
+ mActivePointerId + " - did StaggeredGridView receive an inconsistent " +
+ "event stream?");
+ return false;
+ }
+ final float y = MotionEventCompat.getY(ev, index);
+ final float dy = y - mLastTouchY + mTouchRemainderY;
+ final int deltaY = (int) dy;
+ mTouchRemainderY = dy - deltaY;
+
+ if (Math.abs(dy) > mTouchSlop) {
+ mTouchMode = TOUCH_MODE_DRAGGING;
+ return true;
+ }
+ }
+ }
+
+ return false;
+ }
+
+ @Override
+ public boolean onTouchEvent(MotionEvent ev) {
+ mVelocityTracker.addMovement(ev);
+ final int action = ev.getAction() & MotionEventCompat.ACTION_MASK;
+ switch (action) {
+ case MotionEvent.ACTION_DOWN:
+ mVelocityTracker.clear();
+ mScroller.abortAnimation();
+ mLastTouchY = ev.getY();
+ mActivePointerId = MotionEventCompat.getPointerId(ev, 0);
+ mTouchRemainderY = 0;
+ break;
+
+ case MotionEvent.ACTION_MOVE: {
+ final int index = MotionEventCompat.findPointerIndex(ev, mActivePointerId);
+ if (index < 0) {
+ Log.e(TAG, "onInterceptTouchEvent could not find pointer with id " +
+ mActivePointerId + " - did StaggeredGridView receive an inconsistent " +
+ "event stream?");
+ return false;
+ }
+ final float y = MotionEventCompat.getY(ev, index);
+ final float dy = y - mLastTouchY + mTouchRemainderY;
+ final int deltaY = (int) dy;
+ mTouchRemainderY = dy - deltaY;
+
+ if (Math.abs(dy) > mTouchSlop) {
+ mTouchMode = TOUCH_MODE_DRAGGING;
+ }
+
+ if (mTouchMode == TOUCH_MODE_DRAGGING) {
+ mLastTouchY = y;
+
+ if (!trackMotionScroll(deltaY, true)) {
+ // Break fling velocity if we impacted an edge.
+ mVelocityTracker.clear();
+ }
+ }
+ } break;
+
+ case MotionEvent.ACTION_CANCEL:
+ mTouchMode = TOUCH_MODE_IDLE;
+ break;
+
+ case MotionEvent.ACTION_UP: {
+ mVelocityTracker.computeCurrentVelocity(1000, mMaximumVelocity);
+ final float velocity = VelocityTrackerCompat.getYVelocity(mVelocityTracker,
+ mActivePointerId);
+ if (Math.abs(velocity) > mFlingVelocity) { // TODO
+ mTouchMode = TOUCH_MODE_FLINGING;
+ mScroller.fling(0, 0, 0, (int) velocity, 0, 0,
+ Integer.MIN_VALUE, Integer.MAX_VALUE);
+ mLastTouchY = 0;
+ ViewCompat.postInvalidateOnAnimation(this);
+ } else {
+ mTouchMode = TOUCH_MODE_IDLE;
+ }
+
+ } break;
+ }
+ return true;
+ }
+
+ /**
+ *
+ * @param deltaY Pixels that content should move by
+ * @return true if the movement completed, false if it was stopped prematurely.
+ */
+ private boolean trackMotionScroll(int deltaY, boolean allowOverScroll) {
+ final boolean contentFits = contentFits();
+ final int allowOverhang = Math.abs(deltaY);
+
+ final int overScrolledBy;
+ final int movedBy;
+ if (!contentFits) {
+ final int overhang;
+ final boolean up;
+ mPopulating = true;
+ if (deltaY > 0) {
+ overhang = fillUp(mFirstPosition - 1, allowOverhang);
+ up = true;
+ } else {
+ overhang = fillDown(mFirstPosition + getChildCount(), allowOverhang) + mItemMargin;
+ up = false;
+ }
+ movedBy = Math.min(overhang, allowOverhang);
+ offsetChildren(up ? movedBy : -movedBy);
+ recycleOffscreenViews();
+ mPopulating = false;
+ overScrolledBy = allowOverhang - overhang;
+ } else {
+ overScrolledBy = allowOverhang;
+ movedBy = 0;
+ }
+
+ if (allowOverScroll) {
+ final int overScrollMode = ViewCompat.getOverScrollMode(this);
+
+ if (overScrollMode == ViewCompat.OVER_SCROLL_ALWAYS ||
+ (overScrollMode == ViewCompat.OVER_SCROLL_IF_CONTENT_SCROLLS && !contentFits)) {
+
+ if (overScrolledBy > 0) {
+ EdgeEffectCompat edge = deltaY > 0 ? mTopEdge : mBottomEdge;
+ edge.onPull((float) Math.abs(deltaY) / getHeight());
+ ViewCompat.postInvalidateOnAnimation(this);
+ }
+ }
+ }
+
+ return deltaY == 0 || movedBy != 0;
+ }
+
+ private final boolean contentFits() {
+ if (mFirstPosition != 0 || getChildCount() != mItemCount) {
+ return false;
+ }
+
+ int topmost = Integer.MAX_VALUE;
+ int bottommost = Integer.MIN_VALUE;
+ for (int i = 0; i < mColCount; i++) {
+ if (mItemTops[i] < topmost) {
+ topmost = mItemTops[i];
+ }
+ if (mItemBottoms[i] > bottommost) {
+ bottommost = mItemBottoms[i];
+ }
+ }
+
+ return topmost >= getPaddingTop() && bottommost <= getHeight() - getPaddingBottom();
+ }
+
+ private void recycleAllViews() {
+ for (int i = 0; i < getChildCount(); i++) {
+ mRecycler.addScrap(getChildAt(i));
+ }
+
+ if (mInLayout) {
+ removeAllViewsInLayout();
+ } else {
+ removeAllViews();
+ }
+ }
+
+ /**
+ * Important: this method will leave offscreen views attached if they
+ * are required to maintain the invariant that child view with index i
+ * is always the view corresponding to position mFirstPosition + i.
+ */
+ private void recycleOffscreenViews() {
+ final int height = getHeight();
+ final int clearAbove = -mItemMargin;
+ final int clearBelow = height + mItemMargin;
+ for (int i = getChildCount() - 1; i >= 0; i--) {
+ final View child = getChildAt(i);
+ if (child.getTop() <= clearBelow) {
+ // There may be other offscreen views, but we need to maintain
+ // the invariant documented above.
+ break;
+ }
+
+ if (mInLayout) {
+ removeViewsInLayout(i, 1);
+ } else {
+ removeViewAt(i);
+ }
+
+ mRecycler.addScrap(child);
+ }
+
+ while (getChildCount() > 0) {
+ final View child = getChildAt(0);
+ if (child.getBottom() >= clearAbove) {
+ // There may be other offscreen views, but we need to maintain
+ // the invariant documented above.
+ break;
+ }
+
+ if (mInLayout) {
+ removeViewsInLayout(0, 1);
+ } else {
+ removeViewAt(0);
+ }
+
+ mRecycler.addScrap(child);
+ mFirstPosition++;
+ }
+
+ final int childCount = getChildCount();
+ if (childCount > 0) {
+ // Repair the top and bottom column boundaries from the views we still have
+ Arrays.fill(mItemTops, Integer.MAX_VALUE);
+ Arrays.fill(mItemBottoms, Integer.MIN_VALUE);
+
+ for (int i = 0; i < childCount; i++){
+ final View child = getChildAt(i);
+ final LayoutParams lp = (LayoutParams) child.getLayoutParams();
+ final int top = child.getTop() - mItemMargin;
+ final int bottom = child.getBottom();
+ final LayoutRecord rec = mLayoutRecords.get(mFirstPosition + i);
+
+ final int colEnd = lp.column + Math.min(mColCount, lp.span);
+ for (int col = lp.column; col < colEnd; col++) {
+ final int colTop = top - rec.getMarginAbove(col - lp.column);
+ final int colBottom = bottom + rec.getMarginBelow(col - lp.column);
+ if (colTop < mItemTops[col]) {
+ mItemTops[col] = colTop;
+ }
+ if (colBottom > mItemBottoms[col]) {
+ mItemBottoms[col] = colBottom;
+ }
+ }
+ }
+
+ for (int col = 0; col < mColCount; col++) {
+ if (mItemTops[col] == Integer.MAX_VALUE) {
+ // If one was untouched, both were.
+ mItemTops[col] = 0;
+ mItemBottoms[col] = 0;
+ }
+ }
+ }
+ }
+
+ public void computeScroll() {
+ if (mScroller.computeScrollOffset()) {
+ final int y = mScroller.getCurrY();
+ final int dy = (int) (y - mLastTouchY);
+ mLastTouchY = y;
+ final boolean stopped = !trackMotionScroll(dy, false);
+
+ if (!stopped && !mScroller.isFinished()) {
+ ViewCompat.postInvalidateOnAnimation(this);
+ } else {
+ if (stopped) {
+ final int overScrollMode = ViewCompat.getOverScrollMode(this);
+ if (overScrollMode != ViewCompat.OVER_SCROLL_NEVER) {
+ final EdgeEffectCompat edge;
+ if (dy > 0) {
+ edge = mTopEdge;
+ } else {
+ edge = mBottomEdge;
+ }
+ edge.onAbsorb(Math.abs((int) mScroller.getCurrVelocity()));
+ ViewCompat.postInvalidateOnAnimation(this);
+ }
+ mScroller.abortAnimation();
+ }
+ mTouchMode = TOUCH_MODE_IDLE;
+ }
+ }
+ }
+
+ @Override
+ public void draw(Canvas canvas) {
+ super.draw(canvas);
+
+ if (mTopEdge != null) {
+ boolean needsInvalidate = false;
+ if (!mTopEdge.isFinished()) {
+ mTopEdge.draw(canvas);
+ needsInvalidate = true;
+ }
+ if (!mBottomEdge.isFinished()) {
+ final int restoreCount = canvas.save();
+ final int width = getWidth();
+ canvas.translate(-width, getHeight());
+ canvas.rotate(180, width, 0);
+ mBottomEdge.draw(canvas);
+ canvas.restoreToCount(restoreCount);
+ needsInvalidate = true;
+ }
+
+ if (needsInvalidate) {
+ ViewCompat.postInvalidateOnAnimation(this);
+ }
+ }
+ }
+
+ public void beginFastChildLayout() {
+ mFastChildLayout = true;
+ }
+
+ public void endFastChildLayout() {
+ mFastChildLayout = false;
+ populate();
+ }
+
+ @Override
+ public void requestLayout() {
+ if (!mPopulating && !mFastChildLayout) {
+ super.requestLayout();
+ }
+ }
+
+ @Override
+ protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
+ int widthMode = MeasureSpec.getMode(widthMeasureSpec);
+ int heightMode = MeasureSpec.getMode(heightMeasureSpec);
+ int widthSize = MeasureSpec.getSize(widthMeasureSpec);
+ int heightSize = MeasureSpec.getSize(heightMeasureSpec);
+
+ if (widthMode != MeasureSpec.EXACTLY) {
+ Log.e(TAG, "onMeasure: must have an exact width or match_parent! " +
+ "Using fallback spec of EXACTLY " + widthSize);
+ widthMode = MeasureSpec.EXACTLY;
+ }
+ if (heightMode != MeasureSpec.EXACTLY) {
+ Log.e(TAG, "onMeasure: must have an exact height or match_parent! " +
+ "Using fallback spec of EXACTLY " + heightSize);
+ heightMode = MeasureSpec.EXACTLY;
+ }
+
+ setMeasuredDimension(widthSize, heightSize);
+
+ if (mColCountSetting == COLUMN_COUNT_AUTO) {
+ final int colCount = widthSize / mMinColWidth;
+ if (colCount != mColCount) {
+ mColCount = colCount;
+ mForcePopulateOnLayout = true;
+ }
+ }
+ }
+
+ @Override
+ protected void onLayout(boolean changed, int l, int t, int r, int b) {
+ mInLayout = true;
+ populate();
+ mInLayout = false;
+ mForcePopulateOnLayout = false;
+
+ final int width = r - l;
+ final int height = b - t;
+ mTopEdge.setSize(width, height);
+ mBottomEdge.setSize(width, height);
+ }
+
+ private void populate() {
+ if (getWidth() == 0 || getHeight() == 0) {
+ return;
+ }
+
+ if (mColCount == COLUMN_COUNT_AUTO) {
+ final int colCount = getWidth() / mMinColWidth;
+ if (colCount != mColCount) {
+ mColCount = colCount;
+ }
+ }
+
+ final int colCount = mColCount;
+ if (mItemTops == null || mItemTops.length != colCount) {
+ mItemTops = new int[colCount];
+ mItemBottoms = new int[colCount];
+ final int top = getPaddingTop();
+ final int offset = top + Math.min(mRestoreOffset, 0);
+ Arrays.fill(mItemTops, offset);
+ Arrays.fill(mItemBottoms, offset);
+ mLayoutRecords.clear();
+ if (mInLayout) {
+ removeAllViewsInLayout();
+ } else {
+ removeAllViews();
+ }
+ mRestoreOffset = 0;
+ }
+
+ mPopulating = true;
+ layoutChildren(mDataChanged);
+ fillDown(mFirstPosition + getChildCount(), 0);
+ fillUp(mFirstPosition - 1, 0);
+ mPopulating = false;
+ mDataChanged = false;
+ }
+
+ private void dumpItemPositions() {
+ final int childCount = getChildCount();
+ Log.d(TAG, "dumpItemPositions:");
+ Log.d(TAG, " => Tops:");
+ for (int i = 0; i < mColCount; i++) {
+ Log.d(TAG, " => " + mItemTops[i]);
+ boolean found = false;
+ for (int j = 0; j < childCount; j++) {
+ final View child = getChildAt(j);
+ if (mItemTops[i] == child.getTop() - mItemMargin) {
+ found = true;
+ }
+ }
+ if (!found) {
+ Log.d(TAG, "!!! No top item found for column " + i + " value " + mItemTops[i]);
+ }
+ }
+ Log.d(TAG, " => Bottoms:");
+ for (int i = 0; i < mColCount; i++) {
+ Log.d(TAG, " => " + mItemBottoms[i]);
+ boolean found = false;
+ for (int j = 0; j < childCount; j++) {
+ final View child = getChildAt(j);
+ if (mItemBottoms[i] == child.getBottom()) {
+ found = true;
+ }
+ }
+ if (!found) {
+ Log.d(TAG, "!!! No bottom item found for column " + i + " value " + mItemBottoms[i]);
+ }
+ }
+ }
+
+ final void offsetChildren(int offset) {
+ final int childCount = getChildCount();
+ for (int i = 0; i < childCount; i++) {
+ final View child = getChildAt(i);
+ child.layout(child.getLeft(), child.getTop() + offset,
+ child.getRight(), child.getBottom() + offset);
+ }
+
+ final int colCount = mColCount;
+ for (int i = 0; i < colCount; i++) {
+ mItemTops[i] += offset;
+ mItemBottoms[i] += offset;
+ }
+ }
+
+ /**
+ * Measure and layout all currently visible children.
+ *
+ * @param queryAdapter true to requery the adapter for view data
+ */
+ final void layoutChildren(boolean queryAdapter) {
+ final int paddingLeft = getPaddingLeft();
+ final int paddingRight = getPaddingRight();
+ final int itemMargin = mItemMargin;
+ final int colWidth =
+ (getWidth() - paddingLeft - paddingRight - itemMargin * (mColCount - 1)) / mColCount;
+ int rebuildLayoutRecordsBefore = -1;
+ int rebuildLayoutRecordsAfter = -1;
+
+ Arrays.fill(mItemBottoms, Integer.MIN_VALUE);
+
+ final int childCount = getChildCount();
+ for (int i = 0; i < childCount; i++) {
+ View child = getChildAt(i);
+ LayoutParams lp = (LayoutParams) child.getLayoutParams();
+ final int col = lp.column;
+ final int position = mFirstPosition + i;
+ final boolean needsLayout = queryAdapter || child.isLayoutRequested();
+
+ if (queryAdapter) {
+ View newView = obtainView(position, child);
+ if (newView != child) {
+ removeViewAt(i);
+ addView(newView, i);
+ child = newView;
+ }
+ lp = (LayoutParams) child.getLayoutParams(); // Might have changed
+ }
+
+ final int span = Math.min(mColCount, lp.span);
+ final int widthSize = colWidth * span + itemMargin * (span - 1);
+
+ if (needsLayout) {
+ final int widthSpec = MeasureSpec.makeMeasureSpec(widthSize, MeasureSpec.EXACTLY);
+
+ final int heightSpec;
+ if (lp.height == LayoutParams.WRAP_CONTENT) {
+ heightSpec = MeasureSpec.makeMeasureSpec(0, MeasureSpec.UNSPECIFIED);
+ } else {
+ heightSpec = MeasureSpec.makeMeasureSpec(lp.height, MeasureSpec.EXACTLY);
+ }
+
+ child.measure(widthSpec, heightSpec);
+ }
+
+ int childTop = mItemBottoms[col] > Integer.MIN_VALUE ?
+ mItemBottoms[col] + mItemMargin : child.getTop();
+ if (span > 1) {
+ int lowest = childTop;
+ for (int j = col + 1; j < col + span; j++) {
+ final int bottom = mItemBottoms[j] + mItemMargin;
+ if (bottom > lowest) {
+ lowest = bottom;
+ }
+ }
+ childTop = lowest;
+ }
+ final int childHeight = child.getMeasuredHeight();
+ final int childBottom = childTop + childHeight;
+ final int childLeft = paddingLeft + col * (colWidth + itemMargin);
+ final int childRight = childLeft + child.getMeasuredWidth();
+ child.layout(childLeft, childTop, childRight, childBottom);
+
+ for (int j = col; j < col + span; j++) {
+ mItemBottoms[j] = childBottom;
+ }
+
+ final LayoutRecord rec = mLayoutRecords.get(position);
+ if (rec != null && rec.height != childHeight) {
+ // Invalidate our layout records for everything before this.
+ rec.height = childHeight;
+ rebuildLayoutRecordsBefore = position;
+ }
+
+ if (rec != null && rec.span != span) {
+ // Invalidate our layout records for everything after this.
+ rec.span = span;
+ rebuildLayoutRecordsAfter = position;
+ }
+ }
+
+ // Update mItemBottoms for any empty columns
+ for (int i = 0; i < mColCount; i++) {
+ if (mItemBottoms[i] == Integer.MIN_VALUE) {
+ mItemBottoms[i] = mItemTops[i];
+ }
+ }
+
+ if (rebuildLayoutRecordsBefore >= 0 || rebuildLayoutRecordsAfter >= 0) {
+ if (rebuildLayoutRecordsBefore >= 0) {
+ invalidateLayoutRecordsBeforePosition(rebuildLayoutRecordsBefore);
+ }
+ if (rebuildLayoutRecordsAfter >= 0) {
+ invalidateLayoutRecordsAfterPosition(rebuildLayoutRecordsAfter);
+ }
+ for (int i = 0; i < childCount; i++) {
+ final int position = mFirstPosition + i;
+ final View child = getChildAt(i);
+ final LayoutParams lp = (LayoutParams) child.getLayoutParams();
+ LayoutRecord rec = mLayoutRecords.get(position);
+ if (rec == null) {
+ rec = new LayoutRecord();
+ mLayoutRecords.put(position, rec);
+ }
+ rec.column = lp.column;
+ rec.height = child.getHeight();
+ rec.id = lp.id;
+ rec.span = Math.min(mColCount, lp.span);
+ }
+ }
+ }
+
+ final void invalidateLayoutRecordsBeforePosition(int position) {
+ int endAt = 0;
+ while (endAt < mLayoutRecords.size() && mLayoutRecords.keyAt(endAt) < position) {
+ endAt++;
+ }
+ mLayoutRecords.removeAtRange(0, endAt);
+ }
+
+ final void invalidateLayoutRecordsAfterPosition(int position) {
+ int beginAt = mLayoutRecords.size() - 1;
+ while (beginAt >= 0 && mLayoutRecords.keyAt(beginAt) > position) {
+ beginAt--;
+ }
+ beginAt++;
+ mLayoutRecords.removeAtRange(beginAt + 1, mLayoutRecords.size() - beginAt);
+ }
+
+ /**
+ * Should be called with mPopulating set to true
+ *
+ * @param fromPosition Position to start filling from
+ * @param overhang the number of extra pixels to fill beyond the current top edge
+ * @return the max overhang beyond the beginning of the view of any added items at the top
+ */
+ final int fillUp(int fromPosition, int overhang) {
+ final int paddingLeft = getPaddingLeft();
+ final int paddingRight = getPaddingRight();
+ final int itemMargin = mItemMargin;
+ final int colWidth =
+ (getWidth() - paddingLeft - paddingRight - itemMargin * (mColCount - 1)) / mColCount;
+ final int gridTop = getPaddingTop();
+ final int fillTo = gridTop - overhang;
+ int nextCol = getNextColumnUp();
+ int position = fromPosition;
+
+ while (nextCol >= 0 && mItemTops[nextCol] > fillTo && position >= 0) {
+ final View child = obtainView(position, null);
+ LayoutParams lp = (LayoutParams) child.getLayoutParams();
+
+ if (child.getParent() != this) {
+ if (mInLayout) {
+ addViewInLayout(child, 0, lp);
+ } else {
+ addView(child, 0);
+ }
+ }
+
+ final int span = Math.min(mColCount, lp.span);
+ final int widthSize = colWidth * span + itemMargin * (span - 1);
+ final int widthSpec = MeasureSpec.makeMeasureSpec(widthSize, MeasureSpec.EXACTLY);
+
+ LayoutRecord rec;
+ if (span > 1) {
+ rec = getNextRecordUp(position, span);
+ nextCol = rec.column;
+ } else {
+ rec = mLayoutRecords.get(position);
+ }
+
+ boolean invalidateBefore = false;
+ if (rec == null) {
+ rec = new LayoutRecord();
+ mLayoutRecords.put(position, rec);
+ rec.column = nextCol;
+ rec.span = span;
+ } else if (span != rec.span) {
+ rec.span = span;
+ rec.column = nextCol;
+ invalidateBefore = true;
+ } else {
+ nextCol = rec.column;
+ }
+
+ if (mHasStableIds) {
+ final long id = mAdapter.getItemId(position);
+ rec.id = id;
+ lp.id = id;
+ }
+
+ lp.column = nextCol;
+
+ final int heightSpec;
+ if (lp.height == LayoutParams.WRAP_CONTENT) {
+ heightSpec = MeasureSpec.makeMeasureSpec(0, MeasureSpec.UNSPECIFIED);
+ } else {
+ heightSpec = MeasureSpec.makeMeasureSpec(lp.height, MeasureSpec.EXACTLY);
+ }
+ child.measure(widthSpec, heightSpec);
+
+ final int childHeight = child.getMeasuredHeight();
+ if (invalidateBefore || (childHeight != rec.height && rec.height > 0)) {
+ invalidateLayoutRecordsBeforePosition(position);
+ }
+ rec.height = childHeight;
+
+ final int startFrom;
+ if (span > 1) {
+ int highest = mItemTops[nextCol];
+ for (int i = nextCol + 1; i < nextCol + span; i++) {
+ final int top = mItemTops[i];
+ if (top < highest) {
+ highest = top;
+ }
+ }
+ startFrom = highest;
+ } else {
+ startFrom = mItemTops[nextCol];
+ }
+ final int childBottom = startFrom;
+ final int childTop = childBottom - childHeight;
+ final int childLeft = paddingLeft + nextCol * (colWidth + itemMargin);
+ final int childRight = childLeft + child.getMeasuredWidth();
+ child.layout(childLeft, childTop, childRight, childBottom);
+
+ for (int i = nextCol; i < nextCol + span; i++) {
+ mItemTops[i] = childTop - rec.getMarginAbove(i - nextCol) - itemMargin;
+ }
+
+ nextCol = getNextColumnUp();
+ mFirstPosition = position--;
+ }
+
+ int highestView = getHeight();
+ for (int i = 0; i < mColCount; i++) {
+ if (mItemTops[i] < highestView) {
+ highestView = mItemTops[i];
+ }
+ }
+ return gridTop - highestView;
+ }
+
+ /**
+ * Should be called with mPopulating set to true
+ *
+ * @param fromPosition Position to start filling from
+ * @param overhang the number of extra pixels to fill beyond the current bottom edge
+ * @return the max overhang beyond the end of the view of any added items at the bottom
+ */
+ final int fillDown(int fromPosition, int overhang) {
+ final int paddingLeft = getPaddingLeft();
+ final int paddingRight = getPaddingRight();
+ final int itemMargin = mItemMargin;
+ final int colWidth =
+ (getWidth() - paddingLeft - paddingRight - itemMargin * (mColCount - 1)) / mColCount;
+ final int gridBottom = getHeight() - getPaddingBottom();
+ final int fillTo = gridBottom + overhang;
+ int nextCol = getNextColumnDown();
+ int position = fromPosition;
+
+ while (nextCol >= 0 && mItemBottoms[nextCol] < fillTo && position < mItemCount) {
+ final View child = obtainView(position, null);
+ LayoutParams lp = (LayoutParams) child.getLayoutParams();
+
+ if (child.getParent() != this) {
+ if (mInLayout) {
+ addViewInLayout(child, -1, lp);
+ } else {
+ addView(child);
+ }
+ }
+
+ final int span = Math.min(mColCount, lp.span);
+ final int widthSize = colWidth * span + itemMargin * (span - 1);
+ final int widthSpec = MeasureSpec.makeMeasureSpec(widthSize, MeasureSpec.EXACTLY);
+
+ LayoutRecord rec;
+ if (span > 1) {
+ rec = getNextRecordDown(position, span);
+ nextCol = rec.column;
+ } else {
+ rec = mLayoutRecords.get(position);
+ }
+
+ boolean invalidateAfter = false;
+ if (rec == null) {
+ rec = new LayoutRecord();
+ mLayoutRecords.put(position, rec);
+ rec.column = nextCol;
+ rec.span = span;
+ } else if (span != rec.span) {
+ rec.span = span;
+ rec.column = nextCol;
+ invalidateAfter = true;
+ } else {
+ nextCol = rec.column;
+ }
+
+ if (mHasStableIds) {
+ final long id = mAdapter.getItemId(position);
+ rec.id = id;
+ lp.id = id;
+ }
+
+ lp.column = nextCol;
+
+ final int heightSpec;
+ if (lp.height == LayoutParams.WRAP_CONTENT) {
+ heightSpec = MeasureSpec.makeMeasureSpec(0, MeasureSpec.UNSPECIFIED);
+ } else {
+ heightSpec = MeasureSpec.makeMeasureSpec(lp.height, MeasureSpec.EXACTLY);
+ }
+ child.measure(widthSpec, heightSpec);
+
+ final int childHeight = child.getMeasuredHeight();
+ if (invalidateAfter || (childHeight != rec.height && rec.height > 0)) {
+ invalidateLayoutRecordsAfterPosition(position);
+ }
+ rec.height = childHeight;
+
+ final int startFrom;
+ if (span > 1) {
+ int lowest = mItemBottoms[nextCol];
+ for (int i = nextCol + 1; i < nextCol + span; i++) {
+ final int bottom = mItemBottoms[i];
+ if (bottom > lowest) {
+ lowest = bottom;
+ }
+ }
+ startFrom = lowest;
+ } else {
+ startFrom = mItemBottoms[nextCol];
+ }
+ final int childTop = startFrom + itemMargin;
+ final int childBottom = childTop + childHeight;
+ final int childLeft = paddingLeft + nextCol * (colWidth + itemMargin);
+ final int childRight = childLeft + child.getMeasuredWidth();
+ child.layout(childLeft, childTop, childRight, childBottom);
+
+ for (int i = nextCol; i < nextCol + span; i++) {
+ mItemBottoms[i] = childBottom + rec.getMarginBelow(i - nextCol);
+ }
+
+ nextCol = getNextColumnDown();
+ position++;
+ }
+
+ int lowestView = 0;
+ for (int i = 0; i < mColCount; i++) {
+ if (mItemBottoms[i] > lowestView) {
+ lowestView = mItemBottoms[i];
+ }
+ }
+ return lowestView - gridBottom;
+ }
+
+ /**
+ * @return column that the next view filling upwards should occupy. This is the bottom-most
+ * position available for a single-column item.
+ */
+ final int getNextColumnUp() {
+ int result = -1;
+ int bottomMost = Integer.MIN_VALUE;
+
+ final int colCount = mColCount;
+ for (int i = colCount - 1; i >= 0; i--) {
+ final int top = mItemTops[i];
+ if (top > bottomMost) {
+ bottomMost = top;
+ result = i;
+ }
+ }
+ return result;
+ }
+
+ /**
+ * Return a LayoutRecord for the given position
+ * @param position
+ * @param span
+ * @return
+ */
+ final LayoutRecord getNextRecordUp(int position, int span) {
+ LayoutRecord rec = mLayoutRecords.get(position);
+ if (rec == null) {
+ rec = new LayoutRecord();
+ rec.span = span;
+ mLayoutRecords.put(position, rec);
+ } else if (rec.span != span) {
+ throw new IllegalStateException("Invalid LayoutRecord! Record had span=" + rec.span +
+ " but caller requested span=" + span + " for position=" + position);
+ }
+ int targetCol = -1;
+ int bottomMost = Integer.MIN_VALUE;
+
+ final int colCount = mColCount;
+ for (int i = colCount - span; i >= 0; i--) {
+ int top = Integer.MAX_VALUE;
+ for (int j = i; j < i + span; j++) {
+ final int singleTop = mItemTops[j];
+ if (singleTop < top) {
+ top = singleTop;
+ }
+ }
+ if (top > bottomMost) {
+ bottomMost = top;
+ targetCol = i;
+ }
+ }
+
+ rec.column = targetCol;
+
+ for (int i = 0; i < span; i++) {
+ rec.setMarginBelow(i, mItemTops[i + targetCol] - bottomMost);
+ }
+
+ return rec;
+ }
+
+ /**
+ * @return column that the next view filling downwards should occupy. This is the top-most
+ * position available.
+ */
+ final int getNextColumnDown() {
+ int result = -1;
+ int topMost = Integer.MAX_VALUE;
+
+ final int colCount = mColCount;
+ for (int i = 0; i < colCount; i++) {
+ final int bottom = mItemBottoms[i];
+ if (bottom < topMost) {
+ topMost = bottom;
+ result = i;
+ }
+ }
+ return result;
+ }
+
+ final LayoutRecord getNextRecordDown(int position, int span) {
+ LayoutRecord rec = mLayoutRecords.get(position);
+ if (rec == null) {
+ rec = new LayoutRecord();
+ rec.span = span;
+ mLayoutRecords.put(position, rec);
+ } else if (rec.span != span) {
+ throw new IllegalStateException("Invalid LayoutRecord! Record had span=" + rec.span +
+ " but caller requested span=" + span + " for position=" + position);
+ }
+ int targetCol = -1;
+ int topMost = Integer.MAX_VALUE;
+
+ final int colCount = mColCount;
+ for (int i = 0; i <= colCount - span; i++) {
+ int bottom = Integer.MIN_VALUE;
+ for (int j = i; j < i + span; j++) {
+ final int singleBottom = mItemBottoms[j];
+ if (singleBottom > bottom) {
+ bottom = singleBottom;
+ }
+ }
+ if (bottom < topMost) {
+ topMost = bottom;
+ targetCol = i;
+ }
+ }
+
+ rec.column = targetCol;
+
+ for (int i = 0; i < span; i++) {
+ rec.setMarginAbove(i, topMost - mItemBottoms[i + targetCol]);
+ }
+
+ return rec;
+ }
+
+ /**
+ * Obtain a populated view from the adapter. If optScrap is non-null and is not
+ * reused it will be placed in the recycle bin.
+ *
+ * @param position position to get view for
+ * @param optScrap Optional scrap view; will be reused if possible
+ * @return A new view, a recycled view from mRecycler, or optScrap
+ */
+ final View obtainView(int position, View optScrap) {
+ View view = mRecycler.getTransientStateView(position);
+ if (view != null) {
+ return view;
+ }
+
+ // Reuse optScrap if it's of the right type (and not null)
+ final int optType = optScrap != null ?
+ ((LayoutParams) optScrap.getLayoutParams()).viewType : -1;
+ final int positionViewType = mAdapter.getItemViewType(position);
+ final View scrap = optType == positionViewType ?
+ optScrap : mRecycler.getScrapView(positionViewType);
+
+ view = mAdapter.getView(position, scrap, this);
+
+ if (view != scrap && scrap != null) {
+ // The adapter didn't use it; put it back.
+ mRecycler.addScrap(scrap);
+ }
+
+ ViewGroup.LayoutParams lp = view.getLayoutParams();
+
+ if (view.getParent() != this) {
+ if (lp == null) {
+ lp = generateDefaultLayoutParams();
+ } else if (!checkLayoutParams(lp)) {
+ lp = generateLayoutParams(lp);
+ }
+ }
+
+ final LayoutParams sglp = (LayoutParams) lp;
+ sglp.position = position;
+ sglp.viewType = positionViewType;
+
+ return view;
+ }
+
+ public ListAdapter getAdapter() {
+ return mAdapter;
+ }
+
+ public void setAdapter(ListAdapter adapter) {
+ if (mAdapter != null) {
+ mAdapter.unregisterDataSetObserver(mObserver);
+ }
+ // TODO: If the new adapter says that there are stable IDs, remove certain layout records
+ // and onscreen views if they have changed instead of removing all of the state here.
+ clearAllState();
+ mAdapter = adapter;
+ mDataChanged = true;
+ mOldItemCount = mItemCount = adapter != null ? adapter.getCount() : 0;
+ if (adapter != null) {
+ adapter.registerDataSetObserver(mObserver);
+ mRecycler.setViewTypeCount(adapter.getViewTypeCount());
+ mHasStableIds = adapter.hasStableIds();
+ } else {
+ mHasStableIds = false;
+ }
+ populate();
+ }
+
+ /**
+ * Clear all state because the grid will be used for a completely different set of data.
+ */
+ private void clearAllState() {
+ // Clear all layout records and views
+ mLayoutRecords.clear();
+ removeAllViews();
+
+ // Reset to the top of the grid
+ resetStateForGridTop();
+
+ // Clear recycler because there could be different view types now
+ mRecycler.clear();
+ }
+
+ /**
+ * Reset all internal state to be at the top of the grid.
+ */
+ private void resetStateForGridTop() {
+ // Reset mItemTops and mItemBottoms
+ final int colCount = mColCount;
+ if (mItemTops == null || mItemTops.length != colCount) {
+ mItemTops = new int[colCount];
+ mItemBottoms = new int[colCount];
+ }
+ final int top = getPaddingTop();
+ Arrays.fill(mItemTops, top);
+ Arrays.fill(mItemBottoms, top);
+
+ // Reset the first visible position in the grid to be item 0
+ mFirstPosition = 0;
+ mRestoreOffset = 0;
+ }
+
+ /**
+ * Scroll the list so the first visible position in the grid is the first item in the adapter.
+ */
+ public void setSelectionToTop() {
+ // Clear out the views (but don't clear out the layout records or recycler because the data
+ // has not changed)
+ removeAllViews();
+
+ // Reset to top of grid
+ resetStateForGridTop();
+
+ // Start populating again
+ populate();
+ }
+
+ @Override
+ protected LayoutParams generateDefaultLayoutParams() {
+ return new LayoutParams(LayoutParams.WRAP_CONTENT);
+ }
+
+ @Override
+ protected LayoutParams generateLayoutParams(ViewGroup.LayoutParams lp) {
+ return new LayoutParams(lp);
+ }
+
+ @Override
+ protected boolean checkLayoutParams(ViewGroup.LayoutParams lp) {
+ return lp instanceof LayoutParams;
+ }
+
+ @Override
+ public ViewGroup.LayoutParams generateLayoutParams(AttributeSet attrs) {
+ return new LayoutParams(getContext(), attrs);
+ }
+
+ @Override
+ public Parcelable onSaveInstanceState() {
+ final Parcelable superState = super.onSaveInstanceState();
+ final SavedState ss = new SavedState(superState);
+ final int position = mFirstPosition;
+ ss.position = position;
+ if (position >= 0 && mAdapter != null && position < mAdapter.getCount()) {
+ ss.firstId = mAdapter.getItemId(position);
+ }
+ if (getChildCount() > 0) {
+ ss.topOffset = getChildAt(0).getTop() - mItemMargin - getPaddingTop();
+ }
+ return ss;
+ }
+
+ @Override
+ public void onRestoreInstanceState(Parcelable state) {
+ SavedState ss = (SavedState) state;
+ super.onRestoreInstanceState(ss.getSuperState());
+ mDataChanged = true;
+ mFirstPosition = ss.position;
+ mRestoreOffset = ss.topOffset;
+ requestLayout();
+ }
+
+ public static class LayoutParams extends ViewGroup.LayoutParams {
+ private static final int[] LAYOUT_ATTRS = new int[] {
+ android.R.attr.layout_span
+ };
+
+ private static final int SPAN_INDEX = 0;
+
+ /**
+ * The number of columns this item should span
+ */
+ public int span = 1;
+
+ /**
+ * Item position this view represents
+ */
+ int position;
+
+ /**
+ * Type of this view as reported by the adapter
+ */
+ int viewType;
+
+ /**
+ * The column this view is occupying
+ */
+ int column;
+
+ /**
+ * The stable ID of the item this view displays
+ */
+ long id = -1;
+
+ public LayoutParams(int height) {
+ super(FILL_PARENT, height);
+
+ if (this.height == FILL_PARENT) {
+ Log.w(TAG, "Constructing LayoutParams with height FILL_PARENT - " +
+ "impossible! Falling back to WRAP_CONTENT");
+ this.height = WRAP_CONTENT;
+ }
+ }
+
+ public LayoutParams(Context c, AttributeSet attrs) {
+ super(c, attrs);
+
+ if (this.width != FILL_PARENT) {
+ Log.w(TAG, "Inflation setting LayoutParams width to " + this.width +
+ " - must be MATCH_PARENT");
+ this.width = FILL_PARENT;
+ }
+ if (this.height == FILL_PARENT) {
+ Log.w(TAG, "Inflation setting LayoutParams height to MATCH_PARENT - " +
+ "impossible! Falling back to WRAP_CONTENT");
+ this.height = WRAP_CONTENT;
+ }
+
+ TypedArray a = c.obtainStyledAttributes(attrs, LAYOUT_ATTRS);
+ span = a.getInteger(SPAN_INDEX, 1);
+ a.recycle();
+ }
+
+ public LayoutParams(ViewGroup.LayoutParams other) {
+ super(other);
+
+ if (this.width != FILL_PARENT) {
+ Log.w(TAG, "Constructing LayoutParams with width " + this.width +
+ " - must be MATCH_PARENT");
+ this.width = FILL_PARENT;
+ }
+ if (this.height == FILL_PARENT) {
+ Log.w(TAG, "Constructing LayoutParams with height MATCH_PARENT - " +
+ "impossible! Falling back to WRAP_CONTENT");
+ this.height = WRAP_CONTENT;
+ }
+ }
+ }
+
+ private class RecycleBin {
+ private ArrayList<View>[] mScrapViews;
+ private int mViewTypeCount;
+ private int mMaxScrap;
+
+ private SparseArray<View> mTransientStateViews;
+
+ public void setViewTypeCount(int viewTypeCount) {
+ if (viewTypeCount < 1) {
+ throw new IllegalArgumentException("Must have at least one view type (" +
+ viewTypeCount + " types reported)");
+ }
+ if (viewTypeCount == mViewTypeCount) {
+ return;
+ }
+
+ ArrayList<View>[] scrapViews = new ArrayList[viewTypeCount];
+ for (int i = 0; i < viewTypeCount; i++) {
+ scrapViews[i] = new ArrayList<View>();
+ }
+ mViewTypeCount = viewTypeCount;
+ mScrapViews = scrapViews;
+ }
+
+ public void clear() {
+ final int typeCount = mViewTypeCount;
+ for (int i = 0; i < typeCount; i++) {
+ mScrapViews[i].clear();
+ }
+ if (mTransientStateViews != null) {
+ mTransientStateViews.clear();
+ }
+ }
+
+ public void clearTransientViews() {
+ if (mTransientStateViews != null) {
+ mTransientStateViews.clear();
+ }
+ }
+
+ public void addScrap(View v) {
+ final LayoutParams lp = (LayoutParams) v.getLayoutParams();
+ if (ViewCompat.hasTransientState(v)) {
+ if (mTransientStateViews == null) {
+ mTransientStateViews = new SparseArray<View>();
+ }
+ mTransientStateViews.put(lp.position, v);
+ return;
+ }
+
+ final int childCount = getChildCount();
+ if (childCount > mMaxScrap) {
+ mMaxScrap = childCount;
+ }
+
+ ArrayList<View> scrap = mScrapViews[lp.viewType];
+ if (scrap.size() < mMaxScrap) {
+ scrap.add(v);
+ }
+ }
+
+ public View getTransientStateView(int position) {
+ if (mTransientStateViews == null) {
+ return null;
+ }
+
+ final View result = mTransientStateViews.get(position);
+ if (result != null) {
+ mTransientStateViews.remove(position);
+ }
+ return result;
+ }
+
+ public View getScrapView(int type) {
+ ArrayList<View> scrap = mScrapViews[type];
+ if (scrap.isEmpty()) {
+ return null;
+ }
+
+ final int index = scrap.size() - 1;
+ final View result = scrap.get(index);
+ scrap.remove(index);
+ return result;
+ }
+ }
+
+ private class AdapterDataSetObserver extends DataSetObserver {
+ @Override
+ public void onChanged() {
+ mDataChanged = true;
+ mOldItemCount = mItemCount;
+ mItemCount = mAdapter.getCount();
+
+ // TODO: Consider matching these back up if we have stable IDs.
+ mRecycler.clearTransientViews();
+
+ if (!mHasStableIds) {
+ // Clear all layout records and recycle the views
+ mLayoutRecords.clear();
+ recycleAllViews();
+
+ // Reset item bottoms to be equal to item tops
+ final int colCount = mColCount;
+ for (int i = 0; i < colCount; i++) {
+ mItemBottoms[i] = mItemTops[i];
+ }
+ }
+
+ // TODO: consider repopulating in a deferred runnable instead
+ // (so that successive changes may still be batched)
+ requestLayout();
+ }
+
+ @Override
+ public void onInvalidated() {
+ }
+ }
+
+ static class SavedState extends BaseSavedState {
+ long firstId = -1;
+ int position;
+ int topOffset;
+
+ SavedState(Parcelable superState) {
+ super(superState);
+ }
+
+ private SavedState(Parcel in) {
+ super(in);
+ firstId = in.readLong();
+ position = in.readInt();
+ topOffset = in.readInt();
+ }
+
+ @Override
+ public void writeToParcel(Parcel out, int flags) {
+ super.writeToParcel(out, flags);
+ out.writeLong(firstId);
+ out.writeInt(position);
+ out.writeInt(topOffset);
+ }
+
+ @Override
+ public String toString() {
+ return "StaggereGridView.SavedState{"
+ + Integer.toHexString(System.identityHashCode(this))
+ + " firstId=" + firstId
+ + " position=" + position + "}";
+ }
+
+ public static final Parcelable.Creator<SavedState> CREATOR
+ = new Parcelable.Creator<SavedState>() {
+ public SavedState createFromParcel(Parcel in) {
+ return new SavedState(in);
+ }
+
+ public SavedState[] newArray(int size) {
+ return new SavedState[size];
+ }
+ };
+ }
+}