diff options
-rw-r--r-- | carousel/test/res/values-in/strings.xml | 2 | ||||
-rw-r--r-- | chips/AndroidManifest.xml | 16 | ||||
-rw-r--r-- | chips/res/values-ko/strings.xml | 2 | ||||
-rw-r--r-- | chips/res/values-pt-rPT/strings.xml | 2 | ||||
-rw-r--r-- | chips/src/com/android/ex/chips/RecipientEditTextView.java | 81 | ||||
-rw-r--r-- | common/java/com/android/common/OperationScheduler.java | 53 | ||||
-rw-r--r-- | common/tests/src/com/android/common/OperationSchedulerTest.java | 40 | ||||
-rw-r--r-- | photoviewer/.gitignore | 8 | ||||
-rw-r--r-- | variablespeed/jni/variablespeed.cc | 4 | ||||
-rw-r--r-- | widget/java/com/android/ex/widget/StaggeredGridView.java | 1621 |
10 files changed, 1772 insertions, 57 deletions
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 4347202..5e433fa 100644 --- a/chips/src/com/android/ex/chips/RecipientEditTextView.java +++ b/chips/src/com/android/ex/chips/RecipientEditTextView.java @@ -409,6 +409,44 @@ public class RecipientEditTextView extends MultiAutoCompleteTextView implements } } + 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. @@ -2239,45 +2277,7 @@ public class RecipientEditTextView extends MultiAutoCompleteTextView implements } } - @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; @@ -2356,6 +2356,9 @@ public class RecipientEditTextView extends MultiAutoCompleteTextView implements 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 @@ public class OperationScheduler { /** 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 @@ public class OperationScheduler { @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 @@ public class OperationScheduler { * 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 @@ public class OperationScheduler { 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 @@ public class OperationScheduler { 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 @@ public class OperationSchedulerTest extends AndroidTestCase { 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 @@ public class OperationSchedulerTest extends AndroidTestCase { 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 @@ static void CreateAndRealizeAudioPlayer(SLuint32 slSampleRate, 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 @@ bool AudioEngine::PlayFromThisSource(const SLDataSource& audioSrc) { 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]; + } + }; + } +} |