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];
+            }
+        };
+    }
+}