diff --git a/res/layout/list.xml b/res/layout/favorite_list.xml
similarity index 97%
rename from res/layout/list.xml
rename to res/layout/favorite_list.xml
index 889c3b6a4..1c99d6e0a 100644
--- a/res/layout/list.xml
+++ b/res/layout/favorite_list.xml
@@ -26,7 +26,7 @@
android:divider="@android:color/transparent"
android:dividerHeight="8dp" />
-
diff --git a/res/layout/list_item.xml b/res/layout/favorite_list_item.xml
similarity index 100%
rename from res/layout/list_item.xml
rename to res/layout/favorite_list_item.xml
diff --git a/res/layout/favourite.xml b/res/layout/gallery_list.xml
similarity index 59%
rename from res/layout/favourite.xml
rename to res/layout/gallery_list.xml
index 72c895da6..45a5086d8 100644
--- a/res/layout/favourite.xml
+++ b/res/layout/gallery_list.xml
@@ -1,5 +1,6 @@
-
+-->
-
+
-
-
+
+
-
-
+ app:item_margin="8dp" />
+
+
+
\ No newline at end of file
diff --git a/res/layout/gallery_list_detail_item.xml b/res/layout/gallery_list_detail_item.xml
new file mode 100644
index 000000000..cce03ce31
--- /dev/null
+++ b/res/layout/gallery_list_detail_item.xml
@@ -0,0 +1,85 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/res/layout/gallery_list_thumb_item.xml b/res/layout/gallery_list_thumb_item.xml
new file mode 100644
index 000000000..f11a297a5
--- /dev/null
+++ b/res/layout/gallery_list_thumb_item.xml
@@ -0,0 +1,35 @@
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/res/menu/main.xml b/res/menu/main.xml
index 159c3de9b..d17753782 100644
--- a/res/menu/main.xml
+++ b/res/menu/main.xml
@@ -14,22 +14,33 @@
limitations under the License.
-->
diff --git a/res/values/values_staggeredgridview.xml b/res/values/values_staggeredgridview.xml
new file mode 100644
index 000000000..6831b25ee
--- /dev/null
+++ b/res/values/values_staggeredgridview.xml
@@ -0,0 +1,15 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/res/xml/display_settings.xml b/res/xml/display_settings.xml
index 6120186f0..5c7a4dbcc 100644
--- a/res/xml/display_settings.xml
+++ b/res/xml/display_settings.xml
@@ -18,20 +18,20 @@
-
+
-
+
-
+
-
+
@@ -42,9 +42,9 @@
android:entryValues="@array/screen_orientation_entry_values"
android:defaultValue="0"
android:summary="%s" />
-
+
-
+
@@ -55,7 +55,7 @@
android:entryValues="@array/page_scaling_entry_values"
android:defaultValue="3"
android:summary="%s" />
-
+
-
+
-
+
-
+
-
+
-
+
-
+
-
+
-
+
\ No newline at end of file
diff --git a/src/com/etsy/android/grid/ClassLoaderSavedState.java b/src/com/etsy/android/grid/ClassLoaderSavedState.java
new file mode 100644
index 000000000..87f5e8542
--- /dev/null
+++ b/src/com/etsy/android/grid/ClassLoaderSavedState.java
@@ -0,0 +1,95 @@
+/*
+ * Copyright (C) 2013 Etsy
+ * Copyright (C) 2006 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.etsy.android.grid;
+
+import android.os.Parcel;
+import android.os.Parcelable;
+
+
+/**
+ * A {@link android.os.Parcelable} implementation that should be used by inheritance
+ * hierarchies to ensure the state of all classes along the chain is saved.
+ */
+public abstract class ClassLoaderSavedState implements Parcelable {
+ public static final ClassLoaderSavedState EMPTY_STATE = new ClassLoaderSavedState() {};
+
+ private Parcelable mSuperState = EMPTY_STATE;
+ private ClassLoader mClassLoader;
+
+ /**
+ * Constructor used to make the EMPTY_STATE singleton
+ */
+ private ClassLoaderSavedState() {
+ mSuperState = null;
+ mClassLoader = null;
+ }
+
+ /**
+ * Constructor called by derived classes when creating their ListSavedState objects
+ *
+ * @param superState The state of the superclass of this view
+ */
+ protected ClassLoaderSavedState(Parcelable superState, ClassLoader classLoader) {
+ mClassLoader = classLoader;
+ if (superState == null) {
+ throw new IllegalArgumentException("superState must not be null");
+ }
+ else {
+ mSuperState = superState != EMPTY_STATE ? superState : null;
+ }
+ }
+
+ /**
+ * Constructor used when reading from a parcel. Reads the state of the superclass.
+ *
+ * @param source
+ */
+ protected ClassLoaderSavedState(Parcel source) {
+ // ETSY : we're using the passed super class loader unlike AbsSavedState
+ Parcelable superState = source.readParcelable(mClassLoader);
+ mSuperState = superState != null ? superState : EMPTY_STATE;
+ }
+
+ final public Parcelable getSuperState() {
+ return mSuperState;
+ }
+
+ public int describeContents() {
+ return 0;
+ }
+
+ public void writeToParcel(Parcel dest, int flags) {
+ dest.writeParcelable(mSuperState, flags);
+ }
+
+ public static final Parcelable.Creator CREATOR
+ = new Parcelable.Creator() {
+
+ public ClassLoaderSavedState createFromParcel(Parcel in) {
+ Parcelable superState = in.readParcelable(null);
+ if (superState != null) {
+ throw new IllegalStateException("superState must be null");
+ }
+ return EMPTY_STATE;
+ }
+
+ public ClassLoaderSavedState[] newArray(int size) {
+ return new ClassLoaderSavedState[size];
+ }
+ };
+}
diff --git a/src/com/etsy/android/grid/ExtendableListView.java b/src/com/etsy/android/grid/ExtendableListView.java
new file mode 100644
index 000000000..bd2f44372
--- /dev/null
+++ b/src/com/etsy/android/grid/ExtendableListView.java
@@ -0,0 +1,2944 @@
+/*
+ * Copyright (c) 2013 Etsy
+ * Copyright (C) 2006 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.etsy.android.grid;
+
+import java.util.ArrayList;
+
+import android.annotation.SuppressLint;
+import android.content.Context;
+import android.database.DataSetObserver;
+import android.graphics.Rect;
+import android.os.Handler;
+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.ViewCompat;
+import android.util.AttributeSet;
+import android.util.Log;
+import android.view.HapticFeedbackConstants;
+import android.view.MotionEvent;
+import android.view.VelocityTracker;
+import android.view.View;
+import android.view.ViewConfiguration;
+import android.view.ViewGroup;
+import android.view.ViewParent;
+import android.widget.AbsListView;
+import android.widget.AdapterView;
+import android.widget.ListAdapter;
+import android.widget.Scroller;
+
+/**
+ * An extendable implementation of the Android {@link android.widget.ListView}
+ *
+ * This is partly inspired by the incomplete StaggeredGridView supplied in the
+ * Android 4.2+ source & the {@link android.widget.AbsListView} & {@link android.widget.ListView} source;
+ * however this is intended to have a smaller simplified
+ * scope of functionality and hopefully therefore be a workable solution.
+ *
+ * Some things that this doesn't support (yet)
+ * - Dividers (We don't use them in our Etsy grid)
+ * - Edge effect
+ * - Fading edge - yuck
+ * - Item selection
+ * - Focus
+ *
+ * Note: we only really extend {@link android.widget.AbsListView} so we can appear to be one of its direct subclasses.
+ * However most of the code we need to modify is either 1. hidden or 2. package private
+ * So a lot of it's code and some {@link android.widget.AdapterView} code is repeated here
+ * Be careful with this - not everything may be how you expect if you assume this to be
+ * a regular old {@link android.widget.ListView}
+ */
+public abstract class ExtendableListView extends AbsListView {
+
+ private static final String TAG = "ExtendableListView";
+
+ private static final boolean DBG = false;
+
+ private static final int TOUCH_MODE_IDLE = 0;
+ private static final int TOUCH_MODE_SCROLLING = 1;
+ private static final int TOUCH_MODE_FLINGING = 2;
+ private static final int TOUCH_MODE_DOWN = 3;
+ private static final int TOUCH_MODE_TAP = 4;
+ private static final int TOUCH_MODE_DONE_WAITING = 5;
+
+ private static final int INVALID_POINTER = -1;
+
+ // Layout using our default existing state
+ private static final int LAYOUT_NORMAL = 0;
+ // Layout from the first item down
+ private static final int LAYOUT_FORCE_TOP = 1;
+ // Layout from the saved instance state data
+ private static final int LAYOUT_SYNC = 2;
+
+ private int mLayoutMode;
+
+ private int mTouchMode;
+ private int mScrollState = OnScrollListener.SCROLL_STATE_IDLE;
+
+ // Rectangle used for hit testing children
+ // private Rect mTouchFrame;
+ // TODO : ItemClick support from AdapterView
+
+ // For managing scrolling
+ private VelocityTracker mVelocityTracker = null;
+
+ private final int mTouchSlop;
+ private final int mMaximumVelocity;
+ private final int mFlingVelocity;
+
+ // TODO : Edge effect handling
+ // private EdgeEffectCompat mEdgeGlowTop;
+ // private EdgeEffectCompat mEdgeGlowBottom;
+
+ // blocker for when we're in a layout pass
+ private boolean mInLayout;
+
+ ListAdapter mAdapter;
+
+ private int mMotionY;
+ private int mMotionX;
+ private int mMotionCorrection;
+ private int mMotionPosition;
+
+ private int mLastY;
+
+ private int mActivePointerId = INVALID_POINTER;
+
+ protected int mFirstPosition;
+
+ // are we attached to a window - we shouldn't handle any touch events if we're not!
+ private boolean mIsAttached;
+
+ /**
+ * When set to true, calls to requestLayout() will not propagate up the parent hierarchy.
+ * This is used to layout the children during a layout pass.
+ */
+ private boolean mBlockLayoutRequests = false;
+
+ // has our data changed - and should we react to it
+ private boolean mDataChanged;
+ private int mItemCount;
+ private int mOldItemCount;
+
+ final boolean[] mIsScrap = new boolean[1];
+
+ private final RecycleBin mRecycleBin;
+
+ private final AdapterDataSetObserver mObserver;
+ private int mWidthMeasureSpec;
+ private FlingRunnable mFlingRunnable;
+
+ protected boolean mClipToPadding;
+ private PerformClick mPerformClick;
+
+ private Runnable mPendingCheckForTap;
+ private CheckForLongPress mPendingCheckForLongPress;
+
+ private class CheckForLongPress extends WindowRunnnable implements Runnable {
+ @Override
+ public void run() {
+ final int motionPosition = mMotionPosition;
+ final View child = getChildAt(motionPosition);
+ if (child != null) {
+ final int longPressPosition = mMotionPosition;
+ final long longPressId = mAdapter.getItemId(mMotionPosition + mFirstPosition);
+
+ boolean handled = false;
+ if (sameWindow() && !mDataChanged) {
+ handled = performLongPress(child, longPressPosition + mFirstPosition, longPressId);
+ }
+ if (handled) {
+ mTouchMode = TOUCH_MODE_IDLE;
+ setPressed(false);
+ child.setPressed(false);
+ } else {
+ mTouchMode = TOUCH_MODE_DONE_WAITING;
+ }
+
+ }
+ }
+ }
+
+ /**
+ * A class that represents a fixed view in a list, for example a header at the top
+ * or a footer at the bottom.
+ */
+ public class FixedViewInfo {
+ /**
+ * The view to add to the list
+ */
+ public View view;
+ /**
+ * The data backing the view. This is returned from {@link android.widget.ListAdapter#getItem(int)}.
+ */
+ public Object data;
+ /**
+ * true if the fixed view should be selectable in the list
+ */
+ public boolean isSelectable;
+ }
+
+ private final ArrayList mHeaderViewInfos;
+ private final ArrayList mFooterViewInfos;
+
+
+ public ExtendableListView(final Context context, final AttributeSet attrs, final int defStyle) {
+ super(context, attrs, defStyle);
+
+ // setting up to be a scrollable view group
+ setWillNotDraw(false);
+ setClipToPadding(false);
+ setFocusableInTouchMode(false);
+
+ final ViewConfiguration viewConfiguration = ViewConfiguration.get(context);
+ mTouchSlop = viewConfiguration.getScaledTouchSlop();
+ mMaximumVelocity = viewConfiguration.getScaledMaximumFlingVelocity();
+ mFlingVelocity = viewConfiguration.getScaledMinimumFlingVelocity();
+
+ mRecycleBin = new RecycleBin();
+ mObserver = new AdapterDataSetObserver();
+
+ mHeaderViewInfos = new ArrayList();
+ mFooterViewInfos = new ArrayList();
+
+ // start our layout mode drawing from the top
+ mLayoutMode = LAYOUT_NORMAL;
+ }
+
+
+ // //////////////////////////////////////////////////////////////////////////////////////////
+ // MAINTAINING SOME STATE
+ //
+
+ @Override
+ protected void onAttachedToWindow() {
+ super.onAttachedToWindow();
+
+ if (mAdapter != null) {
+ // Data may have changed while we were detached. Refresh.
+ mDataChanged = true;
+ mOldItemCount = mItemCount;
+ mItemCount = mAdapter.getCount();
+ }
+ mIsAttached = true;
+ }
+
+ @Override
+ protected void onDetachedFromWindow() {
+ super.onDetachedFromWindow();
+
+ // Detach any view left in the scrap heap
+ mRecycleBin.clear();
+
+ if (mFlingRunnable != null) {
+ removeCallbacks(mFlingRunnable);
+ }
+
+ mIsAttached = false;
+ }
+
+ @Override
+ protected void onFocusChanged(boolean gainFocus, int direction, Rect previouslyFocusedRect) {
+ // TODO : handle focus and its impact on selection - if we add item selection support
+ }
+
+ @Override
+ public void onWindowFocusChanged(boolean hasWindowFocus) {
+ // TODO : handle focus and its impact on selection - if we add item selection support
+ }
+
+ @Override
+ protected void onSizeChanged(int w, int h, int oldw, int oldh) {
+ onSizeChanged(w, h);
+ }
+
+ protected void onSizeChanged(int w, int h) {
+ if (getChildCount() > 0) {
+ stopFlingRunnable();
+ mRecycleBin.clear();
+ mDataChanged = true;
+ rememberSyncState();
+ }
+ }
+
+ // //////////////////////////////////////////////////////////////////////////////////////////
+ // ADAPTER
+ //
+
+ @Override
+ public ListAdapter getAdapter() {
+ return mAdapter;
+ }
+
+ @Override
+ public void setAdapter(final ListAdapter adapter) {
+ if (mAdapter != null) {
+ mAdapter.unregisterDataSetObserver(mObserver);
+ }
+
+ // use a wrapper list adapter if we have a header or footer
+ if (mHeaderViewInfos.size() > 0 || mFooterViewInfos.size() > 0) {
+ mAdapter = new HeaderViewListAdapter(mHeaderViewInfos, mFooterViewInfos, adapter);
+ }
+ else {
+ mAdapter = adapter;
+ }
+
+ mDataChanged = true;
+ mItemCount = mAdapter != null ? mAdapter.getCount() : 0;
+
+ if (mAdapter != null) {
+ mAdapter.registerDataSetObserver(mObserver);
+ mRecycleBin.setViewTypeCount(mAdapter.getViewTypeCount());
+ }
+
+ requestLayout();
+ }
+
+ @Override
+ public int getCount() {
+ return mItemCount;
+ }
+
+ // //////////////////////////////////////////////////////////////////////////////////////////
+ // ADAPTER VIEW - UNSUPPORTED
+ //
+
+ @Override
+ public View getSelectedView() {
+ if (DBG) Log.e(TAG, "getSelectedView() is not supported in ExtendableListView yet");
+ return null;
+ }
+
+ @Override
+ public void setSelection(final int position) {
+ setSelectionFromTop(position, 0);
+ }
+
+ /**
+ * Sets the selected item and positions the selection y pixels from the top edge
+ * of the ListView. (If in touch mode, the item will not be selected but it will
+ * still be positioned appropriately.)
+ *
+ * @param position Index (starting at 0) of the data item to be selected.
+ * @param y The distance from the top edge of the ListView (plus padding) that the
+ * item will be positioned.
+ */
+ @SuppressLint("Override") // WTF ??????
+ public void setSelectionFromTop(int position, int y) {
+ if (position >= 0) {
+ mLayoutMode = LAYOUT_SYNC;
+ mSpecificTop = getListPaddingTop() + y;
+
+ mFirstPosition = 0;
+ if (mNeedSync) {
+ mSyncPosition = position;
+ mSyncRowId = mAdapter.getItemId(position);
+ }
+ requestLayout();
+ }
+ }
+
+ // //////////////////////////////////////////////////////////////////////////////////////////
+ // HEADER & FOOTER
+ //
+
+ /**
+ * Add a fixed view to appear at the top of the list. If addHeaderView is
+ * called more than once, the views will appear in the order they were
+ * added. Views added using this call can take focus if they want.
+ *
+ * NOTE: Call this before calling setAdapter. This is so ListView can wrap
+ * the supplied cursor with one that will also account for header and footer
+ * views.
+ *
+ * @param v The view to add.
+ * @param data Data to associate with this view
+ * @param isSelectable whether the item is selectable
+ */
+ public void addHeaderView(View v, Object data, boolean isSelectable) {
+
+ if (mAdapter != null && !(mAdapter instanceof HeaderViewListAdapter)) {
+ throw new IllegalStateException(
+ "Cannot add header view to list -- setAdapter has already been called.");
+ }
+
+ FixedViewInfo info = new FixedViewInfo();
+ info.view = v;
+ info.data = data;
+ info.isSelectable = isSelectable;
+ mHeaderViewInfos.add(info);
+
+ // in the case of re-adding a header view, or adding one later on,
+ // we need to notify the observer
+ if (mAdapter != null && mObserver != null) {
+ mObserver.onChanged();
+ }
+ }
+
+ /**
+ * Add a fixed view to appear at the top of the list. If addHeaderView is
+ * called more than once, the views will appear in the order they were
+ * added. Views added using this call can take focus if they want.
+ *
+ * NOTE: Call this before calling setAdapter. This is so ListView can wrap
+ * the supplied cursor with one that will also account for header and footer
+ * views.
+ *
+ * @param v The view to add.
+ */
+ public void addHeaderView(View v) {
+ addHeaderView(v, null, true);
+ }
+
+ public int getHeaderViewsCount() {
+ return mHeaderViewInfos.size();
+ }
+
+ /**
+ * Removes a previously-added header view.
+ *
+ * @param v The view to remove
+ * @return true if the view was removed, false if the view was not a header
+ * view
+ */
+ public boolean removeHeaderView(View v) {
+ if (mHeaderViewInfos.size() > 0) {
+ boolean result = false;
+ if (mAdapter != null && ((HeaderViewListAdapter) mAdapter).removeHeader(v)) {
+ if (mObserver != null) {
+ mObserver.onChanged();
+ }
+ result = true;
+ }
+ removeFixedViewInfo(v, mHeaderViewInfos);
+ return result;
+ }
+ return false;
+ }
+
+ private void removeFixedViewInfo(View v, ArrayList where) {
+ int len = where.size();
+ for (int i = 0; i < len; ++i) {
+ FixedViewInfo info = where.get(i);
+ if (info.view == v) {
+ where.remove(i);
+ break;
+ }
+ }
+ }
+
+ /**
+ * Add a fixed view to appear at the bottom of the list. If addFooterView is
+ * called more than once, the views will appear in the order they were
+ * added. Views added using this call can take focus if they want.
+ *
+ * NOTE: Call this before calling setAdapter. This is so ListView can wrap
+ * the supplied cursor with one that will also account for header and footer
+ * views.
+ *
+ * @param v The view to add.
+ * @param data Data to associate with this view
+ * @param isSelectable true if the footer view can be selected
+ */
+ public void addFooterView(View v, Object data, boolean isSelectable) {
+
+ // NOTE: do not enforce the adapter being null here, since unlike in
+ // addHeaderView, it was never enforced here, and so existing apps are
+ // relying on being able to add a footer and then calling setAdapter to
+ // force creation of the HeaderViewListAdapter wrapper
+
+ FixedViewInfo info = new FixedViewInfo();
+ info.view = v;
+ info.data = data;
+ info.isSelectable = isSelectable;
+ mFooterViewInfos.add(info);
+
+ // in the case of re-adding a footer view, or adding one later on,
+ // we need to notify the observer
+ if (mAdapter != null && mObserver != null) {
+ mObserver.onChanged();
+ }
+ }
+
+ /**
+ * Add a fixed view to appear at the bottom of the list. If addFooterView is called more
+ * than once, the views will appear in the order they were added. Views added using
+ * this call can take focus if they want.
+ *
NOTE: Call this before calling setAdapter. This is so ListView can wrap the supplied
+ * cursor with one that will also account for header and footer views.
+ *
+ * @param v The view to add.
+ */
+ public void addFooterView(View v) {
+ addFooterView(v, null, true);
+ }
+
+ public int getFooterViewsCount() {
+ return mFooterViewInfos.size();
+ }
+
+ /**
+ * Removes a previously-added footer view.
+ *
+ * @param v The view to remove
+ * @return true if the view was removed, false if the view was not a footer view
+ */
+ public boolean removeFooterView(View v) {
+ if (mFooterViewInfos.size() > 0) {
+ boolean result = false;
+ if (mAdapter != null && ((HeaderViewListAdapter) mAdapter).removeFooter(v)) {
+ if (mObserver != null) {
+ mObserver.onChanged();
+ }
+ result = true;
+ }
+ removeFixedViewInfo(v, mFooterViewInfos);
+ return result;
+ }
+ return false;
+ }
+
+ // //////////////////////////////////////////////////////////////////////////////////////////
+ // Property Overrides
+ //
+
+ @Override
+ public void setClipToPadding(final boolean clipToPadding) {
+ super.setClipToPadding(clipToPadding);
+ mClipToPadding = clipToPadding;
+ }
+
+ // //////////////////////////////////////////////////////////////////////////////////////////
+ // LAYOUT
+ //
+
+ /**
+ * {@inheritDoc}
+ */
+ @Override
+ public void requestLayout() {
+ if (!mBlockLayoutRequests && !mInLayout) {
+ super.requestLayout();
+ }
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ @Override
+ protected void onLayout(final boolean changed, final int l, final int t, final int r, final int b) {
+ // super.onLayout(changed, l, t, r, b); - skipping base AbsListView implementation on purpose
+ // haven't set an adapter yet? get to it
+ if (mAdapter == null) {
+ return;
+ }
+
+ if (changed) {
+ int childCount = getChildCount();
+ for (int i = 0; i < childCount; i++) {
+ getChildAt(i).forceLayout();
+ }
+ mRecycleBin.markChildrenDirty();
+ }
+
+ // TODO get the height of the view??
+ mInLayout = true;
+ layoutChildren();
+ mInLayout = false;
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ @Override
+ protected void layoutChildren() {
+ if (mBlockLayoutRequests) return;
+ mBlockLayoutRequests = true;
+
+ try {
+ super.layoutChildren();
+ invalidate();
+
+ if (mAdapter == null) {
+ clearState();
+ invokeOnItemScrollListener();
+ return;
+ }
+
+ int childrenTop = getListPaddingTop();
+
+ int childCount = getChildCount();
+ View oldFirst = null;
+
+ // our last state so we keep our position
+ if (mLayoutMode == LAYOUT_NORMAL) {
+ oldFirst = getChildAt(0);
+ }
+
+ boolean dataChanged = mDataChanged;
+ if (dataChanged) {
+ handleDataChanged();
+ }
+
+ // safety check!
+ // Handle the empty set by removing all views that are visible
+ // and calling it a day
+ if (mItemCount == 0) {
+ clearState();
+ invokeOnItemScrollListener();
+ return;
+ }
+ else if (mItemCount != mAdapter.getCount()) {
+ throw new IllegalStateException("The content of the adapter has changed but "
+ + "ExtendableListView did not receive a notification. Make sure the content of "
+ + "your adapter is not modified from a background thread, but only "
+ + "from the UI thread. [in ExtendableListView(" + getId() + ", " + getClass()
+ + ") with Adapter(" + mAdapter.getClass() + ")]");
+ }
+
+ // Pull all children into the RecycleBin.
+ // These views will be reused if possible
+ final int firstPosition = mFirstPosition;
+ final RecycleBin recycleBin = mRecycleBin;
+
+ if (dataChanged) {
+ for (int i = 0; i < childCount; i++) {
+ recycleBin.addScrapView(getChildAt(i), firstPosition + i);
+ }
+ }
+ else {
+ recycleBin.fillActiveViews(childCount, firstPosition);
+ }
+
+ // Clear out old views
+ detachAllViewsFromParent();
+ recycleBin.removeSkippedScrap();
+
+ switch (mLayoutMode) {
+ case LAYOUT_FORCE_TOP: {
+ mFirstPosition = 0;
+ resetToTop();
+ adjustViewsUpOrDown();
+ fillFromTop(childrenTop);
+ adjustViewsUpOrDown();
+ break;
+ }
+ case LAYOUT_SYNC: {
+ fillSpecific(mSyncPosition, mSpecificTop);
+ break;
+ }
+ case LAYOUT_NORMAL:
+ default: {
+ if (childCount == 0) {
+ fillFromTop(childrenTop);
+ }
+ else if (mFirstPosition < mItemCount) {
+ fillSpecific(mFirstPosition,
+ oldFirst == null ? childrenTop : oldFirst.getTop());
+ }
+ else {
+ fillSpecific(0, childrenTop);
+ }
+ break;
+ }
+ }
+
+ // Flush any cached views that did not get reused above
+ recycleBin.scrapActiveViews();
+ mDataChanged = false;
+ mNeedSync = false;
+ mLayoutMode = LAYOUT_NORMAL;
+ invokeOnItemScrollListener();
+ } finally {
+ mBlockLayoutRequests = false;
+ }
+ }
+
+
+ @Override
+ protected void handleDataChanged() {
+ super.handleDataChanged();
+
+ final int count = mItemCount;
+
+ if (count > 0 && mNeedSync) {
+ mNeedSync = false;
+ mSyncState = null;
+
+ mLayoutMode = LAYOUT_SYNC;
+ mSyncPosition = Math.min(Math.max(0, mSyncPosition), count - 1);
+ return;
+ }
+
+ mLayoutMode = LAYOUT_FORCE_TOP;
+ mNeedSync = false;
+ mSyncState = null;
+
+ // TODO : add selection handling here
+ }
+
+ public void resetToTop() {
+ // TO override
+ }
+
+ // //////////////////////////////////////////////////////////////////////////////////////////
+ // MEASUREMENT
+ //
+
+ /**
+ * {@inheritDoc}
+ */
+ @Override
+ protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
+ super.onMeasure(widthMeasureSpec, heightMeasureSpec);
+ int widthSize = MeasureSpec.getSize(widthMeasureSpec);
+ int heightSize = MeasureSpec.getSize(heightMeasureSpec);
+ setMeasuredDimension(widthSize, heightSize);
+ mWidthMeasureSpec = widthMeasureSpec;
+ }
+
+ // //////////////////////////////////////////////////////////////////////////////////////////
+ // ON TOUCH
+ //
+
+ /**
+ * {@inheritDoc}
+ */
+ @Override
+ public boolean onTouchEvent(MotionEvent event) {
+ // we're not passing this down as
+ // all the touch handling is right here
+ // super.onTouchEvent(event);
+
+ if (!isEnabled()) {
+ // A disabled view that is clickable still consumes the touch
+ // events, it just doesn't respond to them.
+ return isClickable() || isLongClickable();
+ }
+
+ initVelocityTrackerIfNotExists();
+ mVelocityTracker.addMovement(event);
+
+ if (!hasChildren()) return false;
+
+ boolean handled;
+ final int action = event.getAction() & MotionEventCompat.ACTION_MASK;
+ switch (action) {
+ case MotionEvent.ACTION_DOWN:
+ handled = onTouchDown(event);
+ break;
+
+ case MotionEvent.ACTION_MOVE:
+ handled = onTouchMove(event);
+ break;
+
+ case MotionEvent.ACTION_CANCEL:
+ handled = onTouchCancel(event);
+ break;
+
+ case MotionEvent.ACTION_POINTER_UP:
+ handled = onTouchPointerUp(event);
+ break;
+
+ case MotionEvent.ACTION_UP:
+ handled = onTouchUp(event);
+ break;
+
+ default:
+ handled = false;
+ break;
+ }
+
+ notifyTouchMode();
+
+ return handled;
+ }
+
+
+ @Override
+ public boolean onInterceptTouchEvent(MotionEvent ev) {
+ int action = ev.getAction();
+
+ if (!mIsAttached) {
+ // Something isn't right.
+ // Since we rely on being attached to get data set change notifications,
+ // don't risk doing anything where we might try to resync and find things
+ // in a bogus state.
+ return false;
+ }
+
+
+ switch (action & MotionEvent.ACTION_MASK) {
+ case MotionEvent.ACTION_DOWN: {
+ int touchMode = mTouchMode;
+
+ // TODO : overscroll
+// if (touchMode == TOUCH_MODE_OVERFLING || touchMode == TOUCH_MODE_OVERSCROLL) {
+// mMotionCorrection = 0;
+// return true;
+// }
+
+ final int x = (int) ev.getX();
+ final int y = (int) ev.getY();
+ mActivePointerId = ev.getPointerId(0);
+
+ int motionPosition = findMotionRow(y);
+ if (touchMode != TOUCH_MODE_FLINGING && motionPosition >= 0) {
+ // User clicked on an actual view (and was not stopping a fling).
+ // Remember where the motion event started
+ mMotionX = x;
+ mMotionY = y;
+ mMotionPosition = motionPosition;
+ mTouchMode = TOUCH_MODE_DOWN;
+ }
+ mLastY = Integer.MIN_VALUE;
+ initOrResetVelocityTracker();
+ mVelocityTracker.addMovement(ev);
+ if (touchMode == TOUCH_MODE_FLINGING) {
+ return true;
+ }
+ break;
+ }
+
+ case MotionEvent.ACTION_MOVE: {
+ switch (mTouchMode) {
+ case TOUCH_MODE_DOWN:
+ int pointerIndex = ev.findPointerIndex(mActivePointerId);
+ if (pointerIndex == -1) {
+ pointerIndex = 0;
+ mActivePointerId = ev.getPointerId(pointerIndex);
+ }
+ final int y = (int) ev.getY(pointerIndex);
+ initVelocityTrackerIfNotExists();
+ mVelocityTracker.addMovement(ev);
+ if (startScrollIfNeeded(y)) {
+ return true;
+ }
+ break;
+ }
+ break;
+ }
+
+ case MotionEvent.ACTION_CANCEL:
+ case MotionEvent.ACTION_UP: {
+ mTouchMode = TOUCH_MODE_IDLE;
+ mActivePointerId = INVALID_POINTER;
+ recycleVelocityTracker();
+ reportScrollStateChange(OnScrollListener.SCROLL_STATE_IDLE);
+ break;
+ }
+
+ case MotionEvent.ACTION_POINTER_UP: {
+ onSecondaryPointerUp(ev);
+ break;
+ }
+ }
+
+ return false;
+ }
+
+ @Override
+ public void requestDisallowInterceptTouchEvent(boolean disallowIntercept) {
+ if (disallowIntercept) {
+ recycleVelocityTracker();
+ }
+ super.requestDisallowInterceptTouchEvent(disallowIntercept);
+ }
+
+ final class CheckForTap implements Runnable {
+ @Override
+ public void run() {
+ if (mTouchMode == TOUCH_MODE_DOWN) {
+ mTouchMode = TOUCH_MODE_TAP;
+ final View child = getChildAt(mMotionPosition);
+ if (child != null && !child.hasFocusable()) {
+ mLayoutMode = LAYOUT_NORMAL;
+
+ if (!mDataChanged) {
+ layoutChildren();
+ child.setPressed(true);
+ setPressed(true);
+
+ final int longPressTimeout = ViewConfiguration.getLongPressTimeout();
+ final boolean longClickable = isLongClickable();
+
+ if (longClickable) {
+ if (mPendingCheckForLongPress == null) {
+ mPendingCheckForLongPress = new CheckForLongPress();
+ }
+ mPendingCheckForLongPress.rememberWindowAttachCount();
+ postDelayed(mPendingCheckForLongPress, longPressTimeout);
+ } else {
+ mTouchMode = TOUCH_MODE_DONE_WAITING;
+ }
+ } else {
+ mTouchMode = TOUCH_MODE_DONE_WAITING;
+ }
+ }
+ }
+ }
+ }
+
+ private boolean onTouchDown(final MotionEvent event) {
+ final int x = (int) event.getX();
+ final int y = (int) event.getY();
+ int motionPosition = pointToPosition(x, y);
+
+ mVelocityTracker.clear();
+ mActivePointerId = MotionEventCompat.getPointerId(event, 0);
+
+ // TODO : use the motion position for fling support
+ // TODO : support long press!
+ // startLongPressCheck();
+
+ if ((mTouchMode != TOUCH_MODE_FLINGING) &&
+ !mDataChanged &&
+ motionPosition >= 0 &&
+ getAdapter().isEnabled(motionPosition)) {
+ // is it a tap or a scroll .. we don't know yet!
+ mTouchMode = TOUCH_MODE_DOWN;
+
+ if (mPendingCheckForTap == null) {
+ mPendingCheckForTap = new CheckForTap();
+ }
+ postDelayed(mPendingCheckForTap, ViewConfiguration.getTapTimeout());
+
+ if (event.getEdgeFlags() != 0 && motionPosition < 0) {
+ // If we couldn't find a view to click on, but the down event was touching
+ // the edge, we will bail out and try again. This allows the edge correcting
+ // code in ViewRoot to try to find a nearby view to select
+ return false;
+ }
+ }
+ else if (mTouchMode == TOUCH_MODE_FLINGING) {
+ mTouchMode = TOUCH_MODE_SCROLLING;
+ mMotionCorrection = 0;
+ motionPosition = findMotionRow(y);
+ }
+
+ mMotionX = x;
+ mMotionY = y;
+ mMotionPosition = motionPosition;
+ mLastY = Integer.MIN_VALUE;
+
+ return true;
+ }
+
+ private boolean onTouchMove(final MotionEvent event) {
+ final int index = MotionEventCompat.findPointerIndex(event, mActivePointerId);
+ if (index < 0) {
+ Log.e(TAG, "onTouchMove could not find pointer with id " +
+ mActivePointerId + " - did ExtendableListView receive an inconsistent " +
+ "event stream?");
+ return false;
+ }
+ final int y = (int) MotionEventCompat.getY(event, index);
+
+ // our data's changed so we need to do a layout before moving any further
+ if (mDataChanged) {
+ layoutChildren();
+ }
+
+ switch (mTouchMode) {
+ case TOUCH_MODE_DOWN:
+ case TOUCH_MODE_TAP:
+ case TOUCH_MODE_DONE_WAITING:
+ // Check if we have moved far enough that it looks more like a
+ // scroll than a tap
+ startScrollIfNeeded(y);
+ break;
+ case TOUCH_MODE_SCROLLING:
+// case TOUCH_MODE_OVERSCROLL:
+ scrollIfNeeded(y);
+ break;
+ }
+
+ return true;
+ }
+
+
+ private boolean onTouchCancel(final MotionEvent event) {
+ mTouchMode = TOUCH_MODE_IDLE;
+ setPressed(false);
+ invalidate(); // redraw selector
+ final Handler handler = getHandler();
+
+ if (handler != null) {
+ handler.removeCallbacks(mPendingCheckForLongPress);
+ }
+
+ recycleVelocityTracker();
+ mActivePointerId = INVALID_POINTER;
+ return true;
+ }
+
+ private boolean onTouchUp(final MotionEvent event) {
+ switch (mTouchMode) {
+ case TOUCH_MODE_DOWN:
+ case TOUCH_MODE_TAP:
+ case TOUCH_MODE_DONE_WAITING:
+ return onTouchUpTap(event);
+
+ case TOUCH_MODE_SCROLLING:
+ return onTouchUpScrolling(event);
+ }
+
+ setPressed(false);
+ invalidate(); // redraw selector
+
+ final Handler handler = getHandler();
+ if (handler != null) {
+ handler.removeCallbacks(mPendingCheckForLongPress);
+ }
+
+ recycleVelocityTracker();
+
+ mActivePointerId = INVALID_POINTER;
+ return true;
+ }
+
+ private boolean onTouchUpScrolling(final MotionEvent event) {
+ if (hasChildren()) {
+ // 2 - Are we at the top or bottom?
+ int top = getFirstChildTop();
+ int bottom = getLastChildBottom();
+ final boolean atEdge = mFirstPosition == 0 &&
+ top >= getListPaddingTop() &&
+ mFirstPosition + getChildCount() < mItemCount &&
+ bottom <= getHeight() - getListPaddingBottom();
+
+ if (!atEdge) {
+ mVelocityTracker.computeCurrentVelocity(1000, mMaximumVelocity);
+ final float velocity = mVelocityTracker.getYVelocity(mActivePointerId);
+
+ if (Math.abs(velocity) > mFlingVelocity) {
+ startFlingRunnable(velocity);
+ mTouchMode = TOUCH_MODE_FLINGING;
+ mMotionY = 0;
+ invalidate();
+ return true;
+ }
+ }
+ }
+
+ stopFlingRunnable();
+ recycleVelocityTracker();
+ mTouchMode = TOUCH_MODE_IDLE;
+ return true;
+ }
+
+ private boolean onTouchUpTap(final MotionEvent event) {
+ final int motionPosition = mMotionPosition;
+ if (motionPosition >= 0) {
+ final View child = getChildAt(motionPosition);
+ if (child != null && !child.hasFocusable()) {
+ if (mTouchMode != TOUCH_MODE_DOWN) {
+ child.setPressed(false);
+ }
+
+ if (mPerformClick == null) {
+ invalidate();
+ mPerformClick = new PerformClick();
+ }
+
+ final PerformClick performClick = mPerformClick;
+ performClick.mClickMotionPosition = motionPosition;
+ performClick.rememberWindowAttachCount();
+
+ // mResurrectToPosition = motionPosition;
+
+ if (mTouchMode == TOUCH_MODE_DOWN || mTouchMode == TOUCH_MODE_TAP) {
+ final Handler handler = getHandler();
+ if (handler != null) {
+ handler.removeCallbacks(mTouchMode == TOUCH_MODE_DOWN ?
+ mPendingCheckForTap : mPendingCheckForLongPress);
+ }
+ mLayoutMode = LAYOUT_NORMAL;
+ if (!mDataChanged && motionPosition >= 0 && mAdapter.isEnabled(motionPosition)) {
+ mTouchMode = TOUCH_MODE_TAP;
+ layoutChildren();
+ child.setPressed(true);
+ setPressed(true);
+ postDelayed(new Runnable() {
+ @Override
+ public void run() {
+ child.setPressed(false);
+ setPressed(false);
+ if (!mDataChanged) {
+ post(performClick);
+ }
+ mTouchMode = TOUCH_MODE_IDLE;
+ }
+ }, ViewConfiguration.getPressedStateDuration());
+ } else {
+ mTouchMode = TOUCH_MODE_IDLE;
+ }
+ return true;
+ } else if (!mDataChanged && motionPosition >= 0 && mAdapter.isEnabled(motionPosition)) {
+ post(performClick);
+ }
+ }
+ }
+ mTouchMode = TOUCH_MODE_IDLE;
+
+ return true;
+ }
+
+ private boolean onTouchPointerUp(final MotionEvent event) {
+ onSecondaryPointerUp(event);
+ final int x = mMotionX;
+ final int y = mMotionY;
+ final int motionPosition = pointToPosition(x, y);
+ if (motionPosition >= 0) {
+ mMotionPosition = motionPosition;
+ }
+ mLastY = y;
+ return true;
+ }
+
+ private void onSecondaryPointerUp(MotionEvent event) {
+ final int pointerIndex = (event.getAction() &
+ MotionEventCompat.ACTION_POINTER_INDEX_MASK) >>
+ MotionEventCompat.ACTION_POINTER_INDEX_SHIFT;
+ final int pointerId = event.getPointerId(pointerIndex);
+ if (pointerId == mActivePointerId) {
+ // This was our active pointer going up. Choose a new
+ // active pointer and adjust accordingly.
+ // TODO: Make this decision more intelligent.
+ final int newPointerIndex = pointerIndex == 0 ? 1 : 0;
+ mMotionX = (int) event.getX(newPointerIndex);
+ mMotionY = (int) event.getY(newPointerIndex);
+ mActivePointerId = event.getPointerId(newPointerIndex);
+ recycleVelocityTracker();
+ }
+ }
+
+ // //////////////////////////////////////////////////////////////////////////////////////////
+ // SCROLL HELPERS
+ //
+
+ /**
+ * Starts a scroll that moves the difference between y and our last motions y
+ * if it's a movement that represents a big enough scroll.
+ */
+ private boolean startScrollIfNeeded(final int y) {
+ final int deltaY = y - mMotionY;
+ final int distance = Math.abs(deltaY);
+ // TODO : Overscroll?
+ // final boolean overscroll = mScrollY != 0;
+ final boolean overscroll = false;
+ if (overscroll || distance > mTouchSlop) {
+ if (overscroll) {
+ mMotionCorrection = 0;
+ }
+ else {
+ mTouchMode = TOUCH_MODE_SCROLLING;
+ mMotionCorrection = deltaY > 0 ? mTouchSlop : -mTouchSlop;
+ }
+
+ final Handler handler = getHandler();
+ if (handler != null) {
+ handler.removeCallbacks(mPendingCheckForLongPress);
+ }
+ setPressed(false);
+ View motionView = getChildAt(mMotionPosition - mFirstPosition);
+ if (motionView != null) {
+ motionView.setPressed(false);
+ }
+ final ViewParent parent = getParent();
+ if (parent != null) {
+ parent.requestDisallowInterceptTouchEvent(true);
+ }
+
+ scrollIfNeeded(y);
+ return true;
+ }
+ return false;
+ }
+
+ private void scrollIfNeeded(final int y) {
+ if (DBG) Log.d(TAG, "scrollIfNeeded y: " + y);
+ final int rawDeltaY = y - mMotionY;
+ final int deltaY = rawDeltaY - mMotionCorrection;
+ int incrementalDeltaY = mLastY != Integer.MIN_VALUE ? y - mLastY : deltaY;
+
+ if (mTouchMode == TOUCH_MODE_SCROLLING) {
+ if (DBG) Log.d(TAG, "scrollIfNeeded TOUCH_MODE_SCROLLING");
+ if (y != mLastY) {
+ // stop our parent
+ if (Math.abs(rawDeltaY) > mTouchSlop) {
+ final ViewParent parent = getParent();
+ if (parent != null) {
+ parent.requestDisallowInterceptTouchEvent(true);
+ }
+ }
+
+ final int motionIndex;
+ if (mMotionPosition >= 0) {
+ motionIndex = mMotionPosition - mFirstPosition;
+ }
+ else {
+ // If we don't have a motion position that we can reliably track,
+ // pick something in the middle to make a best guess at things below.
+ motionIndex = getChildCount() / 2;
+ }
+
+ // No need to do all this work if we're not going to move anyway
+ boolean atEdge = false;
+ if (incrementalDeltaY != 0) {
+ atEdge = moveTheChildren(deltaY, incrementalDeltaY);
+ }
+
+ // Check to see if we have bumped into the scroll limit
+ View motionView = this.getChildAt(motionIndex);
+ if (motionView != null) {
+ if (atEdge) {
+ // TODO : edge effect & overscroll
+ }
+ mMotionY = y;
+ }
+ mLastY = y;
+ }
+
+ }
+ // TODO : ELSE SUPPORT OVERSCROLL!
+ }
+
+ private int findMotionRow(int y) {
+ int childCount = getChildCount();
+ if (childCount > 0) {
+ // always from the top
+ for (int i = 0; i < childCount; i++) {
+ View v = getChildAt(i);
+ if (y <= v.getBottom()) {
+ return mFirstPosition + i;
+ }
+ }
+ }
+ return INVALID_POSITION;
+ }
+
+ // //////////////////////////////////////////////////////////////////////////////////////////
+ // MOVING STUFF!
+ //
+ // It's not scrolling - we're just moving views!
+ // Move our views and implement view recycling to show new views if necessary
+
+ // move our views by deltaY - what's the incrementalDeltaY?
+ private boolean moveTheChildren(int deltaY, int incrementalDeltaY) {
+ if (DBG) Log.d(TAG, "moveTheChildren deltaY: " + deltaY + "incrementalDeltaY: " + incrementalDeltaY);
+ // there's nothing to move!
+ if (!hasChildren()) return true;
+
+ final int firstTop = getHighestChildTop();
+ final int lastBottom = getLowestChildBottom();
+
+ // "effective padding" In this case is the amount of padding that affects
+ // how much space should not be filled by items. If we don't clip to padding
+ // there is no effective padding.
+ int effectivePaddingTop = 0;
+ int effectivePaddingBottom = 0;
+ if (mClipToPadding) {
+ effectivePaddingTop = getListPaddingTop();
+ effectivePaddingBottom = getListPaddingBottom();
+ }
+
+ final int gridHeight = getHeight();
+ final int spaceAbove = effectivePaddingTop - getFirstChildTop();
+ final int end = gridHeight - effectivePaddingBottom;
+ final int spaceBelow = getLastChildBottom() - end;
+
+ final int height = gridHeight - getListPaddingBottom() - getListPaddingTop();
+
+ if (incrementalDeltaY < 0) {
+ incrementalDeltaY = Math.max(-(height - 1), incrementalDeltaY);
+ }
+ else {
+ incrementalDeltaY = Math.min(height - 1, incrementalDeltaY);
+ }
+
+ final int firstPosition = mFirstPosition;
+
+ int maxTop = getListPaddingTop();
+ int maxBottom = gridHeight - getListPaddingBottom();
+ int childCount = getChildCount();
+
+ final boolean cannotScrollDown = (firstPosition == 0 &&
+ firstTop >= maxTop && incrementalDeltaY >= 0);
+ final boolean cannotScrollUp = (firstPosition + childCount == mItemCount &&
+ lastBottom <= maxBottom && incrementalDeltaY <= 0);
+
+ if (DBG) {
+ Log.d(TAG, "moveTheChildren " +
+ " firstTop " + firstTop +
+ " maxTop " + maxTop +
+ " incrementalDeltaY " + incrementalDeltaY);
+ Log.d(TAG, "moveTheChildren " +
+ " lastBottom " + lastBottom +
+ " maxBottom " + maxBottom +
+ " incrementalDeltaY " + incrementalDeltaY);
+ }
+
+ if (cannotScrollDown) {
+ if (DBG) Log.d(TAG, "moveTheChildren cannotScrollDown " + cannotScrollDown);
+ return incrementalDeltaY != 0;
+ }
+
+ if (cannotScrollUp) {
+ if (DBG) Log.d(TAG, "moveTheChildren cannotScrollUp " + cannotScrollUp);
+ return incrementalDeltaY != 0;
+ }
+
+ final boolean isDown = incrementalDeltaY < 0;
+
+ final int headerViewsCount = getHeaderViewsCount();
+ final int footerViewsStart = mItemCount - getFooterViewsCount();
+
+ int start = 0;
+ int count = 0;
+
+ if (isDown) {
+ int top = -incrementalDeltaY;
+ if (mClipToPadding) {
+ top += getListPaddingTop();
+ }
+ for (int i = 0; i < childCount; i++) {
+ final View child = getChildAt(i);
+ if (child.getBottom() >= top) {
+ break;
+ }
+ else {
+ count++;
+ int position = firstPosition + i;
+ if (position >= headerViewsCount && position < footerViewsStart) {
+ mRecycleBin.addScrapView(child, position);
+ }
+ }
+ }
+ }
+ else {
+ int bottom = gridHeight - incrementalDeltaY;
+ if (mClipToPadding) {
+ bottom -= getListPaddingBottom();
+ }
+ for (int i = childCount - 1; i >= 0; i--) {
+ final View child = getChildAt(i);
+ if (child.getTop() <= bottom) {
+ break;
+ }
+ else {
+ start = i;
+ count++;
+ int position = firstPosition + i;
+ if (position >= headerViewsCount && position < footerViewsStart) {
+ mRecycleBin.addScrapView(child, position);
+ }
+ }
+ }
+ }
+
+ mBlockLayoutRequests = true;
+
+ if (count > 0) {
+ if (DBG) Log.d(TAG, "scrap - detachViewsFromParent start:" + start + " count:" + count);
+ detachViewsFromParent(start, count);
+ mRecycleBin.removeSkippedScrap();
+ onChildrenDetached(start, count);
+ }
+
+ // invalidate before moving the children to avoid unnecessary invalidate
+ // calls to bubble up from the children all the way to the top
+ if (!awakenScrollBars()) {
+ invalidate();
+ }
+
+ offsetChildrenTopAndBottom(incrementalDeltaY);
+
+ if (isDown) {
+ mFirstPosition += count;
+ }
+
+ final int absIncrementalDeltaY = Math.abs(incrementalDeltaY);
+ if (spaceAbove < absIncrementalDeltaY || spaceBelow < absIncrementalDeltaY) {
+ fillGap(isDown);
+ }
+
+ // TODO : touch mode selector handling
+ mBlockLayoutRequests = false;
+ invokeOnItemScrollListener();
+
+ return false;
+ }
+
+ protected void onChildrenDetached(final int start, final int count) {
+
+ }
+
+ // //////////////////////////////////////////////////////////////////////////////////////////
+ // FILLING THE GRID!
+ //
+
+ /**
+ * As we move and scroll and recycle views we want to fill the gap created with new views
+ */
+ protected void fillGap(boolean down) {
+ final int count = getChildCount();
+ if (down) {
+ // fill down from the top of the position below our last
+ int position = mFirstPosition + count;
+ final int startOffset = getChildTop(position);
+ fillDown(position, startOffset);
+ }
+ else {
+ // fill up from the bottom of the position above our first.
+ int position = mFirstPosition - 1;
+ final int startOffset = getChildBottom(position);
+ fillUp(position, startOffset);
+ }
+ adjustViewsAfterFillGap(down);
+ }
+
+ protected void adjustViewsAfterFillGap(boolean down) {
+ if (down) {
+ correctTooHigh(getChildCount());
+ }
+ else {
+ correctTooLow(getChildCount());
+ }
+ }
+
+ private View fillDown(int pos, int nextTop) {
+ if (DBG) Log.d(TAG, "fillDown - pos:" + pos + " nextTop:" + nextTop);
+
+ View selectedView = null;
+
+ int end = getHeight();
+ if (mClipToPadding) {
+ end -= getListPaddingBottom();
+ }
+
+ while ((nextTop < end || hasSpaceDown()) && pos < mItemCount) {
+ // TODO : add selection support
+ makeAndAddView(pos, nextTop, true, false);
+ pos++;
+ nextTop = getNextChildDownsTop(pos); // = child.getBottom();
+ }
+
+ return selectedView;
+ }
+
+ /***
+ * Override to tell filling flow to continue to fill up as we have space.
+ */
+ protected boolean hasSpaceDown() {
+ return false;
+ }
+
+ private View fillUp(int pos, int nextBottom) {
+ if (DBG) Log.d(TAG, "fillUp - position:" + pos + " nextBottom:" + nextBottom);
+ View selectedView = null;
+
+ int end = mClipToPadding ? getListPaddingTop() : 0;
+
+ while ((nextBottom > end || hasSpaceUp()) && pos >= 0) {
+ // TODO : add selection support
+ makeAndAddView(pos, nextBottom, false, false);
+ pos--;
+ nextBottom = getNextChildUpsBottom(pos);
+ if (DBG) Log.d(TAG, "fillUp next - position:" + pos + " nextBottom:" + nextBottom);
+ }
+
+ mFirstPosition = pos + 1;
+ return selectedView;
+ }
+
+ /***
+ * Override to tell filling flow to continue to fill up as we have space.
+ */
+ protected boolean hasSpaceUp() {
+ return false;
+ }
+
+ /**
+ * Fills the list from top to bottom, starting with mFirstPosition
+ */
+ private View fillFromTop(int nextTop) {
+ mFirstPosition = Math.min(mFirstPosition, mItemCount - 1);
+ if (mFirstPosition < 0) {
+ mFirstPosition = 0;
+ }
+ return fillDown(mFirstPosition, nextTop);
+ }
+
+ /**
+ * Put a specific item at a specific location on the screen and then build
+ * up and down from there.
+ *
+ * @param position The reference view to use as the starting point
+ * @param top Pixel offset from the top of this view to the top of the
+ * reference view.
+ * @return The selected view, or null if the selected view is outside the
+ * visible area.
+ */
+ private View fillSpecific(int position, int top) {
+ boolean tempIsSelected = false; // ain't no body got time for that @ Etsy
+ View temp = makeAndAddView(position, top, true, tempIsSelected);
+ // Possibly changed again in fillUp if we add rows above this one.
+ mFirstPosition = position;
+
+ View above;
+ View below;
+
+ int nextBottom = getNextChildUpsBottom(position - 1);
+ int nextTop = getNextChildDownsTop(position + 1);
+
+ above = fillUp(position - 1, nextBottom);
+ // This will correct for the top of the first view not touching the top of the list
+ adjustViewsUpOrDown();
+ below = fillDown(position + 1, nextTop);
+ int childCount = getChildCount();
+ if (childCount > 0) {
+ correctTooHigh(childCount);
+ }
+
+ if (tempIsSelected) {
+ return temp;
+ }
+ else if (above != null) {
+ return above;
+ }
+ else {
+ return below;
+ }
+ }
+
+ /**
+ * Gets a view either a new view an unused view?? or a recycled view and adds it to our children
+ */
+ private View makeAndAddView(int position, int y, boolean flowDown, boolean selected) {
+ View child;
+
+ onChildCreated(position, flowDown);
+
+ if (!mDataChanged) {
+ // Try to use an existing view for this position
+ child = mRecycleBin.getActiveView(position);
+ if (child != null) {
+
+ // Found it -- we're using an existing child
+ // This just needs to be positioned
+ setupChild(child, position, y, flowDown, selected, true);
+ return child;
+ }
+ }
+
+ // Make a new view for this position, or convert an unused view if possible
+ child = obtainView(position, mIsScrap);
+ // This needs to be positioned and measured
+ setupChild(child, position, y, flowDown, selected, mIsScrap[0]);
+
+ return child;
+ }
+
+ /**
+ * Add a view as a child and make sure it is measured (if necessary) and
+ * positioned properly.
+ *
+ * @param child The view to add
+ * @param position The position of this child
+ * @param y The y position relative to which this view will be positioned
+ * @param flowDown If true, align top edge to y. If false, align bottom
+ * edge to y.
+ * @param selected Is this position selected?
+ * @param recycled Has this view been pulled from the recycle bin? If so it
+ * does not need to be remeasured.
+ */
+ private void setupChild(View child, int position, int y, boolean flowDown,
+ boolean selected, boolean recycled) {
+ final boolean isSelected = false; // TODO : selected && shouldShowSelector();
+ final boolean updateChildSelected = isSelected != child.isSelected();
+ final int mode = mTouchMode;
+ final boolean isPressed = mode > TOUCH_MODE_DOWN && mode < TOUCH_MODE_SCROLLING &&
+ mMotionPosition == position;
+ final boolean updateChildPressed = isPressed != child.isPressed();
+ final boolean needToMeasure = !recycled || updateChildSelected || child.isLayoutRequested();
+
+ int itemViewType = mAdapter.getItemViewType(position);
+
+ LayoutParams layoutParams;
+ if (itemViewType == ITEM_VIEW_TYPE_HEADER_OR_FOOTER) {
+ layoutParams = generateWrapperLayoutParams(child);
+ }
+ else {
+ layoutParams = generateChildLayoutParams(child);
+ }
+
+ layoutParams.viewType = itemViewType;
+ layoutParams.position = position;
+
+ if (recycled || (layoutParams.recycledHeaderFooter &&
+ layoutParams.viewType == AdapterView.ITEM_VIEW_TYPE_HEADER_OR_FOOTER)) {
+ if (DBG) Log.d(TAG, "setupChild attachViewToParent position:" + position);
+ attachViewToParent(child, flowDown ? -1 : 0, layoutParams);
+ }
+ else {
+ if (DBG) Log.d(TAG, "setupChild addViewInLayout position:" + position);
+ if (layoutParams.viewType == AdapterView.ITEM_VIEW_TYPE_HEADER_OR_FOOTER) {
+ layoutParams.recycledHeaderFooter = true;
+ }
+ addViewInLayout(child, flowDown ? -1 : 0, layoutParams, true);
+ }
+
+ if (updateChildSelected) {
+ child.setSelected(isSelected);
+ }
+
+ if (updateChildPressed) {
+ child.setPressed(isPressed);
+ }
+
+ if (needToMeasure) {
+ if (DBG) Log.d(TAG, "setupChild onMeasureChild position:" + position);
+ onMeasureChild(child, layoutParams);
+ }
+ else {
+ if (DBG) Log.d(TAG, "setupChild cleanupLayoutState position:" + position);
+ cleanupLayoutState(child);
+ }
+
+ final int w = child.getMeasuredWidth();
+ final int h = child.getMeasuredHeight();
+ final int childTop = flowDown ? y : y - h;
+
+ if (DBG) {
+ Log.d(TAG, "setupChild position:" + position + " h:" + h + " w:" + w);
+ }
+
+ final int childrenLeft = getChildLeft(position);
+
+ if (needToMeasure) {
+ final int childRight = childrenLeft + w;
+ final int childBottom = childTop + h;
+ onLayoutChild(child, position, flowDown, childrenLeft, childTop, childRight, childBottom);
+ }
+ else {
+ onOffsetChild(child, position, flowDown, childrenLeft, childTop);
+ }
+
+ }
+
+ protected LayoutParams generateChildLayoutParams(final View child) {
+ return generateWrapperLayoutParams(child);
+ }
+
+ protected LayoutParams generateWrapperLayoutParams(final View child) {
+ LayoutParams layoutParams = null;
+
+ final ViewGroup.LayoutParams childParams = child.getLayoutParams();
+ if (childParams != null) {
+ if (childParams instanceof LayoutParams) {
+ layoutParams = (LayoutParams) childParams;
+ }
+ else {
+ layoutParams = new LayoutParams(childParams);
+ }
+ }
+ if (layoutParams == null) {
+ layoutParams = generateDefaultLayoutParams();
+ }
+
+ return layoutParams;
+ }
+
+
+ /**
+ * Measures a child view in the list. Should call
+ */
+ protected void onMeasureChild(final View child, final LayoutParams layoutParams) {
+ int childWidthSpec = ViewGroup.getChildMeasureSpec(mWidthMeasureSpec,
+ getListPaddingLeft() + getListPaddingRight(), layoutParams.width);
+ int lpHeight = layoutParams.height;
+ int childHeightSpec;
+ if (lpHeight > 0) {
+ childHeightSpec = MeasureSpec.makeMeasureSpec(lpHeight, MeasureSpec.EXACTLY);
+ }
+ else {
+ childHeightSpec = MeasureSpec.makeMeasureSpec(0, MeasureSpec.UNSPECIFIED);
+ }
+ child.measure(childWidthSpec, childHeightSpec);
+ }
+
+ @Override
+ protected LayoutParams generateDefaultLayoutParams() {
+ return new LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT,
+ ViewGroup.LayoutParams.WRAP_CONTENT, 0);
+ }
+
+ protected LayoutParams generateHeaderFooterLayoutParams(final View child) {
+ return new LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT,
+ ViewGroup.LayoutParams.WRAP_CONTENT, 0);
+ }
+
+ /**
+ * Get a view and have it show the data associated with the specified
+ * position. This is called when we have already discovered that the view is
+ * not available for reuse in the recycle bin. The only choices left are
+ * converting an old view or making a new one.
+ *
+ * @param position The position to display
+ * @param isScrap Array of at least 1 boolean, the first entry will become true if
+ * the returned view was taken from the scrap heap, false if otherwise.
+ * @return A view displaying the data associated with the specified position
+ */
+ private View obtainView(int position, boolean[] isScrap) {
+ isScrap[0] = false;
+ View scrapView;
+
+ scrapView = mRecycleBin.getScrapView(position);
+
+ View child;
+ if (scrapView != null) {
+ if (DBG) Log.d(TAG, "getView from scrap position:" + position);
+ child = mAdapter.getView(position, scrapView, this);
+
+ if (child != scrapView) {
+ mRecycleBin.addScrapView(scrapView, position);
+ }
+ else {
+ isScrap[0] = true;
+ }
+ }
+ else {
+ if (DBG) Log.d(TAG, "getView position:" + position);
+ child = mAdapter.getView(position, null, this);
+ }
+
+ return child;
+ }
+
+
+ /**
+ * Check if we have dragged the bottom of the list too high (we have pushed the
+ * top element off the top of the screen when we did not need to). Correct by sliding
+ * everything back down.
+ *
+ * @param childCount Number of children
+ */
+ private void correctTooHigh(int childCount) {
+ // First see if the last item is visible. If it is not, it is OK for the
+ // top of the list to be pushed up.
+ int lastPosition = mFirstPosition + childCount - 1;
+ if (lastPosition == mItemCount - 1 && childCount > 0) {
+
+ // ... and its bottom edge
+ final int lastBottom = getLowestChildBottom();
+
+ // This is bottom of our drawable area
+ final int end = (getBottom() - getTop()) - getListPaddingBottom();
+
+ // This is how far the bottom edge of the last view is from the bottom of the
+ // drawable area
+ int bottomOffset = end - lastBottom;
+
+ final int firstTop = getHighestChildTop();
+
+ // Make sure we are 1) Too high, and 2) Either there are more rows above the
+ // first row or the first row is scrolled off the top of the drawable area
+ if (bottomOffset > 0 && (mFirstPosition > 0 || firstTop < getListPaddingTop())) {
+ if (mFirstPosition == 0) {
+ // Don't pull the top too far down
+ bottomOffset = Math.min(bottomOffset, getListPaddingTop() - firstTop);
+ }
+ // Move everything down
+ offsetChildrenTopAndBottom(bottomOffset);
+ if (mFirstPosition > 0) {
+ // Fill the gap that was opened above mFirstPosition with more rows, if
+ // possible
+ int previousPosition = mFirstPosition - 1;
+ fillUp(previousPosition, getNextChildUpsBottom(previousPosition));
+ // Close up the remaining gap
+ adjustViewsUpOrDown();
+ }
+
+ }
+ }
+ }
+
+ /**
+ * Check if we have dragged the bottom of the list too low (we have pushed the
+ * bottom element off the bottom of the screen when we did not need to). Correct by sliding
+ * everything back up.
+ *
+ * @param childCount Number of children
+ */
+ private void correctTooLow(int childCount) {
+ // First see if the first item is visible. If it is not, it is OK for the
+ // bottom of the list to be pushed down.
+ if (mFirstPosition == 0 && childCount > 0) {
+
+ // ... and its top edge
+ final int firstTop = getHighestChildTop();
+
+ // This is top of our drawable area
+ final int start = getListPaddingTop();
+
+ // This is bottom of our drawable area
+ final int end = (getTop() - getBottom()) - getListPaddingBottom();
+
+ // This is how far the top edge of the first view is from the top of the
+ // drawable area
+ int topOffset = firstTop - start;
+ final int lastBottom = getLowestChildBottom();
+
+ int lastPosition = mFirstPosition + childCount - 1;
+
+ // Make sure we are 1) Too low, and 2) Either there are more rows below the
+ // last row or the last row is scrolled off the bottom of the drawable area
+ if (topOffset > 0) {
+ if (lastPosition < mItemCount - 1 || lastBottom > end) {
+ if (lastPosition == mItemCount - 1) {
+ // Don't pull the bottom too far up
+ topOffset = Math.min(topOffset, lastBottom - end);
+ }
+ // Move everything up
+ offsetChildrenTopAndBottom(-topOffset);
+ if (lastPosition < mItemCount - 1) {
+ // Fill the gap that was opened below the last position with more rows, if
+ // possible
+ int nextPosition = lastPosition + 1;
+ fillDown(nextPosition, getNextChildDownsTop(nextPosition));
+ // Close up the remaining gap
+ adjustViewsUpOrDown();
+ }
+ }
+ else if (lastPosition == mItemCount - 1) {
+ adjustViewsUpOrDown();
+ }
+ }
+ }
+ }
+
+ /**
+ * Make sure views are touching the top or bottom edge, as appropriate for
+ * our gravity
+ */
+ private void adjustViewsUpOrDown() {
+ final int childCount = getChildCount();
+ int delta;
+
+ if (childCount > 0) {
+ // Uh-oh -- we came up short. Slide all views up to make them
+ // align with the top
+ delta = getHighestChildTop() - getListPaddingTop();
+ if (delta < 0) {
+ // We only are looking to see if we are too low, not too high
+ delta = 0;
+ }
+
+ if (delta != 0) {
+ offsetChildrenTopAndBottom(-delta);
+ }
+ }
+ }
+
+ // //////////////////////////////////////////////////////////////////////////////////////////
+ // PROTECTED POSITIONING EXTENSABLES
+ //
+
+ /**
+ * Override
+ */
+ protected void onChildCreated(final int position, final boolean flowDown) {
+
+ }
+
+ /**
+ * Override to position the child as you so wish
+ */
+ protected void onLayoutChild(final View child, final int position,
+ final boolean flowDown, final int childrenLeft, final int childTop,
+ final int childRight, final int childBottom) {
+ child.layout(childrenLeft, childTop, childRight, childBottom);
+ }
+
+ /**
+ * Override to offset the child as you so wish
+ */
+ protected void onOffsetChild(final View child, final int position,
+ final boolean flowDown, final int childrenLeft, final int childTop) {
+ child.offsetLeftAndRight(childrenLeft - child.getLeft());
+ child.offsetTopAndBottom(childTop - child.getTop());
+ }
+
+ /**
+ * Override to set you custom listviews child to a specific left location
+ *
+ * @return the left location to layout the child for the given position
+ */
+ protected int getChildLeft(final int position) {
+ return getListPaddingLeft();
+ }
+
+ /**
+ * Override to set you custom listviews child to a specific top location
+ *
+ * @return the top location to layout the child for the given position
+ */
+ protected int getChildTop(final int position) {
+ int count = getChildCount();
+ int paddingTop = 0;
+ if (mClipToPadding) {
+ paddingTop = getListPaddingTop();
+ }
+ return count > 0 ? getChildAt(count - 1).getBottom() : paddingTop;
+ }
+
+ /**
+ * Override to set you custom listviews child to a bottom top location
+ *
+ * @return the bottom location to layout the child for the given position
+ */
+ protected int getChildBottom(final int position) {
+ int count = getChildCount();
+ int paddingBottom = 0;
+ if (mClipToPadding) {
+ paddingBottom = getListPaddingBottom();
+ }
+ return count > 0 ? getChildAt(0).getTop() : getHeight() - paddingBottom;
+ }
+
+ protected int getNextChildDownsTop(final int position) {
+ final int count = getChildCount();
+ return count > 0 ? getChildAt(count - 1).getBottom() : 0;
+ }
+
+ protected int getNextChildUpsBottom(final int position) {
+ final int count = getChildCount();
+ if (count == 0) {
+ return 0;
+ }
+ return count > 0 ? getChildAt(0).getTop() : 0;
+ }
+
+ protected int getFirstChildTop() {
+ return hasChildren() ? getChildAt(0).getTop() : 0;
+ }
+
+ protected int getHighestChildTop() {
+ return hasChildren() ? getChildAt(0).getTop() : 0;
+ }
+
+ protected int getLastChildBottom() {
+ return hasChildren() ? getChildAt(getChildCount() - 1).getBottom() : 0;
+ }
+
+ protected int getLowestChildBottom() {
+ return hasChildren() ? getChildAt(getChildCount() - 1).getBottom() : 0;
+ }
+
+ protected boolean hasChildren() {
+ return getChildCount() > 0;
+ }
+
+ protected void offsetChildrenTopAndBottom(int offset) {
+ if (DBG) Log.d(TAG, "offsetChildrenTopAndBottom: " + offset);
+ final int count = getChildCount();
+ for (int i = 0; i < count; i++) {
+ final View v = getChildAt(i);
+ v.offsetTopAndBottom(offset);
+ }
+ }
+
+ @Override
+ public int getFirstVisiblePosition() {
+ return Math.max(0, mFirstPosition - getHeaderViewsCount());
+ }
+
+ @Override
+ public int getLastVisiblePosition() {
+ return Math.min(mFirstPosition + getChildCount() - 1, mAdapter != null ? mAdapter.getCount() - 1 : 0);
+ }
+
+ // //////////////////////////////////////////////////////////////////////////////////////////
+ // FLING
+ //
+
+ private void initOrResetVelocityTracker() {
+ if (mVelocityTracker == null) {
+ mVelocityTracker = VelocityTracker.obtain();
+ }
+ else {
+ mVelocityTracker.clear();
+ }
+ }
+
+ private void initVelocityTrackerIfNotExists() {
+ if (mVelocityTracker == null) {
+ mVelocityTracker = VelocityTracker.obtain();
+ }
+ }
+
+ private void recycleVelocityTracker() {
+ if (mVelocityTracker != null) {
+ mVelocityTracker.recycle();
+ mVelocityTracker = null;
+ }
+ }
+
+ private void startFlingRunnable(final float velocity) {
+ if (mFlingRunnable == null) {
+ mFlingRunnable = new FlingRunnable();
+ }
+ mFlingRunnable.start((int) -velocity);
+ }
+
+ private void stopFlingRunnable() {
+ if (mFlingRunnable != null) {
+ mFlingRunnable.endFling();
+ }
+ }
+
+ // //////////////////////////////////////////////////////////////////////////////////////////
+ // FLING RUNNABLE
+ //
+
+ /**
+ * Responsible for fling behavior. Use {@link #start(int)} to
+ * initiate a fling. Each frame of the fling is handled in {@link #run()}.
+ * A FlingRunnable will keep re-posting itself until the fling is done.
+ */
+ private class FlingRunnable implements Runnable {
+ /**
+ * Tracks the decay of a fling scroll
+ */
+ private final Scroller mScroller;
+
+ /**
+ * Y value reported by mScroller on the previous fling
+ */
+ private int mLastFlingY;
+
+ FlingRunnable() {
+ mScroller = new Scroller(getContext());
+ }
+
+ void start(int initialVelocity) {
+ int initialY = initialVelocity < 0 ? Integer.MAX_VALUE : 0;
+ mLastFlingY = initialY;
+ mScroller.forceFinished(true);
+ mScroller.fling(0, initialY, 0, initialVelocity, 0, Integer.MAX_VALUE, 0, Integer.MAX_VALUE);
+ mTouchMode = TOUCH_MODE_FLINGING;
+ postOnAnimate(this);
+ }
+
+ void startScroll(int distance, int duration) {
+ int initialY = distance < 0 ? Integer.MAX_VALUE : 0;
+ mLastFlingY = initialY;
+ mScroller.startScroll(0, initialY, 0, distance, duration);
+ mTouchMode = TOUCH_MODE_FLINGING;
+ postOnAnimate(this);
+ }
+
+ private void endFling() {
+ mLastFlingY = 0;
+ mTouchMode = TOUCH_MODE_IDLE;
+
+ reportScrollStateChange(OnScrollListener.SCROLL_STATE_IDLE);
+ removeCallbacks(this);
+
+ mScroller.forceFinished(true);
+ }
+
+ @Override
+ public void run() {
+ switch (mTouchMode) {
+ default:
+ return;
+
+ case TOUCH_MODE_FLINGING: {
+ if (mItemCount == 0 || getChildCount() == 0) {
+ endFling();
+ return;
+ }
+
+ final Scroller scroller = mScroller;
+ boolean more = scroller.computeScrollOffset();
+ final int y = scroller.getCurrY();
+
+ // Flip sign to convert finger direction to list items direction
+ // (e.g. finger moving down means list is moving towards the top)
+ int delta = mLastFlingY - y;
+
+ // Pretend that each frame of a fling scroll is a touch scroll
+ if (delta > 0) {
+ // List is moving towards the top. Use first view as mMotionPosition
+ mMotionPosition = mFirstPosition;
+ // Don't fling more than 1 screen
+ delta = Math.min(getHeight() - getPaddingBottom() - getPaddingTop() - 1, delta);
+ }
+ else {
+ // List is moving towards the bottom. Use last view as mMotionPosition
+ int offsetToLast = getChildCount() - 1;
+ mMotionPosition = mFirstPosition + offsetToLast;
+
+ // Don't fling more than 1 screen
+ delta = Math.max(-(getHeight() - getPaddingBottom() - getPaddingTop() - 1), delta);
+ }
+
+ final boolean atEnd = moveTheChildren(delta, delta);
+
+ if (more && !atEnd) {
+ invalidate();
+ mLastFlingY = y;
+ postOnAnimate(this);
+ }
+ else {
+ endFling();
+ }
+ break;
+ }
+ }
+ }
+
+ }
+
+ private void postOnAnimate(Runnable runnable) {
+ ViewCompat.postOnAnimation(this, runnable);
+ }
+
+ // //////////////////////////////////////////////////////////////////////////////////////////
+ // SCROLL LISTENER
+ //
+
+ /**
+ * Notify any scroll listeners of our current touch mode
+ */
+ public void notifyTouchMode() {
+ // only tell the scroll listener about some things we want it to know
+ switch (mTouchMode) {
+ case TOUCH_MODE_SCROLLING:
+ reportScrollStateChange(OnScrollListener.SCROLL_STATE_TOUCH_SCROLL);
+ break;
+ case TOUCH_MODE_FLINGING:
+ reportScrollStateChange(OnScrollListener.SCROLL_STATE_FLING);
+ break;
+ case TOUCH_MODE_IDLE:
+ reportScrollStateChange(OnScrollListener.SCROLL_STATE_IDLE);
+ break;
+ }
+ }
+
+ private OnScrollListener mOnScrollListener;
+
+ @Override
+ public void setOnScrollListener(OnScrollListener scrollListener) {
+ super.setOnScrollListener(scrollListener);
+ mOnScrollListener = scrollListener;
+ }
+
+ void reportScrollStateChange(int newState) {
+ if (newState != mScrollState) {
+ mScrollState = newState;
+ if (mOnScrollListener != null) {
+ mOnScrollListener.onScrollStateChanged(this, newState);
+ }
+ }
+ }
+
+ void invokeOnItemScrollListener() {
+ if (mOnScrollListener != null) {
+ mOnScrollListener.onScroll(this, mFirstPosition, getChildCount(), mItemCount);
+ }
+ }
+
+ /**
+ * Update the status of the list based on the empty parameter. If empty is true and
+ * we have an empty view, display it. In all the other cases, make sure that the listview
+ * is VISIBLE and that the empty view is GONE (if it's not null).
+ */
+ @SuppressLint("WrongCall")
+ private void updateEmptyStatus() {
+ boolean empty = getAdapter() == null || getAdapter().isEmpty();
+ if (isInFilterMode()) {
+ empty = false;
+ }
+
+ View emptyView = getEmptyView();
+ if (empty) {
+ if (emptyView != null) {
+ emptyView.setVisibility(View.VISIBLE);
+ setVisibility(View.GONE);
+ }
+ else {
+ // If the caller just removed our empty view, make sure the list view is visible
+ setVisibility(View.VISIBLE);
+ }
+
+ // We are now GONE, so pending layouts will not be dispatched.
+ // Force one here to make sure that the state of the list matches
+ // the state of the adapter.
+ if (mDataChanged) {
+ this.onLayout(false, getLeft(), getTop(), getRight(), getBottom());
+ }
+ }
+ else {
+ if (emptyView != null) {
+ emptyView.setVisibility(View.GONE);
+ }
+ setVisibility(View.VISIBLE);
+ }
+ }
+
+ // //////////////////////////////////////////////////////////////////////////////////////////
+ // ADAPTER OBSERVER
+ //
+
+ class AdapterDataSetObserver extends DataSetObserver {
+
+ private Parcelable mInstanceState = null;
+
+ @Override
+ public void onChanged() {
+ mDataChanged = true;
+ mOldItemCount = mItemCount;
+ mItemCount = getAdapter().getCount();
+
+ mRecycleBin.clearTransientStateViews();
+
+ // Detect the case where a cursor that was previously invalidated has
+ // been repopulated with new data.
+ if (ExtendableListView.this.getAdapter().hasStableIds() && mInstanceState != null
+ && mOldItemCount == 0 && mItemCount > 0) {
+ ExtendableListView.this.onRestoreInstanceState(mInstanceState);
+ mInstanceState = null;
+ }
+ else {
+ rememberSyncState();
+ }
+
+ updateEmptyStatus();
+ requestLayout();
+ }
+
+ @Override
+ public void onInvalidated() {
+ mDataChanged = true;
+
+ if (ExtendableListView.this.getAdapter().hasStableIds()) {
+ // Remember the current state for the case where our hosting activity is being
+ // stopped and later restarted
+ mInstanceState = ExtendableListView.this.onSaveInstanceState();
+ }
+
+ // Data is invalid so we should reset our state
+ mOldItemCount = mItemCount;
+ mItemCount = 0;
+ mNeedSync = false;
+
+ updateEmptyStatus();
+ requestLayout();
+ }
+
+ public void clearSavedState() {
+ mInstanceState = null;
+ }
+ }
+
+
+ // //////////////////////////////////////////////////////////////////////////////////////////
+ // LAYOUT PARAMS
+ //
+
+ /**
+ * Re-implementing some properties in {@link android.view.ViewGroup.LayoutParams} since they're package
+ * private but we want to appear to be an extension of the existing class.
+ */
+ public static class LayoutParams extends AbsListView.LayoutParams {
+
+ boolean recycledHeaderFooter;
+
+ // Position of the view in the data
+ int position;
+
+ // adapter ID the view represents fetched from the adapter if it's stable
+ long itemId = -1;
+
+ // adapter view type
+ int viewType;
+
+ public LayoutParams(Context c, AttributeSet attrs) {
+ super(c, attrs);
+ }
+
+ public LayoutParams(int w, int h) {
+ super(w, h);
+ }
+
+ public LayoutParams(int w, int h, int viewType) {
+ super(w, h);
+ this.viewType = viewType;
+ }
+
+ public LayoutParams(ViewGroup.LayoutParams source) {
+ super(source);
+ }
+
+ }
+
+ // //////////////////////////////////////////////////////////////////////////////////////////
+ // RecycleBin
+ //
+
+ /**
+ * Note there's no RecyclerListener. The caller shouldn't have a need and we can add it later.
+ */
+ class RecycleBin {
+
+ /**
+ * The position of the first view stored in mActiveViews.
+ */
+ private int mFirstActivePosition;
+
+ /**
+ * Views that were on screen at the start of layout. This array is populated at the start of
+ * layout, and at the end of layout all view in mActiveViews are moved to mScrapViews.
+ * Views in mActiveViews represent a contiguous range of Views, with position of the first
+ * view store in mFirstActivePosition.
+ */
+ private View[] mActiveViews = new View[0];
+
+ /**
+ * Unsorted views that can be used by the adapter as a convert view.
+ */
+ private ArrayList[] mScrapViews;
+
+ private int mViewTypeCount;
+
+ private ArrayList mCurrentScrap;
+
+ private ArrayList mSkippedScrap;
+
+ private SparseArrayCompat mTransientStateViews;
+
+ public void setViewTypeCount(int viewTypeCount) {
+ if (viewTypeCount < 1) {
+ throw new IllegalArgumentException("Can't have a viewTypeCount < 1");
+ }
+ //noinspection unchecked
+ ArrayList[] scrapViews = new ArrayList[viewTypeCount];
+ for (int i = 0; i < viewTypeCount; i++) {
+ scrapViews[i] = new ArrayList();
+ }
+ mViewTypeCount = viewTypeCount;
+ mCurrentScrap = scrapViews[0];
+ mScrapViews = scrapViews;
+ }
+
+ public void markChildrenDirty() {
+ if (mViewTypeCount == 1) {
+ final ArrayList scrap = mCurrentScrap;
+ final int scrapCount = scrap.size();
+ for (int i = 0; i < scrapCount; i++) {
+ scrap.get(i).forceLayout();
+ }
+ }
+ else {
+ final int typeCount = mViewTypeCount;
+ for (int i = 0; i < typeCount; i++) {
+ final ArrayList scrap = mScrapViews[i];
+ final int scrapCount = scrap.size();
+ for (int j = 0; j < scrapCount; j++) {
+ scrap.get(j).forceLayout();
+ }
+ }
+ }
+ if (mTransientStateViews != null) {
+ final int count = mTransientStateViews.size();
+ for (int i = 0; i < count; i++) {
+ mTransientStateViews.valueAt(i).forceLayout();
+ }
+ }
+ }
+
+ public boolean shouldRecycleViewType(int viewType) {
+ return viewType >= 0;
+ }
+
+ /**
+ * Clears the scrap heap.
+ */
+ void clear() {
+ if (mViewTypeCount == 1) {
+ final ArrayList scrap = mCurrentScrap;
+ final int scrapCount = scrap.size();
+ for (int i = 0; i < scrapCount; i++) {
+ removeDetachedView(scrap.remove(scrapCount - 1 - i), false);
+ }
+ }
+ else {
+ final int typeCount = mViewTypeCount;
+ for (int i = 0; i < typeCount; i++) {
+ final ArrayList scrap = mScrapViews[i];
+ final int scrapCount = scrap.size();
+ for (int j = 0; j < scrapCount; j++) {
+ removeDetachedView(scrap.remove(scrapCount - 1 - j), false);
+ }
+ }
+ }
+ if (mTransientStateViews != null) {
+ mTransientStateViews.clear();
+ }
+ }
+
+ /**
+ * Fill ActiveViews with all of the children of the AbsListView.
+ *
+ * @param childCount The minimum number of views mActiveViews should hold
+ * @param firstActivePosition The position of the first view that will be stored in
+ * mActiveViews
+ */
+ void fillActiveViews(int childCount, int firstActivePosition) {
+ if (mActiveViews.length < childCount) {
+ mActiveViews = new View[childCount];
+ }
+ mFirstActivePosition = firstActivePosition;
+
+ final View[] activeViews = mActiveViews;
+ for (int i = 0; i < childCount; i++) {
+ View child = getChildAt(i);
+ LayoutParams lp = (LayoutParams) child.getLayoutParams();
+ // Don't put header or footer views into the scrap heap
+ if (lp != null && lp.viewType != ITEM_VIEW_TYPE_HEADER_OR_FOOTER) {
+ // Note: We do place AdapterView.ITEM_VIEW_TYPE_IGNORE in active views.
+ // However, we will NOT place them into scrap views.
+ activeViews[i] = child;
+ }
+ }
+ }
+
+ /**
+ * Get the view corresponding to the specified position. The view will be removed from
+ * mActiveViews if it is found.
+ *
+ * @param position The position to look up in mActiveViews
+ * @return The view if it is found, null otherwise
+ */
+ View getActiveView(int position) {
+ int index = position - mFirstActivePosition;
+ final View[] activeViews = mActiveViews;
+ if (index >= 0 && index < activeViews.length) {
+ final View match = activeViews[index];
+ activeViews[index] = null;
+ return match;
+ }
+ return null;
+ }
+
+ View getTransientStateView(int position) {
+ if (mTransientStateViews == null) {
+ return null;
+ }
+ final int index = mTransientStateViews.indexOfKey(position);
+ if (index < 0) {
+ return null;
+ }
+ final View result = mTransientStateViews.valueAt(index);
+ mTransientStateViews.removeAt(index);
+ return result;
+ }
+
+ /**
+ * Dump any currently saved views with transient state.
+ */
+ void clearTransientStateViews() {
+ if (mTransientStateViews != null) {
+ mTransientStateViews.clear();
+ }
+ }
+
+ /**
+ * @return A view from the ScrapViews collection. These are unordered.
+ */
+ View getScrapView(int position) {
+ if (mViewTypeCount == 1) {
+ return retrieveFromScrap(mCurrentScrap, position);
+ }
+ else {
+ int whichScrap = mAdapter.getItemViewType(position);
+ if (whichScrap >= 0 && whichScrap < mScrapViews.length) {
+ return retrieveFromScrap(mScrapViews[whichScrap], position);
+ }
+ }
+ return null;
+ }
+
+ /**
+ * Put a view into the ScrapViews list. These views are unordered.
+ *
+ * @param scrap The view to add
+ */
+ void addScrapView(View scrap, int position) {
+ if (DBG) Log.d(TAG, "addScrapView position = " + position);
+
+ LayoutParams lp = (LayoutParams) scrap.getLayoutParams();
+ if (lp == null) {
+ return;
+ }
+
+ lp.position = position;
+
+ // Don't put header or footer views or views that should be ignored
+ // into the scrap heap
+ int viewType = lp.viewType;
+ final boolean scrapHasTransientState = ViewCompat.hasTransientState(scrap);
+ if (!shouldRecycleViewType(viewType) || scrapHasTransientState) {
+ if (viewType != ITEM_VIEW_TYPE_HEADER_OR_FOOTER || scrapHasTransientState) {
+ if (mSkippedScrap == null) {
+ mSkippedScrap = new ArrayList();
+ }
+ mSkippedScrap.add(scrap);
+ }
+ if (scrapHasTransientState) {
+ if (mTransientStateViews == null) {
+ mTransientStateViews = new SparseArrayCompat();
+ }
+ mTransientStateViews.put(position, scrap);
+ }
+ return;
+ }
+
+ if (mViewTypeCount == 1) {
+ mCurrentScrap.add(scrap);
+ }
+ else {
+ mScrapViews[viewType].add(scrap);
+ }
+ }
+
+ /**
+ * Finish the removal of any views that skipped the scrap heap.
+ */
+ void removeSkippedScrap() {
+ if (mSkippedScrap == null) {
+ return;
+ }
+ final int count = mSkippedScrap.size();
+ for (int i = 0; i < count; i++) {
+ removeDetachedView(mSkippedScrap.get(i), false);
+ }
+ mSkippedScrap.clear();
+ }
+
+ /**
+ * Move all views remaining in mActiveViews to mScrapViews.
+ */
+ void scrapActiveViews() {
+ final View[] activeViews = mActiveViews;
+ final boolean multipleScraps = mViewTypeCount > 1;
+
+ ArrayList scrapViews = mCurrentScrap;
+ final int count = activeViews.length;
+ for (int i = count - 1; i >= 0; i--) {
+ final View victim = activeViews[i];
+ if (victim != null) {
+ final LayoutParams lp = (LayoutParams) victim.getLayoutParams();
+ activeViews[i] = null;
+
+ final boolean scrapHasTransientState = ViewCompat.hasTransientState(victim);
+ int viewType = lp.viewType;
+
+ if (!shouldRecycleViewType(viewType) || scrapHasTransientState) {
+ // Do not move views that should be ignored
+ if (viewType != ITEM_VIEW_TYPE_HEADER_OR_FOOTER || scrapHasTransientState) {
+ removeDetachedView(victim, false);
+ }
+ if (scrapHasTransientState) {
+ if (mTransientStateViews == null) {
+ mTransientStateViews = new SparseArrayCompat();
+ }
+ mTransientStateViews.put(mFirstActivePosition + i, victim);
+ }
+ continue;
+ }
+
+ if (multipleScraps) {
+ scrapViews = mScrapViews[viewType];
+ }
+ lp.position = mFirstActivePosition + i;
+ scrapViews.add(victim);
+ }
+ }
+
+ pruneScrapViews();
+ }
+
+ /**
+ * Makes sure that the size of mScrapViews does not exceed the size of mActiveViews.
+ * (This can happen if an adapter does not recycle its views).
+ */
+ private void pruneScrapViews() {
+ final int maxViews = mActiveViews.length;
+ final int viewTypeCount = mViewTypeCount;
+ final ArrayList[] scrapViews = mScrapViews;
+ for (int i = 0; i < viewTypeCount; ++i) {
+ final ArrayList scrapPile = scrapViews[i];
+ int size = scrapPile.size();
+ final int extras = size - maxViews;
+ size--;
+ for (int j = 0; j < extras; j++) {
+ removeDetachedView(scrapPile.remove(size--), false);
+ }
+ }
+
+ if (mTransientStateViews != null) {
+ for (int i = 0; i < mTransientStateViews.size(); i++) {
+ final View v = mTransientStateViews.valueAt(i);
+ if (!ViewCompat.hasTransientState(v)) {
+ mTransientStateViews.removeAt(i);
+ i--;
+ }
+ }
+ }
+ }
+
+ /**
+ * Updates the cache color hint of all known views.
+ *
+ * @param color The new cache color hint.
+ */
+ void setCacheColorHint(int color) {
+ if (mViewTypeCount == 1) {
+ final ArrayList scrap = mCurrentScrap;
+ final int scrapCount = scrap.size();
+ for (int i = 0; i < scrapCount; i++) {
+ scrap.get(i).setDrawingCacheBackgroundColor(color);
+ }
+ }
+ else {
+ final int typeCount = mViewTypeCount;
+ for (int i = 0; i < typeCount; i++) {
+ final ArrayList scrap = mScrapViews[i];
+ final int scrapCount = scrap.size();
+ for (int j = 0; j < scrapCount; j++) {
+ scrap.get(j).setDrawingCacheBackgroundColor(color);
+ }
+ }
+ }
+ // Just in case this is called during a layout pass
+ final View[] activeViews = mActiveViews;
+ final int count = activeViews.length;
+ for (int i = 0; i < count; ++i) {
+ final View victim = activeViews[i];
+ if (victim != null) {
+ victim.setDrawingCacheBackgroundColor(color);
+ }
+ }
+ }
+ }
+
+ static View retrieveFromScrap(ArrayList scrapViews, int position) {
+ int size = scrapViews.size();
+ if (size > 0) {
+ // See if we still have a view for this position.
+ for (int i = 0; i < size; i++) {
+ View view = scrapViews.get(i);
+ if (((LayoutParams) view.getLayoutParams()).position == position) {
+ scrapViews.remove(i);
+ return view;
+ }
+ }
+ return scrapViews.remove(size - 1);
+ }
+ else {
+ return null;
+ }
+ }
+
+ // //////////////////////////////////////////////////////////////////////////////////////////
+ // OUR STATE
+ //
+
+ /**
+ * Position from which to start looking for mSyncRowId
+ */
+ protected int mSyncPosition;
+
+ /**
+ * The offset in pixels from the top of the AdapterView to the top
+ * of the view to select during the next layout.
+ */
+ protected int mSpecificTop;
+
+ /**
+ * Row id to look for when data has changed
+ */
+ long mSyncRowId = INVALID_ROW_ID;
+
+ /**
+ * Height of the view when mSyncPosition and mSyncRowId where set
+ */
+ long mSyncHeight;
+
+ /**
+ * True if we need to sync to mSyncRowId
+ */
+ boolean mNeedSync = false;
+
+
+ private ListSavedState mSyncState;
+
+
+ /**
+ * Remember enough information to restore the screen state when the data has
+ * changed.
+ */
+ void rememberSyncState() {
+ if (getChildCount() > 0) {
+ mNeedSync = true;
+ mSyncHeight = getHeight();
+ // Sync the based on the offset of the first view
+ View v = getChildAt(0);
+ ListAdapter adapter = getAdapter();
+ if (mFirstPosition >= 0 && mFirstPosition < adapter.getCount()) {
+ mSyncRowId = adapter.getItemId(mFirstPosition);
+ }
+ else {
+ mSyncRowId = NO_ID;
+ }
+ if (v != null) {
+ mSpecificTop = v.getTop();
+ }
+ mSyncPosition = mFirstPosition;
+ }
+ }
+
+ private void clearState() {
+ // cleanup headers and footers before removing the views
+ clearRecycledState(mHeaderViewInfos);
+ clearRecycledState(mFooterViewInfos);
+
+ removeAllViewsInLayout();
+ mFirstPosition = 0;
+ mDataChanged = false;
+ mRecycleBin.clear();
+ mNeedSync = false;
+ mSyncState = null;
+ mLayoutMode = LAYOUT_NORMAL;
+ invalidate();
+ }
+
+ private void clearRecycledState(ArrayList infos) {
+ if (infos == null) return;
+ for (FixedViewInfo info : infos) {
+ final View child = info.view;
+ final ViewGroup.LayoutParams p = child.getLayoutParams();
+
+ if (p instanceof LayoutParams) {
+ ((LayoutParams) p).recycledHeaderFooter = false;
+ }
+ }
+ }
+
+ public static class ListSavedState extends ClassLoaderSavedState {
+ protected long selectedId;
+ protected long firstId;
+ protected int viewTop;
+ protected int position;
+ protected int height;
+
+ /**
+ * Constructor called from {@link android.widget.AbsListView#onSaveInstanceState()}
+ */
+ public ListSavedState(Parcelable superState) {
+ super(superState, AbsListView.class.getClassLoader());
+ }
+
+ /**
+ * Constructor called from {@link #CREATOR}
+ */
+ public ListSavedState(Parcel in) {
+ super(in);
+ selectedId = in.readLong();
+ firstId = in.readLong();
+ viewTop = in.readInt();
+ position = in.readInt();
+ height = in.readInt();
+ }
+
+ @Override
+ public void writeToParcel(Parcel out, int flags) {
+ super.writeToParcel(out, flags);
+ out.writeLong(selectedId);
+ out.writeLong(firstId);
+ out.writeInt(viewTop);
+ out.writeInt(position);
+ out.writeInt(height);
+ }
+
+ @Override
+ public String toString() {
+ return "ExtendableListView.ListSavedState{"
+ + Integer.toHexString(System.identityHashCode(this))
+ + " selectedId=" + selectedId
+ + " firstId=" + firstId
+ + " viewTop=" + viewTop
+ + " position=" + position
+ + " height=" + height + "}";
+ }
+
+ public static final Creator CREATOR
+ = new Creator() {
+ @Override
+ public ListSavedState createFromParcel(Parcel in) {
+ return new ListSavedState(in);
+ }
+
+ @Override
+ public ListSavedState[] newArray(int size) {
+ return new ListSavedState[size];
+ }
+ };
+ }
+
+
+ @Override
+ public Parcelable onSaveInstanceState() {
+
+ Parcelable superState = super.onSaveInstanceState();
+ ListSavedState ss = new ListSavedState(superState);
+
+ if (mSyncState != null) {
+ // Just keep what we last restored.
+ ss.selectedId = mSyncState.selectedId;
+ ss.firstId = mSyncState.firstId;
+ ss.viewTop = mSyncState.viewTop;
+ ss.position = mSyncState.position;
+ ss.height = mSyncState.height;
+ return ss;
+ }
+
+ boolean haveChildren = getChildCount() > 0 && mItemCount > 0;
+ ss.selectedId = getSelectedItemId();
+ ss.height = getHeight();
+
+ // TODO : sync selection when we handle it
+ if (haveChildren && mFirstPosition > 0) {
+ // Remember the position of the first child.
+ // We only do this if we are not currently at the top of
+ // the list, for two reasons:
+ // (1) The list may be in the process of becoming empty, in
+ // which case mItemCount may not be 0, but if we try to
+ // ask for any information about position 0 we will crash.
+ // (2) Being "at the top" seems like a special case, anyway,
+ // and the user wouldn't expect to end up somewhere else when
+ // they revisit the list even if its content has changed.
+ View v = getChildAt(0);
+ ss.viewTop = v.getTop();
+ int firstPos = mFirstPosition;
+ if (firstPos >= mItemCount) {
+ firstPos = mItemCount - 1;
+ }
+ ss.position = firstPos;
+ ss.firstId = mAdapter.getItemId(firstPos);
+ }
+ else {
+ ss.viewTop = 0;
+ ss.firstId = INVALID_POSITION;
+ ss.position = 0;
+ }
+
+ return ss;
+ }
+
+ @Override
+ public void onRestoreInstanceState(Parcelable state) {
+ ListSavedState ss = (ListSavedState) state;
+ super.onRestoreInstanceState(ss.getSuperState());
+ mDataChanged = true;
+
+ mSyncHeight = ss.height;
+
+ if (ss.firstId >= 0) {
+ mNeedSync = true;
+ mSyncState = ss;
+ mSyncRowId = ss.firstId;
+ mSyncPosition = ss.position;
+ mSpecificTop = ss.viewTop;
+ }
+ requestLayout();
+ }
+
+ private class PerformClick extends WindowRunnnable implements Runnable {
+ int mClickMotionPosition;
+
+ @Override
+ public void run() {
+ if (mDataChanged) return;
+
+ final ListAdapter adapter = mAdapter;
+ final int motionPosition = mClickMotionPosition;
+ if (adapter != null && mItemCount > 0 &&
+ motionPosition != INVALID_POSITION &&
+ motionPosition < adapter.getCount() && sameWindow()) {
+ final View view = getChildAt(motionPosition); // a fix by @pboos
+
+ if (view != null) {
+ final int clickPosition = motionPosition + mFirstPosition;
+ performItemClick(view, clickPosition, adapter.getItemId(clickPosition));
+ }
+ }
+ }
+ }
+
+ private boolean performLongPress(final View child,
+ final int longPressPosition, final long longPressId) {
+ boolean handled = false;
+
+ OnItemLongClickListener onItemLongClickListener = getOnItemLongClickListener();
+ if (onItemLongClickListener != null) {
+ handled = onItemLongClickListener.onItemLongClick(ExtendableListView.this, child,
+ longPressPosition, longPressId);
+ }
+// if (!handled) {
+// mContextMenuInfo = createContextMenuInfo(child, longPressPosition, longPressId);
+// handled = super.showContextMenuForChild(AbsListView.this);
+// }
+ if (handled) {
+ performHapticFeedback(HapticFeedbackConstants.LONG_PRESS);
+ }
+ return handled;
+ }
+
+ /**
+ * A base class for Runnables that will check that their view is still attached to
+ * the original window as when the Runnable was created.
+ */
+ private class WindowRunnnable {
+ private int mOriginalAttachCount;
+
+ public void rememberWindowAttachCount() {
+ mOriginalAttachCount = getWindowAttachCount();
+ }
+
+ public boolean sameWindow() {
+ return hasWindowFocus() && getWindowAttachCount() == mOriginalAttachCount;
+ }
+ }
+}
diff --git a/src/com/etsy/android/grid/HeaderViewListAdapter.java b/src/com/etsy/android/grid/HeaderViewListAdapter.java
new file mode 100644
index 000000000..ad7f9ddf8
--- /dev/null
+++ b/src/com/etsy/android/grid/HeaderViewListAdapter.java
@@ -0,0 +1,275 @@
+/*
+ * Copyright (C) 2006 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.etsy.android.grid;
+
+import android.database.DataSetObserver;
+import android.view.View;
+import android.view.ViewGroup;
+import android.widget.AdapterView;
+import android.widget.Filter;
+import android.widget.Filterable;
+import android.widget.ListAdapter;
+import android.widget.WrapperListAdapter;
+
+import java.util.ArrayList;
+
+/**
+ * ListAdapter used when a ListView has header views. This ListAdapter
+ * wraps another one and also keeps track of the header views and their
+ * associated data objects.
+ *
This is intended as a base class; you will probably not need to
+ * use this class directly in your own code.
+ */
+public class HeaderViewListAdapter implements WrapperListAdapter, Filterable {
+
+ private final ListAdapter mAdapter;
+
+ // These two ArrayList are assumed to NOT be null.
+ // They are indeed created when declared in ListView and then shared.
+ ArrayList mHeaderViewInfos;
+ ArrayList mFooterViewInfos;
+
+ // Used as a placeholder in case the provided info views are indeed null.
+ // Currently only used by some CTS tests, which may be removed.
+ static final ArrayList EMPTY_INFO_LIST =
+ new ArrayList();
+
+ boolean mAreAllFixedViewsSelectable;
+
+ private final boolean mIsFilterable;
+
+ public HeaderViewListAdapter(ArrayList headerViewInfos,
+ ArrayList footerViewInfos,
+ ListAdapter adapter) {
+ mAdapter = adapter;
+ mIsFilterable = adapter instanceof Filterable;
+
+ if (headerViewInfos == null) {
+ mHeaderViewInfos = EMPTY_INFO_LIST;
+ } else {
+ mHeaderViewInfos = headerViewInfos;
+ }
+
+ if (footerViewInfos == null) {
+ mFooterViewInfos = EMPTY_INFO_LIST;
+ } else {
+ mFooterViewInfos = footerViewInfos;
+ }
+
+ mAreAllFixedViewsSelectable =
+ areAllListInfosSelectable(mHeaderViewInfos)
+ && areAllListInfosSelectable(mFooterViewInfos);
+ }
+
+ public int getHeadersCount() {
+ return mHeaderViewInfos.size();
+ }
+
+ public int getFootersCount() {
+ return mFooterViewInfos.size();
+ }
+
+ public boolean isEmpty() {
+ return mAdapter == null || mAdapter.isEmpty();
+ }
+
+ private boolean areAllListInfosSelectable(ArrayList infos) {
+ if (infos != null) {
+ for (StaggeredGridView.FixedViewInfo info : infos) {
+ if (!info.isSelectable) {
+ return false;
+ }
+ }
+ }
+ return true;
+ }
+
+ public boolean removeHeader(View v) {
+ for (int i = 0; i < mHeaderViewInfos.size(); i++) {
+ StaggeredGridView.FixedViewInfo info = mHeaderViewInfos.get(i);
+ if (info.view == v) {
+ mHeaderViewInfos.remove(i);
+
+ mAreAllFixedViewsSelectable =
+ areAllListInfosSelectable(mHeaderViewInfos)
+ && areAllListInfosSelectable(mFooterViewInfos);
+
+ return true;
+ }
+ }
+
+ return false;
+ }
+
+ public boolean removeFooter(View v) {
+ for (int i = 0; i < mFooterViewInfos.size(); i++) {
+ StaggeredGridView.FixedViewInfo info = mFooterViewInfos.get(i);
+ if (info.view == v) {
+ mFooterViewInfos.remove(i);
+
+ mAreAllFixedViewsSelectable =
+ areAllListInfosSelectable(mHeaderViewInfos)
+ && areAllListInfosSelectable(mFooterViewInfos);
+
+ return true;
+ }
+ }
+
+ return false;
+ }
+
+ public int getCount() {
+ if (mAdapter != null) {
+ return getFootersCount() + getHeadersCount() + mAdapter.getCount();
+ } else {
+ return getFootersCount() + getHeadersCount();
+ }
+ }
+
+ public boolean areAllItemsEnabled() {
+ if (mAdapter != null) {
+ return mAreAllFixedViewsSelectable && mAdapter.areAllItemsEnabled();
+ } else {
+ return true;
+ }
+ }
+
+ public boolean isEnabled(int position) {
+ // Header (negative positions will throw an ArrayIndexOutOfBoundsException)
+ int numHeaders = getHeadersCount();
+ if (position < numHeaders) {
+ return mHeaderViewInfos.get(position).isSelectable;
+ }
+
+ // Adapter
+ final int adjPosition = position - numHeaders;
+ int adapterCount = 0;
+ if (mAdapter != null) {
+ adapterCount = mAdapter.getCount();
+ if (adjPosition < adapterCount) {
+ return mAdapter.isEnabled(adjPosition);
+ }
+ }
+
+ // Footer (off-limits positions will throw an ArrayIndexOutOfBoundsException)
+ return mFooterViewInfos.get(adjPosition - adapterCount).isSelectable;
+ }
+
+ public Object getItem(int position) {
+ // Header (negative positions will throw an ArrayIndexOutOfBoundsException)
+ int numHeaders = getHeadersCount();
+ if (position < numHeaders) {
+ return mHeaderViewInfos.get(position).data;
+ }
+
+ // Adapter
+ final int adjPosition = position - numHeaders;
+ int adapterCount = 0;
+ if (mAdapter != null) {
+ adapterCount = mAdapter.getCount();
+ if (adjPosition < adapterCount) {
+ return mAdapter.getItem(adjPosition);
+ }
+ }
+
+ // Footer (off-limits positions will throw an ArrayIndexOutOfBoundsException)
+ return mFooterViewInfos.get(adjPosition - adapterCount).data;
+ }
+
+ public long getItemId(int position) {
+ int numHeaders = getHeadersCount();
+ if (mAdapter != null && position >= numHeaders) {
+ int adjPosition = position - numHeaders;
+ int adapterCount = mAdapter.getCount();
+ if (adjPosition < adapterCount) {
+ return mAdapter.getItemId(adjPosition);
+ }
+ }
+ return -1;
+ }
+
+ public boolean hasStableIds() {
+ if (mAdapter != null) {
+ return mAdapter.hasStableIds();
+ }
+ return false;
+ }
+
+ public View getView(int position, View convertView, ViewGroup parent) {
+ // Header (negative positions will throw an ArrayIndexOutOfBoundsException)
+ int numHeaders = getHeadersCount();
+ if (position < numHeaders) {
+ return mHeaderViewInfos.get(position).view;
+ }
+
+ // Adapter
+ final int adjPosition = position - numHeaders;
+ int adapterCount = 0;
+ if (mAdapter != null) {
+ adapterCount = mAdapter.getCount();
+ if (adjPosition < adapterCount) {
+ return mAdapter.getView(adjPosition, convertView, parent);
+ }
+ }
+
+ // Footer (off-limits positions will throw an ArrayIndexOutOfBoundsException)
+ return mFooterViewInfos.get(adjPosition - adapterCount).view;
+ }
+
+ public int getItemViewType(int position) {
+ int numHeaders = getHeadersCount();
+ if (mAdapter != null && position >= numHeaders) {
+ int adjPosition = position - numHeaders;
+ int adapterCount = mAdapter.getCount();
+ if (adjPosition < adapterCount) {
+ return mAdapter.getItemViewType(adjPosition);
+ }
+ }
+
+ return AdapterView.ITEM_VIEW_TYPE_HEADER_OR_FOOTER;
+ }
+
+ public int getViewTypeCount() {
+ if (mAdapter != null) {
+ return mAdapter.getViewTypeCount();
+ }
+ return 1;
+ }
+
+ public void registerDataSetObserver(DataSetObserver observer) {
+ if (mAdapter != null) {
+ mAdapter.registerDataSetObserver(observer);
+ }
+ }
+
+ public void unregisterDataSetObserver(DataSetObserver observer) {
+ if (mAdapter != null) {
+ mAdapter.unregisterDataSetObserver(observer);
+ }
+ }
+
+ public Filter getFilter() {
+ if (mIsFilterable) {
+ return ((Filterable) mAdapter).getFilter();
+ }
+ return null;
+ }
+
+ public ListAdapter getWrappedAdapter() {
+ return mAdapter;
+ }
+}
\ No newline at end of file
diff --git a/src/com/etsy/android/grid/StaggeredGridView.java b/src/com/etsy/android/grid/StaggeredGridView.java
new file mode 100644
index 000000000..8dd194732
--- /dev/null
+++ b/src/com/etsy/android/grid/StaggeredGridView.java
@@ -0,0 +1,1340 @@
+/*
+ * Copyright (c) 2013 Etsy
+ *
+ * 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.etsy.android.grid;
+
+import java.util.Arrays;
+
+import android.content.Context;
+import android.content.res.Configuration;
+import android.content.res.TypedArray;
+import android.os.Parcel;
+import android.os.Parcelable;
+import android.util.AttributeSet;
+import android.util.Log;
+import android.util.SparseArray;
+import android.view.View;
+import android.view.ViewGroup;
+
+import com.hippo.ehviewer.R;
+
+/**
+ * A staggered grid view which supports multiple columns with rows of varying sizes.
+ *
+ * Builds multiple columns on top of {@link ExtendableListView}
+ *
+ * Partly inspired by - https://github.com/huewu/PinterestLikeAdapterView
+ */
+public class StaggeredGridView extends ExtendableListView {
+
+ private static final String TAG = "StaggeredGridView";
+ private static final boolean DBG = false;
+
+ private static final int DEFAULT_COLUMNS_PORTRAIT = 2;
+ private static final int DEFAULT_COLUMNS_LANDSCAPE = 3;
+
+ private int mColumnCount;
+ private int mItemMargin;
+ private int mColumnWidth;
+ private boolean mNeedSync;
+
+ private int mColumnCountPortrait = DEFAULT_COLUMNS_PORTRAIT;
+ private int mColumnCountLandscape = DEFAULT_COLUMNS_LANDSCAPE;
+
+ private OnChangeColumnListener mOnChangeColumnListener;
+ private int mLastColumnCount = -1;
+ private int mLastWidth = -1;
+
+ /**
+ * A key-value collection where the key is the position and the
+ * {@link GridItemRecord} with some info about that position
+ * so we can maintain it's position - and reorg on orientation change.
+ */
+ private SparseArray mPositionData;
+ private int mGridPaddingLeft;
+ private int mGridPaddingRight;
+ private int mGridPaddingTop;
+ private int mGridPaddingBottom;
+
+ /***
+ * Our grid item state record with {@link Parcelable} implementation
+ * so we can persist them across the SGV lifecycle.
+ */
+ static class GridItemRecord implements Parcelable {
+ int column;
+ double heightRatio;
+ boolean isHeaderFooter;
+
+ GridItemRecord() { }
+
+ /**
+ * Constructor called from {@link #CREATOR}
+ */
+ private GridItemRecord(Parcel in) {
+ column = in.readInt();
+ heightRatio = in.readDouble();
+ isHeaderFooter = in.readByte() == 1;
+ }
+
+ @Override
+ public int describeContents() {
+ return 0;
+ }
+
+ @Override
+ public void writeToParcel(Parcel out, int flags) {
+ out.writeInt(column);
+ out.writeDouble(heightRatio);
+ out.writeByte((byte) (isHeaderFooter ? 1 : 0));
+ }
+
+ @Override
+ public String toString() {
+ return "GridItemRecord.ListSavedState{"
+ + Integer.toHexString(System.identityHashCode(this))
+ + " column:" + column
+ + " heightRatio:" + heightRatio
+ + " isHeaderFooter:" + isHeaderFooter
+ + "}";
+ }
+
+ public static final Parcelable.Creator CREATOR
+ = new Parcelable.Creator() {
+ @Override
+ public GridItemRecord createFromParcel(Parcel in) {
+ return new GridItemRecord(in);
+ }
+
+ @Override
+ public GridItemRecord[] newArray(int size) {
+ return new GridItemRecord[size];
+ }
+ };
+ }
+
+ /**
+ * The location of the top of each top item added in each column.
+ */
+ private int[] mColumnTops;
+
+ /**
+ * The location of the bottom of each bottom item added in each column.
+ */
+ private int[] mColumnBottoms;
+
+ /**
+ * The left location to put items for each column
+ */
+ private int[] mColumnLefts;
+
+ /***
+ * Tells us the distance we've offset from the top.
+ * Can be slightly off on orientation change - TESTING
+ */
+ private int mDistanceToTop;
+
+ public StaggeredGridView(final Context context) {
+ this(context, null);
+ }
+
+ public StaggeredGridView(final Context context, final AttributeSet attrs) {
+ this(context, attrs, 0);
+ }
+
+ public StaggeredGridView(final Context context, final AttributeSet attrs, final int defStyle) {
+ super(context, attrs, defStyle);
+
+ if (attrs != null) {
+ // get the number of columns in portrait and landscape
+ TypedArray typedArray = context.obtainStyledAttributes(attrs, R.styleable.StaggeredGridView, defStyle, 0);
+
+ mColumnCount = typedArray.getInteger(
+ R.styleable.StaggeredGridView_column_count, 0);
+
+ if (mColumnCount > 0) {
+ mColumnCountPortrait = mColumnCount;
+ mColumnCountLandscape = mColumnCount;
+ }
+ else {
+ mColumnCountPortrait = typedArray.getInteger(
+ R.styleable.StaggeredGridView_column_count_portrait,
+ DEFAULT_COLUMNS_PORTRAIT);
+ mColumnCountLandscape = typedArray.getInteger(
+ R.styleable.StaggeredGridView_column_count_landscape,
+ DEFAULT_COLUMNS_LANDSCAPE);
+ }
+
+ mItemMargin = typedArray.getDimensionPixelSize(
+ R.styleable.StaggeredGridView_item_margin, 0);
+ mGridPaddingLeft = typedArray.getDimensionPixelSize(
+ R.styleable.StaggeredGridView_grid_paddingLeft, 0);
+ mGridPaddingRight = typedArray.getDimensionPixelSize(
+ R.styleable.StaggeredGridView_grid_paddingRight, 0);
+ mGridPaddingTop = typedArray.getDimensionPixelSize(
+ R.styleable.StaggeredGridView_grid_paddingTop, 0);
+ mGridPaddingBottom = typedArray.getDimensionPixelSize(
+ R.styleable.StaggeredGridView_grid_paddingBottom, 0);
+
+ typedArray.recycle();
+ }
+
+ mColumnCount = 0; // determined onMeasure
+ // Creating these empty arrays to avoid saving null states
+ mColumnTops = new int[0];
+ mColumnBottoms = new int[0];
+ mColumnLefts = new int[0];
+ mPositionData = new SparseArray();
+ }
+
+ // //////////////////////////////////////////////////////////////////////////////////////////
+ // PROPERTIES
+ //
+
+ // Grid padding is applied to the list item rows but not the header and footer
+ public int getRowPaddingLeft() {
+ return getListPaddingLeft() + mGridPaddingLeft;
+ }
+
+ public int getRowPaddingRight() {
+ return getListPaddingRight() + mGridPaddingRight;
+ }
+
+ public int getRowPaddingTop() {
+ return getListPaddingTop() + mGridPaddingTop;
+ }
+
+ public int getRowPaddingBottom() {
+ return getListPaddingBottom() + mGridPaddingBottom;
+ }
+
+ public void setGridPadding(int left, int top, int right, int bottom) {
+ mGridPaddingLeft = left;
+ mGridPaddingTop = top;
+ mGridPaddingRight = right;
+ mGridPaddingBottom = bottom;
+ }
+
+ public void setColumnCountPortrait(int columnCountPortrait) {
+ mColumnCountPortrait = columnCountPortrait;
+ onSizeChanged(getWidth(), getHeight());
+ requestLayoutChildren();
+ }
+
+ public void setColumnCountLandscape(int columnCountLandscape) {
+ mColumnCountLandscape = columnCountLandscape;
+ onSizeChanged(getWidth(), getHeight());
+ requestLayoutChildren();
+ }
+
+ public void setColumnCount(int columnCount) {
+ mColumnCountPortrait = columnCount;
+ mColumnCountLandscape = columnCount;
+ // mColumnCount set onSizeChanged();
+ onSizeChanged(getWidth(), getHeight());
+ requestLayoutChildren();
+ }
+
+ public void setColumnCountPortraitBefore(int columnCountPortrait) {
+ mColumnCountPortrait = columnCountPortrait;
+ }
+
+ public void setColumnCountLandscapeBefore(int columnCountLandscape) {
+ mColumnCountLandscape = columnCountLandscape;
+ }
+
+ public void setOnChangeColumnListener(OnChangeColumnListener l) {
+ mOnChangeColumnListener = l;
+ }
+
+ // //////////////////////////////////////////////////////////////////////////////////////////
+ // MEASUREMENT
+ //
+ private boolean isLandscape() {
+ return getResources().getConfiguration().orientation == Configuration.ORIENTATION_LANDSCAPE;
+ }
+
+ @Override
+ protected void onMeasure(final int widthMeasureSpec, final int heightMeasureSpec) {
+ super.onMeasure(widthMeasureSpec, heightMeasureSpec);
+
+ if (mColumnCount <= 0) {
+ boolean isLandscape = isLandscape();
+ mColumnCount = isLandscape ? mColumnCountLandscape : mColumnCountPortrait;
+ }
+
+ // on change column
+ int width = getWidth();
+ if ((mLastColumnCount != mColumnCount || mLastWidth != width) &&
+ mOnChangeColumnListener != null)
+ mOnChangeColumnListener.onChangeColumn(mColumnCount, width);
+
+ mLastColumnCount = mColumnCount;
+ mLastWidth = width;
+
+ // our column width is the width of the listview
+ // minus it's padding
+ // minus the total items margin
+ // divided by the number of columns
+ mColumnWidth = calculateColumnWidth(getMeasuredWidth());
+
+ if (mColumnTops == null || mColumnTops.length != mColumnCount) {
+ mColumnTops = new int[mColumnCount];
+ initColumnTops();
+ }
+ if (mColumnBottoms == null || mColumnBottoms.length != mColumnCount) {
+ mColumnBottoms = new int[mColumnCount];
+ initColumnBottoms();
+ }
+ if (mColumnLefts == null || mColumnLefts.length != mColumnCount) {
+ mColumnLefts = new int[mColumnCount];
+ initColumnLefts();
+ }
+ }
+
+ @Override
+ protected void onMeasureChild(final View child, final LayoutParams layoutParams) {
+ final int viewType = layoutParams.viewType;
+ final int position = layoutParams.position;
+
+ if (viewType == ITEM_VIEW_TYPE_HEADER_OR_FOOTER ||
+ viewType == ITEM_VIEW_TYPE_IGNORE) {
+ // for headers and weird ignored views
+ super.onMeasureChild(child, layoutParams);
+ }
+ else {
+ if (DBG) Log.d(TAG, "onMeasureChild BEFORE position:" + position +
+ " h:" + getMeasuredHeight());
+ // measure it to the width of our column.
+ int childWidthSpec = MeasureSpec.makeMeasureSpec(mColumnWidth, MeasureSpec.EXACTLY);
+ int childHeightSpec;
+ if (layoutParams.height > 0) {
+ childHeightSpec = MeasureSpec.makeMeasureSpec(layoutParams.height, MeasureSpec.EXACTLY);
+ }
+ else {
+ childHeightSpec = MeasureSpec.makeMeasureSpec(LayoutParams.WRAP_CONTENT, MeasureSpec.UNSPECIFIED);
+ }
+ child.measure(childWidthSpec, childHeightSpec);
+ }
+
+ final int childHeight = getChildHeight(child);
+ setPositionHeightRatio(position, childHeight);
+
+ if (DBG) Log.d(TAG, "onMeasureChild AFTER position:" + position +
+ " h:" + childHeight);
+ }
+
+ public int getColumnWidth() {
+ return mColumnWidth;
+ }
+
+ @Override
+ public void resetToTop() {
+ if (mColumnCount > 0) {
+
+ if (mColumnTops == null) {
+ mColumnTops = new int[mColumnCount];
+ }
+ if (mColumnBottoms == null) {
+ mColumnBottoms = new int[mColumnCount];
+ }
+ initColumnTopsAndBottoms();
+
+ mPositionData.clear();
+ mNeedSync = false;
+ mDistanceToTop = 0;
+ setSelection(0);
+ }
+ }
+
+ // //////////////////////////////////////////////////////////////////////////////////////////
+ // POSITIONING
+ //
+
+ @Override
+ protected void onChildCreated(final int position, final boolean flowDown) {
+ super.onChildCreated(position, flowDown);
+ if (!isHeaderOrFooter(position)) {
+ // do we already have a column for this position?
+ final int column = getChildColumn(position, flowDown);
+ setPositionColumn(position, column);
+ if (DBG) Log.d(TAG, "onChildCreated position:" + position +
+ " is in column:" + column);
+ }
+ else {
+ setPositionIsHeaderFooter(position);
+ }
+ }
+
+ private void requestLayoutChildren() {
+ final int count = getChildCount();
+ for (int i = 0; i < count; i++) {
+ final View v = getChildAt(i);
+ if (v != null) v.requestLayout();
+ }
+ }
+
+ @Override
+ protected void layoutChildren() {
+ preLayoutChildren();
+ super.layoutChildren();
+ }
+
+ private void preLayoutChildren() {
+ // on a major re-layout reset for our next layout pass
+ if (!mNeedSync) {
+ Arrays.fill(mColumnBottoms, 0);
+ }
+ else {
+ mNeedSync = false;
+ }
+ // copy the tops into the bottom
+ // since we're going to redo a layout pass that will draw down from
+ // the top
+ System.arraycopy(mColumnTops, 0, mColumnBottoms, 0, mColumnCount);
+ }
+
+ // NOTE : Views will either be layout out via onLayoutChild
+ // OR
+ // Views will be offset if they are active but offscreen so that we can recycle!
+ // Both onLayoutChild() and onOffsetChild are called after we measure our view
+ // see ExtensibleListView.setupChild();
+
+ @Override
+ protected void onLayoutChild(final View child,
+ final int position,
+ final boolean flowDown,
+ final int childrenLeft, final int childTop,
+ final int childRight, final int childBottom) {
+ if (isHeaderOrFooter(position)) {
+ layoutGridHeaderFooter(child, position, flowDown, childrenLeft, childTop, childRight, childBottom);
+ }
+ else {
+ layoutGridChild(child, position, flowDown, childrenLeft, childRight);
+ }
+ }
+
+ private void layoutGridHeaderFooter(final View child, final int position, final boolean flowDown, final int childrenLeft, final int childTop, final int childRight, final int childBottom) {
+ // offset the top and bottom of all our columns
+ // if it's the footer we want it below the lowest child bottom
+ int gridChildTop;
+ int gridChildBottom;
+
+ if (flowDown) {
+ gridChildTop = getLowestPositionedBottom();
+ gridChildBottom = gridChildTop + getChildHeight(child);
+ }
+ else {
+ gridChildBottom = getHighestPositionedTop();
+ gridChildTop = gridChildBottom - getChildHeight(child);
+ }
+
+ for (int i = 0; i < mColumnCount; i++) {
+ updateColumnTopIfNeeded(i, gridChildTop);
+ updateColumnBottomIfNeeded(i, gridChildBottom);
+ }
+
+ super.onLayoutChild(child, position, flowDown,
+ childrenLeft, gridChildTop, childRight, gridChildBottom);
+ }
+
+ private void layoutGridChild(final View child, final int position,
+ final boolean flowDown,
+ final int childrenLeft, final int childRight) {
+ // stash the bottom and the top if it's higher positioned
+ int column = getPositionColumn(position);
+
+ int gridChildTop;
+ int gridChildBottom;
+
+ int childTopMargin = getChildTopMargin(position);
+ int childBottomMargin = getChildBottomMargin();
+ int verticalMargins = childTopMargin + childBottomMargin;
+
+ if (flowDown) {
+ gridChildTop = mColumnBottoms[column]; // the next items top is the last items bottom
+ gridChildBottom = gridChildTop + (getChildHeight(child) + verticalMargins);
+ }
+ else {
+ gridChildBottom = mColumnTops[column]; // the bottom of the next column up is our top
+ gridChildTop = gridChildBottom - (getChildHeight(child) + verticalMargins);
+ }
+
+ if (DBG) Log.d(TAG, "onLayoutChild position:" + position +
+ " column:" + column +
+ " gridChildTop:" + gridChildTop +
+ " gridChildBottom:" + gridChildBottom);
+
+ // we also know the column of this view so let's stash it in the
+ // view's layout params
+ GridLayoutParams layoutParams = (GridLayoutParams) child.getLayoutParams();
+ layoutParams.column = column;
+
+ updateColumnBottomIfNeeded(column, gridChildBottom);
+ updateColumnTopIfNeeded(column, gridChildTop);
+
+ // subtract the margins before layout
+ gridChildTop += childTopMargin;
+ gridChildBottom -= childBottomMargin;
+
+ child.layout(childrenLeft, gridChildTop, childRight, gridChildBottom);
+ }
+
+ @Override
+ protected void onOffsetChild(final View child, final int position,
+ final boolean flowDown, final int childrenLeft, final int childTop) {
+ // if the child is recycled and is just offset
+ // we still want to add its deets into our store
+ if (isHeaderOrFooter(position)) {
+
+ offsetGridHeaderFooter(child, position, flowDown, childrenLeft, childTop);
+ }
+ else {
+ offsetGridChild(child, position, flowDown, childrenLeft, childTop);
+ }
+ }
+
+ private void offsetGridHeaderFooter(final View child, final int position, final boolean flowDown, final int childrenLeft, final int childTop) {
+ // offset the top and bottom of all our columns
+ // if it's the footer we want it below the lowest child bottom
+ int gridChildTop;
+ int gridChildBottom;
+
+ if (flowDown) {
+ gridChildTop = getLowestPositionedBottom();
+ gridChildBottom = gridChildTop + getChildHeight(child);
+ }
+ else {
+ gridChildBottom = getHighestPositionedTop();
+ gridChildTop = gridChildBottom - getChildHeight(child);
+ }
+
+ for (int i = 0; i < mColumnCount; i++) {
+ updateColumnTopIfNeeded(i, gridChildTop);
+ updateColumnBottomIfNeeded(i, gridChildBottom);
+ }
+
+ super.onOffsetChild(child, position, flowDown, childrenLeft, gridChildTop);
+ }
+
+ private void offsetGridChild(final View child, final int position, final boolean flowDown, final int childrenLeft, final int childTop) {
+ // stash the bottom and the top if it's higher positioned
+ int column = getPositionColumn(position);
+
+ int gridChildTop;
+ int gridChildBottom;
+
+ int childTopMargin = getChildTopMargin(position);
+ int childBottomMargin = getChildBottomMargin();
+ int verticalMargins = childTopMargin + childBottomMargin;
+
+ if (flowDown) {
+ gridChildTop = mColumnBottoms[column]; // the next items top is the last items bottom
+ gridChildBottom = gridChildTop + (getChildHeight(child) + verticalMargins);
+ }
+ else {
+ gridChildBottom = mColumnTops[column]; // the bottom of the next column up is our top
+ gridChildTop = gridChildBottom - (getChildHeight(child) + verticalMargins);
+ }
+
+ if (DBG) Log.d(TAG, "onOffsetChild position:" + position +
+ " column:" + column +
+ " childTop:" + childTop +
+ " gridChildTop:" + gridChildTop +
+ " gridChildBottom:" + gridChildBottom);
+
+ // we also know the column of this view so let's stash it in the
+ // view's layout params
+ GridLayoutParams layoutParams = (GridLayoutParams) child.getLayoutParams();
+ layoutParams.column = column;
+
+ updateColumnBottomIfNeeded(column, gridChildBottom);
+ updateColumnTopIfNeeded(column, gridChildTop);
+
+ super.onOffsetChild(child, position, flowDown, childrenLeft, gridChildTop + childTopMargin);
+ }
+
+ private int getChildHeight(final View child) {
+ return child.getMeasuredHeight();
+ }
+
+ private int getChildTopMargin(final int position) {
+ boolean isFirstRow = position < (getHeaderViewsCount() + mColumnCount);
+ return isFirstRow ? mItemMargin : 0;
+ }
+
+ private int getChildBottomMargin() {
+ return mItemMargin;
+ }
+
+ @Override
+ protected LayoutParams generateChildLayoutParams(final View child) {
+ GridLayoutParams layoutParams = null;
+
+ final ViewGroup.LayoutParams childParams = child.getLayoutParams();
+ if (childParams != null) {
+ if (childParams instanceof GridLayoutParams) {
+ layoutParams = (GridLayoutParams) childParams;
+ }
+ else {
+ layoutParams = new GridLayoutParams(childParams);
+ }
+ }
+ if (layoutParams == null) {
+ layoutParams = new GridLayoutParams(
+ mColumnWidth, ViewGroup.LayoutParams.WRAP_CONTENT);
+ }
+
+ return layoutParams;
+ }
+
+ private void updateColumnTopIfNeeded(int column, int childTop) {
+ if (childTop < mColumnTops[column]) {
+ mColumnTops[column] = childTop;
+ }
+ }
+
+ private void updateColumnBottomIfNeeded(int column, int childBottom) {
+ if (childBottom > mColumnBottoms[column]) {
+ mColumnBottoms[column] = childBottom;
+ }
+ }
+
+ @Override
+ protected int getChildLeft(final int position) {
+ if (isHeaderOrFooter(position)) {
+ return super.getChildLeft(position);
+ }
+ else {
+ final int column = getPositionColumn(position);
+ return mColumnLefts[column];
+ }
+ }
+
+ @Override
+ protected int getChildTop(final int position) {
+ if (isHeaderOrFooter(position)) {
+ return super.getChildTop(position);
+ }
+ else {
+ final int column = getPositionColumn(position);
+ if (column == -1) {
+ return getHighestPositionedBottom();
+ }
+ return mColumnBottoms[column];
+ }
+ }
+
+ /**
+ * Get the top for the next child down in our view
+ * (maybe a column across) so we can fill down.
+ */
+ @Override
+ protected int getNextChildDownsTop(final int position) {
+ if (isHeaderOrFooter(position)) {
+ return super.getNextChildDownsTop(position);
+ }
+ else {
+ return getHighestPositionedBottom();
+ }
+ }
+
+ @Override
+ protected int getChildBottom(final int position) {
+ if (isHeaderOrFooter(position)) {
+ return super.getChildBottom(position);
+ }
+ else {
+ final int column = getPositionColumn(position);
+ if (column == -1) {
+ return getLowestPositionedTop();
+ }
+ return mColumnTops[column];
+ }
+ }
+
+ /**
+ * Get the bottom for the next child up in our view
+ * (maybe a column across) so we can fill up.
+ */
+ @Override
+ protected int getNextChildUpsBottom(final int position) {
+ if (isHeaderOrFooter(position)) {
+ return super.getNextChildUpsBottom(position);
+ }
+ else {
+ return getLowestPositionedTop();
+ }
+ }
+
+ @Override
+ protected int getLastChildBottom() {
+ final int lastPosition = mFirstPosition + (getChildCount() - 1);
+ if (isHeaderOrFooter(lastPosition)) {
+ return super.getLastChildBottom();
+ }
+ return getHighestPositionedBottom();
+ }
+
+ @Override
+ protected int getFirstChildTop() {
+ if (isHeaderOrFooter(mFirstPosition)) {
+ return super.getFirstChildTop();
+ }
+ return getLowestPositionedTop();
+ }
+
+ @Override
+ protected int getHighestChildTop() {
+ if (isHeaderOrFooter(mFirstPosition)) {
+ return super.getHighestChildTop();
+ }
+ return getHighestPositionedTop();
+ }
+
+ @Override
+ protected int getLowestChildBottom() {
+ final int lastPosition = mFirstPosition + (getChildCount() - 1);
+ if (isHeaderOrFooter(lastPosition)) {
+ return super.getLowestChildBottom();
+ }
+ return getLowestPositionedBottom();
+ }
+
+ @Override
+ protected void offsetChildrenTopAndBottom(final int offset) {
+ super.offsetChildrenTopAndBottom(offset);
+ offsetAllColumnsTopAndBottom(offset);
+ offsetDistanceToTop(offset);
+ }
+
+ protected void offsetChildrenTopAndBottom(final int offset, final int column) {
+ if (DBG) Log.d(TAG, "offsetChildrenTopAndBottom: " + offset + " column:" + column);
+ final int count = getChildCount();
+ for (int i = 0; i < count; i++) {
+ final View v = getChildAt(i);
+ if (v != null &&
+ v.getLayoutParams() != null &&
+ v.getLayoutParams() instanceof GridLayoutParams) {
+ GridLayoutParams lp = (GridLayoutParams) v.getLayoutParams();
+ if (lp.column == column) {
+ v.offsetTopAndBottom(offset);
+ }
+ }
+ }
+ offsetColumnTopAndBottom(offset, column);
+ }
+
+ private void offsetDistanceToTop(final int offset) {
+ mDistanceToTop += offset;
+ if (DBG) Log.d(TAG, "offset mDistanceToTop:" + mDistanceToTop);
+ }
+
+ public int getDistanceToTop() {
+ return mDistanceToTop;
+ }
+
+ private void offsetAllColumnsTopAndBottom(final int offset) {
+ if (offset != 0) {
+ for (int i = 0; i < mColumnCount; i++) {
+ offsetColumnTopAndBottom(offset, i);
+ }
+ }
+ }
+
+ private void offsetColumnTopAndBottom(final int offset, final int column) {
+ if (offset != 0) {
+ mColumnTops[column] += offset;
+ mColumnBottoms[column] += offset;
+ }
+ }
+
+ @Override
+ protected void adjustViewsAfterFillGap(final boolean down) {
+ super.adjustViewsAfterFillGap(down);
+ // fix vertical gaps when hitting the top after a rotate
+ // only when scrolling back up!
+ if (!down) {
+ alignTops();
+ }
+ }
+
+ private void alignTops() {
+ if (mFirstPosition == getHeaderViewsCount()) {
+ // we're showing all the views before the header views
+ int[] nonHeaderTops = getHighestNonHeaderTops();
+ // we should now have our non header tops
+ // align them
+ boolean isAligned = true;
+ int highestColumn = -1;
+ int highestTop = Integer.MAX_VALUE;
+ for (int i = 0; i < nonHeaderTops.length; i++) {
+ // are they all aligned
+ if (isAligned && i > 0 && nonHeaderTops[i] != highestTop) {
+ isAligned = false; // not all the tops are aligned
+ }
+ // what's the highest
+ if (nonHeaderTops[i] < highestTop) {
+ highestTop = nonHeaderTops[i];
+ highestColumn = i;
+ }
+ }
+
+ // skip the rest.
+ if (isAligned) return;
+
+ // we've got the highest column - lets align the others
+ for (int i = 0; i < nonHeaderTops.length; i++) {
+ if (i != highestColumn) {
+ // there's a gap in this column
+ int offset = highestTop - nonHeaderTops[i];
+ offsetChildrenTopAndBottom(offset, i);
+ }
+ }
+ invalidate();
+ }
+ }
+
+ private int[] getHighestNonHeaderTops() {
+ int[] nonHeaderTops = new int[mColumnCount];
+ int childCount = getChildCount();
+ if (childCount > 0) {
+ for (int i = 0; i < childCount; i++) {
+ View child = getChildAt(i);
+ if (child != null &&
+ child.getLayoutParams() != null &&
+ child.getLayoutParams() instanceof GridLayoutParams) {
+ // is this child's top the highest non
+ GridLayoutParams lp = (GridLayoutParams) child.getLayoutParams();
+ // is it a child that isn't a header
+ if (lp.viewType != ITEM_VIEW_TYPE_HEADER_OR_FOOTER &&
+ child.getTop() < nonHeaderTops[lp.column]) {
+ nonHeaderTops[lp.column] = child.getTop();
+ }
+ }
+ }
+ }
+ return nonHeaderTops;
+ }
+
+ @Override
+ protected void onChildrenDetached(final int start, final int count) {
+ super.onChildrenDetached(start, count);
+ // go through our remaining views and sync the top and bottom stash.
+
+ // Repair the top and bottom column boundaries from the views we still have
+ Arrays.fill(mColumnTops, Integer.MAX_VALUE);
+ Arrays.fill(mColumnBottoms, 0);
+
+ for (int i = 0; i < getChildCount(); i++) {
+ final View child = getChildAt(i);
+ if (child != null) {
+ final LayoutParams childParams = (LayoutParams) child.getLayoutParams();
+ if (childParams.viewType != ITEM_VIEW_TYPE_HEADER_OR_FOOTER &&
+ childParams instanceof GridLayoutParams) {
+ GridLayoutParams layoutParams = (GridLayoutParams) childParams;
+ int column = layoutParams.column;
+ int position = layoutParams.position;
+ final int childTop = child.getTop();
+ if (childTop < mColumnTops[column]) {
+ mColumnTops[column] = childTop - getChildTopMargin(position);
+ }
+ final int childBottom = child.getBottom();
+ if (childBottom > mColumnBottoms[column]) {
+ mColumnBottoms[column] = childBottom + getChildBottomMargin();
+ }
+ }
+ else {
+ // the header and footer here
+ final int childTop = child.getTop();
+ final int childBottom = child.getBottom();
+
+ for (int col = 0; col < mColumnCount; col++) {
+ if (childTop < mColumnTops[col]) {
+ mColumnTops[col] = childTop;
+ }
+ if (childBottom > mColumnBottoms[col]) {
+ mColumnBottoms[col] = childBottom;
+ }
+ }
+
+ }
+ }
+ }
+ }
+
+ @Override
+ protected boolean hasSpaceUp() {
+ int end = mClipToPadding ? getRowPaddingTop() : 0;
+ return getLowestPositionedTop() > end;
+ }
+
+ // //////////////////////////////////////////////////////////////////////////////////////////
+ // SYNCING ACROSS ROTATION
+ //
+
+ @Override
+ protected void onSizeChanged(final int w, final int h, final int oldw, final int oldh) {
+ super.onSizeChanged(w, h, oldw, oldh);
+ onSizeChanged(w, h);
+ }
+
+ @Override
+ protected void onSizeChanged(int w, int h) {
+ super.onSizeChanged(w, h);
+ boolean isLandscape = isLandscape();
+ int newColumnCount = isLandscape ? mColumnCountLandscape : mColumnCountPortrait;
+ if (mColumnCount != newColumnCount) {
+ mColumnCount = newColumnCount;
+
+ mColumnWidth = calculateColumnWidth(w);
+
+ mColumnTops = new int[mColumnCount];
+ mColumnBottoms = new int[mColumnCount];
+ mColumnLefts = new int[mColumnCount];
+
+ mDistanceToTop = 0;
+
+ // rebuild the columns
+ initColumnTopsAndBottoms();
+ initColumnLefts();
+
+ // if we have data
+ if (getCount() > 0 && mPositionData.size() > 0) {
+ onColumnSync();
+ }
+
+ requestLayout();
+ }
+ }
+
+ private int calculateColumnWidth(final int gridWidth) {
+ final int listPadding = getRowPaddingLeft() + getRowPaddingRight();
+ return (gridWidth - listPadding - mItemMargin * (mColumnCount + 1)) / mColumnCount;
+ }
+
+ private int calculateColumnLeft(final int colIndex) {
+ return getRowPaddingLeft() + mItemMargin + ((mItemMargin + mColumnWidth) * colIndex);
+ }
+
+ /***
+ * Our mColumnTops and mColumnBottoms need to be re-built up to the
+ * mSyncPosition - the following layout request will then
+ * layout the that position and then fillUp and fillDown appropriately.
+ */
+ private void onColumnSync() {
+ // re-calc tops for new column count!
+ int syncPosition = Math.min(mSyncPosition, getCount() - 1);
+
+ SparseArray positionHeightRatios = new SparseArray(syncPosition);
+ for (int pos = 0; pos < syncPosition; pos++) {
+ // check for weirdness
+ final GridItemRecord rec = mPositionData.get(pos);
+ if (rec == null) break;
+
+ Log.d(TAG, "onColumnSync:" + pos + " ratio:" + rec.heightRatio);
+ positionHeightRatios.append(pos, rec.heightRatio);
+ }
+
+ mPositionData.clear();
+
+ // re-calc our relative position while at the same time
+ // rebuilding our GridItemRecord collection
+
+ if (DBG) Log.d(TAG, "onColumnSync column width:" + mColumnWidth);
+
+ for (int pos = 0; pos < syncPosition; pos++) {
+ //Check for weirdness again
+ final Double heightRatio = positionHeightRatios.get(pos);
+ if(heightRatio == null){
+ break;
+ }
+
+ final GridItemRecord rec = getOrCreateRecord(pos);
+ final int height = (int) (mColumnWidth * heightRatio);
+ rec.heightRatio = heightRatio;
+
+ int top;
+ int bottom;
+ // check for headers
+ if (isHeaderOrFooter(pos)) {
+ // the next top is the bottom for that column
+ top = getLowestPositionedBottom();
+ bottom = top + height;
+
+ for (int i = 0; i < mColumnCount; i++) {
+ mColumnTops[i] = top;
+ mColumnBottoms[i] = bottom;
+ }
+ }
+ else {
+ // what's the next column down ?
+ final int column = getHighestPositionedBottomColumn();
+ // the next top is the bottom for that column
+ top = mColumnBottoms[column];
+ bottom = top + height + getChildTopMargin(pos) + getChildBottomMargin();
+
+ mColumnTops[column] = top;
+ mColumnBottoms[column] = bottom;
+
+ rec.column = column;
+ }
+
+
+ if (DBG) Log.d(TAG, "onColumnSync position:" + pos +
+ " top:" + top +
+ " bottom:" + bottom +
+ " height:" + height +
+ " heightRatio:" + heightRatio);
+ }
+
+ // our sync position will be displayed in this column
+ final int syncColumn = getHighestPositionedBottomColumn();
+ setPositionColumn(syncPosition, syncColumn);
+
+ // we want to offset from height of the sync position
+ // minus the offset
+ int syncToBottom = mColumnBottoms[syncColumn];
+ int offset = -syncToBottom + mSpecificTop;
+ // offset all columns by
+ offsetAllColumnsTopAndBottom(offset);
+
+ // sync the distance to top
+ mDistanceToTop = -syncToBottom;
+
+ // stash our bottoms in our tops - though these will be copied back to the bottoms
+ System.arraycopy(mColumnBottoms, 0, mColumnTops, 0, mColumnCount);
+ }
+
+
+ // //////////////////////////////////////////////////////////////////////////////////////////
+ // GridItemRecord UTILS
+ //
+
+ private void setPositionColumn(final int position, final int column) {
+ GridItemRecord rec = getOrCreateRecord(position);
+ rec.column = column;
+ }
+
+ private void setPositionHeightRatio(final int position, final int height) {
+ GridItemRecord rec = getOrCreateRecord(position);
+ rec.heightRatio = (double) height / (double) mColumnWidth;
+ if (DBG) Log.d(TAG, "position:" + position +
+ " width:" + mColumnWidth +
+ " height:" + height +
+ " heightRatio:" + rec.heightRatio);
+ }
+
+ private void setPositionIsHeaderFooter(final int position) {
+ GridItemRecord rec = getOrCreateRecord(position);
+ rec.isHeaderFooter = true;
+ }
+
+ private GridItemRecord getOrCreateRecord(final int position) {
+ GridItemRecord rec = mPositionData.get(position, null);
+ if (rec == null) {
+ rec = new GridItemRecord();
+ mPositionData.append(position, rec);
+ }
+ return rec;
+ }
+
+ private int getPositionColumn(final int position) {
+ GridItemRecord rec = mPositionData.get(position, null);
+ return rec != null ? rec.column : -1;
+ }
+
+
+ // //////////////////////////////////////////////////////////////////////////////////////////
+ // HELPERS
+ //
+
+ private boolean isHeaderOrFooter(final int position) {
+ final int viewType = mAdapter.getItemViewType(position);
+ return viewType == ITEM_VIEW_TYPE_HEADER_OR_FOOTER;
+ }
+
+ private int getChildColumn(final int position, final boolean flowDown) {
+
+ // do we already have a column for this child position?
+ int column = getPositionColumn(position);
+ // we don't have the column or it no longer fits in our grid
+ final int columnCount = mColumnCount;
+ if (column < 0 || column >= columnCount) {
+ // if we're going down -
+ // get the highest positioned (lowest value)
+ // column bottom
+ if (flowDown) {
+ column = getHighestPositionedBottomColumn();
+ }
+ else {
+ column = getLowestPositionedTopColumn();
+
+ }
+ }
+ return column;
+ }
+
+ private void initColumnTopsAndBottoms() {
+ initColumnTops();
+ initColumnBottoms();
+ }
+
+ private void initColumnTops() {
+ Arrays.fill(mColumnTops, getPaddingTop() + mGridPaddingTop);
+ }
+
+ private void initColumnBottoms() {
+ Arrays.fill(mColumnBottoms, getPaddingTop() + mGridPaddingTop);
+ }
+
+ private void initColumnLefts() {
+ for (int i = 0; i < mColumnCount; i++) {
+ mColumnLefts[i] = calculateColumnLeft(i);
+ }
+ }
+
+
+ // //////////////////////////////////////////////////////////////////////////////////////////
+ // BOTTOM
+ //
+
+ private int getHighestPositionedBottom() {
+ final int column = getHighestPositionedBottomColumn();
+ return mColumnBottoms[column];
+ }
+
+ private int getHighestPositionedBottomColumn() {
+ int columnFound = 0;
+ int highestPositionedBottom = Integer.MAX_VALUE;
+ // the highest positioned bottom is the one with the lowest value :D
+ for (int i = 0; i < mColumnCount; i++) {
+ int bottom = mColumnBottoms[i];
+ if (bottom < highestPositionedBottom) {
+ highestPositionedBottom = bottom;
+ columnFound = i;
+ }
+ }
+ return columnFound;
+ }
+
+ private int getLowestPositionedBottom() {
+ final int column = getLowestPositionedBottomColumn();
+ return mColumnBottoms[column];
+ }
+
+ private int getLowestPositionedBottomColumn() {
+ int columnFound = 0;
+ int lowestPositionedBottom = Integer.MIN_VALUE;
+ // the lowest positioned bottom is the one with the highest value :D
+ for (int i = 0; i < mColumnCount; i++) {
+ int bottom = mColumnBottoms[i];
+ if (bottom > lowestPositionedBottom) {
+ lowestPositionedBottom = bottom;
+ columnFound = i;
+ }
+ }
+ return columnFound;
+ }
+
+ // //////////////////////////////////////////////////////////////////////////////////////////
+ // TOP
+ //
+
+ private int getLowestPositionedTop() {
+ final int column = getLowestPositionedTopColumn();
+ return mColumnTops[column];
+ }
+
+ private int getLowestPositionedTopColumn() {
+ int columnFound = 0;
+ // we'll go backwards through since the right most
+ // will likely be the lowest positioned Top
+ int lowestPositionedTop = Integer.MIN_VALUE;
+ // the lowest positioned top is the one with the highest value :D
+ for (int i = 0; i < mColumnCount; i++) {
+ int top = mColumnTops[i];
+ if (top > lowestPositionedTop) {
+ lowestPositionedTop = top;
+ columnFound = i;
+ }
+ }
+ return columnFound;
+ }
+
+ private int getHighestPositionedTop() {
+ final int column = getHighestPositionedTopColumn();
+ return mColumnTops[column];
+ }
+
+ private int getHighestPositionedTopColumn() {
+ int columnFound = 0;
+ int highestPositionedTop = Integer.MAX_VALUE;
+ // the highest positioned top is the one with the lowest value :D
+ for (int i = 0; i < mColumnCount; i++) {
+ int top = mColumnTops[i];
+ if (top < highestPositionedTop) {
+ highestPositionedTop = top;
+ columnFound = i;
+ }
+ }
+ return columnFound;
+ }
+
+ // //////////////////////////////////////////////////////////////////////////////////////////
+ // LAYOUT PARAMS
+ //
+
+ /**
+ * Extended LayoutParams to column position and anything else we may been for the grid
+ */
+ public static class GridLayoutParams extends LayoutParams {
+
+ // The column the view is displayed in
+ int column;
+
+ public GridLayoutParams(Context c, AttributeSet attrs) {
+ super(c, attrs);
+ enforceStaggeredLayout();
+ }
+
+ public GridLayoutParams(int w, int h) {
+ super(w, h);
+ enforceStaggeredLayout();
+ }
+
+ public GridLayoutParams(int w, int h, int viewType) {
+ super(w, h);
+ enforceStaggeredLayout();
+ }
+
+ public GridLayoutParams(ViewGroup.LayoutParams source) {
+ super(source);
+ enforceStaggeredLayout();
+ }
+
+ /**
+ * Here we're making sure that all grid view items
+ * are width MATCH_PARENT and height WRAP_CONTENT.
+ * That's what this grid is designed for
+ */
+ private void enforceStaggeredLayout() {
+ if (width != MATCH_PARENT) {
+ width = MATCH_PARENT;
+ }
+ if (height == MATCH_PARENT) {
+ height = WRAP_CONTENT;
+ }
+ }
+ }
+
+ // //////////////////////////////////////////////////////////////////////////////////////////
+ // SAVED STATE
+
+
+ public static class GridListSavedState extends ListSavedState {
+ int columnCount;
+ int[] columnTops;
+ SparseArray positionData;
+
+ public GridListSavedState(Parcelable superState) {
+ super(superState);
+ }
+
+ /**
+ * Constructor called from {@link #CREATOR}
+ */
+ public GridListSavedState(Parcel in) {
+ super(in);
+ columnCount = in.readInt();
+ columnTops = new int[columnCount >= 0 ? columnCount : 0];
+ in.readIntArray(columnTops);
+ positionData = in.readSparseArray(GridItemRecord.class.getClassLoader());
+ }
+
+ @Override
+ public void writeToParcel(Parcel out, int flags) {
+ super.writeToParcel(out, flags);
+ out.writeInt(columnCount);
+ out.writeIntArray(columnTops);
+ out.writeSparseArray(positionData);
+ }
+
+ @Override
+ public String toString() {
+ return "StaggeredGridView.GridListSavedState{"
+ + Integer.toHexString(System.identityHashCode(this)) + "}";
+ }
+
+ public static final Creator CREATOR
+ = new Creator() {
+ @Override
+ public GridListSavedState createFromParcel(Parcel in) {
+ return new GridListSavedState(in);
+ }
+
+ @Override
+ public GridListSavedState[] newArray(int size) {
+ return new GridListSavedState[size];
+ }
+ };
+ }
+
+
+ @Override
+ public Parcelable onSaveInstanceState() {
+ ListSavedState listState = (ListSavedState) super.onSaveInstanceState();
+ GridListSavedState ss = new GridListSavedState(listState.getSuperState());
+
+ // from the list state
+ ss.selectedId = listState.selectedId;
+ ss.firstId = listState.firstId;
+ ss.viewTop = listState.viewTop;
+ ss.position = listState.position;
+ ss.height = listState.height;
+
+ // our state
+
+ boolean haveChildren = getChildCount() > 0 && getCount() > 0;
+
+ if (haveChildren && mFirstPosition > 0) {
+ ss.columnCount = mColumnCount;
+ ss.columnTops = mColumnTops;
+ ss.positionData = mPositionData;
+ }
+ else {
+ ss.columnCount = mColumnCount >= 0 ? mColumnCount : 0;
+ ss.columnTops = new int[ss.columnCount];
+ ss.positionData = new SparseArray