Android Staggered Grid & List View.
Create Android Staggered Lib Project.
—> AndroidManifest.xml
<manifest xmlns:android="http://schemas.android.com/apk/res/android" package="com.android.grid" android:versionCode="1" android:versionName="1.0.3"> <uses-sdk android:minSdkVersion="11"/> <application /> </manifest>
values folder
attrs.xml
<?xml version="1.0" encoding="utf-8"?> <resources> <declare-styleable name="StaggeredGridView"> <attr name="column_count" format="integer" /> <attr name="column_count_portrait" format="integer" /> <attr name="column_count_landscape" format="integer" /> <attr name="item_margin" format="dimension" /> <attr name="grid_paddingLeft" format="dimension" /> <attr name="grid_paddingRight" format="dimension" /> <attr name="grid_paddingTop" format="dimension" /> <attr name="grid_paddingBottom" format="dimension" /> </declare-styleable> </resources>
ClassLoaderSavedState.java
package com.android.grid; import android.os.Parcel; import android.os.Parcelable; public abstract class ClassLoaderSavedState implements Parcelable { public static final ClassLoaderSavedState EMPTY_STATE = new ClassLoaderSavedState() {}; private Parcelable mSuperState = EMPTY_STATE; private ClassLoader mClassLoader; private ClassLoaderSavedState() { mSuperState = null; mClassLoader = null; } 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; } } 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<ClassLoaderSavedState> CREATOR = new Parcelable.Creator<ClassLoaderSavedState>() { 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]; } }; }
ExtendableListView.java
package com.android.grid; 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.*; import android.widget.AbsListView; import android.widget.AdapterView; import android.widget.ListAdapter; import android.widget.Scroller; import java.util.ArrayList; 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 int mTouchSlop; private int mMaximumVelocity; private int mFlingVelocity; 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; 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 RecycleBin mRecycleBin; private 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 { 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; } } } } public class FixedViewInfo { public View view; public Object data; public boolean isSelectable; } private ArrayList<FixedViewInfo> mHeaderViewInfos; private ArrayList<FixedViewInfo> 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<FixedViewInfo>(); mFooterViewInfos = new ArrayList<FixedViewInfo>(); // start our layout mode drawing from the top mLayoutMode = LAYOUT_NORMAL; } @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(); } } @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; } @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) { if (position >= 0) { mLayoutMode = LAYOUT_SYNC; mSpecificTop = getListPaddingTop(); mFirstPosition = 0; if (mNeedSync) { mSyncPosition = position; mSyncRowId = mAdapter.getItemId(position); } requestLayout(); } } 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); if (mAdapter != null && mObserver != null) { mObserver.onChanged(); } } public void addHeaderView(View v) { addHeaderView(v, null, true); } public int getHeaderViewsCount() { return mHeaderViewInfos.size(); } 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<FixedViewInfo> 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; } } } public void addFooterView(View v, Object data, boolean isSelectable) { 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(); } } public void addFooterView(View v) { addFooterView(v, null, true); } public int getFooterViewsCount() { return mFooterViewInfos.size(); } 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; } @Override public void setClipToPadding(final boolean clipToPadding) { super.setClipToPadding(clipToPadding); mClipToPadding = clipToPadding; } @Override public void requestLayout() { if (!mBlockLayoutRequests && !mInLayout) { super.requestLayout(); } } @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; } @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 } @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; } @Override public boolean onTouchEvent(MotionEvent 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) { return false; } switch (action & MotionEvent.ACTION_MASK) { case MotionEvent.ACTION_DOWN: { int touchMode = mTouchMode; 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 { 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); 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) { 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: startScrollIfNeeded(y); break; case TOUCH_MODE_SCROLLING: 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() { 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(); } } 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 { 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; } 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(); 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); } 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) { } 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; } 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; } protected boolean hasSpaceUp() { return false; } private View fillFromTop(int nextTop) { mFirstPosition = Math.min(mFirstPosition, mItemCount - 1); if (mFirstPosition < 0) { mFirstPosition = 0; } return fillDown(mFirstPosition, nextTop); } 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; } } 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; } 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; } 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); } 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); } 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; } 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(); } } } } 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(); } } } } 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 void onChildCreated(final int position, final boolean flowDown) { } 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); } 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()); } protected int getChildLeft(final int position) { return getListPaddingLeft(); } protected int getChildTop(final int position) { int count = getChildCount(); int paddingTop = 0; if (mClipToPadding) { paddingTop = getListPaddingTop(); } return count > 0 ? getChildAt(count - 1).getBottom() : paddingTop; } 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); } 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(); } } private class FlingRunnable implements Runnable { private final Scroller mScroller; 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); } 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(); 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); } 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; 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); } } @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; } } 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); } } class RecycleBin { private int mFirstActivePosition; private View[] mActiveViews = new View[0]; private ArrayList<View>[] mScrapViews; private int mViewTypeCount; private ArrayList<View> mCurrentScrap; private ArrayList<View> mSkippedScrap; private SparseArrayCompat<View> mTransientStateViews; public void setViewTypeCount(int viewTypeCount) { if (viewTypeCount < 1) { throw new IllegalArgumentException("Can't have a viewTypeCount < 1"); } //noinspection unchecked ArrayList<View>[] scrapViews = new ArrayList[viewTypeCount]; for (int i = 0; i < viewTypeCount; i++) { scrapViews[i] = new ArrayList<View>(); } mViewTypeCount = viewTypeCount; mCurrentScrap = scrapViews[0]; mScrapViews = scrapViews; } public void markChildrenDirty() { if (mViewTypeCount == 1) { final ArrayList<View> 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<View> 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; } void clear() { if (mViewTypeCount == 1) { final ArrayList<View> 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<View> 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(); } } 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; } } } 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; } void clearTransientStateViews() { if (mTransientStateViews != null) { mTransientStateViews.clear(); } } 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; } 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<View>(); } mSkippedScrap.add(scrap); } if (scrapHasTransientState) { if (mTransientStateViews == null) { mTransientStateViews = new SparseArrayCompat<View>(); } mTransientStateViews.put(position, scrap); } return; } if (mViewTypeCount == 1) { mCurrentScrap.add(scrap); } else { mScrapViews[viewType].add(scrap); } } 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(); } void scrapActiveViews() { final View[] activeViews = mActiveViews; final boolean multipleScraps = mViewTypeCount > 1; ArrayList<View> 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<View>(); } mTransientStateViews.put(mFirstActivePosition + i, victim); } continue; } if (multipleScraps) { scrapViews = mScrapViews[viewType]; } lp.position = mFirstActivePosition + i; scrapViews.add(victim); } } pruneScrapViews(); } private void pruneScrapViews() { final int maxViews = mActiveViews.length; final int viewTypeCount = mViewTypeCount; final ArrayList<View>[] scrapViews = mScrapViews; for (int i = 0; i < viewTypeCount; ++i) { final ArrayList<View> 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--; } } } } void setCacheColorHint(int color) { if (mViewTypeCount == 1) { final ArrayList<View> 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<View> 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<View> 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; } } protected int mSyncPosition; protected int mSpecificTop; long mSyncRowId = INVALID_ROW_ID; long mSyncHeight; boolean mNeedSync = false; private ListSavedState mSyncState; 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<FixedViewInfo> 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; public ListSavedState(Parcelable superState) { super(superState, AbsListView.class.getClassLoader()); } 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<ListSavedState> CREATOR = new Creator<ListSavedState>() { public ListSavedState createFromParcel(Parcel in) { return new ListSavedState(in); } 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) { 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; 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) { performHapticFeedback(HapticFeedbackConstants.LONG_PRESS); } return handled; } private class WindowRunnnable { private int mOriginalAttachCount; public void rememberWindowAttachCount() { mOriginalAttachCount = getWindowAttachCount(); } public boolean sameWindow() { return hasWindowFocus() && getWindowAttachCount() == mOriginalAttachCount; } } }
HeaderViewListAdapter.java
package com.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; 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<StaggeredGridView.FixedViewInfo> mHeaderViewInfos; ArrayList<StaggeredGridView.FixedViewInfo> 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<StaggeredGridView.FixedViewInfo> EMPTY_INFO_LIST = new ArrayList<StaggeredGridView.FixedViewInfo>(); boolean mAreAllFixedViewsSelectable; private final boolean mIsFilterable; public HeaderViewListAdapter(ArrayList<StaggeredGridView.FixedViewInfo> headerViewInfos, ArrayList<StaggeredGridView.FixedViewInfo> 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<StaggeredGridView.FixedViewInfo> 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; } }
StaggeredGridView.java
package com.android.grid; 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 java.util.Arrays; 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 SparseArray<GridItemRecord> mPositionData; private int mGridPaddingLeft; private int mGridPaddingRight; private int mGridPaddingTop; private int mGridPaddingBottom; static class GridItemRecord implements Parcelable { int column; double heightRatio; boolean isHeaderFooter; GridItemRecord() { } 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<GridItemRecord> CREATOR = new Parcelable.Creator<GridItemRecord>() { public GridItemRecord createFromParcel(Parcel in) { return new GridItemRecord(in); } public GridItemRecord[] newArray(int size) { return new GridItemRecord[size]; } }; } private int[] mColumnTops; private int[] mColumnBottoms; private int[] mColumnLefts; 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<GridItemRecord>(); } // ////////////////////////////////////////////////////////////////////////////////////////// // 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(); } // ////////////////////////////////////////////////////////////////////////////////////////// // 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; } // 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; } 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]; } } @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]; } } @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<Double> positionHeightRatios = new SparseArray<Double>(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 😀 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 😀 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 😀 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 😀 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<GridListSavedState> CREATOR = new Creator<GridListSavedState>() { public GridListSavedState createFromParcel(Parcel in) { return new GridListSavedState(in); } 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<Object>(); } return ss; } @Override public void onRestoreInstanceState(Parcelable state) { GridListSavedState ss = (GridListSavedState) state; mColumnCount = ss.columnCount; mColumnTops = ss.columnTops; mColumnBottoms = new int[mColumnCount]; mPositionData = ss.positionData; mNeedSync = true; super.onRestoreInstanceState(ss); } }
DynamicHeightImageView.java
package com.android.grid.util; import android.content.Context; import android.util.AttributeSet; import android.widget.ImageView; public class DynamicHeightImageView extends ImageView { private double mHeightRatio; public DynamicHeightImageView(Context context, AttributeSet attrs) { super(context, attrs); } public DynamicHeightImageView(Context context) { super(context); } public void setHeightRatio(double ratio) { if (ratio != mHeightRatio) { mHeightRatio = ratio; requestLayout(); } } public double getHeightRatio() { return mHeightRatio; } @Override protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { if (mHeightRatio > 0.0) { // set the image views size int width = MeasureSpec.getSize(widthMeasureSpec); int height = (int) (width * mHeightRatio); setMeasuredDimension(width, height); } else { super.onMeasure(widthMeasureSpec, heightMeasureSpec); } } }
DynamicHeightTextView.java
package com.android.grid.util; import android.content.Context; import android.util.AttributeSet; import android.widget.TextView; public class DynamicHeightTextView extends TextView { private double mHeightRatio; public DynamicHeightTextView(Context context, AttributeSet attrs) { super(context, attrs); } public DynamicHeightTextView(Context context) { super(context); } public void setHeightRatio(double ratio) { if (ratio != mHeightRatio) { mHeightRatio = ratio; requestLayout(); } } public double getHeightRatio() { return mHeightRatio; } @Override protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { if (mHeightRatio > 0.0) { // set the image views size int width = MeasureSpec.getSize(widthMeasureSpec); int height = (int) (width * mHeightRatio); setMeasuredDimension(width, height); } else { super.onMeasure(widthMeasureSpec, heightMeasureSpec); } } }
—–> Create Android Staggered Grid Project And Link Above Project AS Lib Project.
—> AndroidManifest.xml
<?xml version="1.0" encoding="utf-8"?> <manifest xmlns:android="http://schemas.android.com/apk/res/android" package="com.android.sample" android:versionCode="1" android:versionName="1.0" > <uses-sdk android:minSdkVersion="11" android:targetSdkVersion="22" /> <application android:allowBackup="true" android:icon="@drawable/ic_launcher" android:label="@string/app_name" android:theme="@style/AppTheme" > <activity android:name=".MainActivity"> <intent-filter> <action android:name="android.intent.action.MAIN"/> <category android:name="android.intent.category.DEFAULT"/> <category android:name="android.intent.category.LAUNCHER"/> </intent-filter> </activity> <activity android:name=".StaggeredGridActivity"/> <activity android:name=".StaggeredGridActivityFragment"/> <activity android:name=".StaggeredGridEmptyViewActivity" /> <activity android:name=".ListViewActivity"/> </application> </manifest>
In Values Folder
colors.xml
<?xml version="1.0" encoding="utf-8"?> <resources> <color name="orange">#ffffdc7e</color> <color name="grey">#ffd3d3d3</color> <color name="green">#ff8dd304</color> <color name="blue">#ff82e0ff</color> <color name="yellow">#fffffbae</color> <color name="red">#fff10800</color> <color name="list_item_pressed">#1A000000</color> </resources>
integers.xml
<?xml version="1.0" encoding="utf-8"?> <resources> <integer name="grid_column_count">2</integer> </resources>
IN Layout Folder
activity_list_view.xml
<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android" android:layout_width="match_parent" android:layout_height="match_parent"> <ListView android:id="@+id/list_view" android:layout_width="match_parent" android:layout_height="match_parent" android:listSelector="@drawable/list_item_selector" android:drawSelectorOnTop="true" /> </FrameLayout>
activity_main.xml
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android" android:layout_width="match_parent" android:layout_height="match_parent" android:orientation="vertical"> <Button android:id="@+id/btn_sgv" android:layout_width="match_parent" android:layout_height="wrap_content" android:gravity="center" android:padding="16dp" android:layout_margin="16dp" android:text="Staggered Grid View" /> <Button android:id="@+id/btn_sgv_fragment" android:layout_width="match_parent" android:layout_height="wrap_content" android:gravity="center" android:padding="16dp" android:layout_margin="16dp" android:text="Staggered Grid View in Fragment" /> <Button android:id="@+id/btn_sgv_empty_view" android:layout_width="match_parent" android:layout_height="wrap_content" android:gravity="center" android:padding="16dp" android:layout_margin="16dp" android:text="Staggered Grid View with Empty View" /> <Button android:id="@+id/btn_listview" android:layout_width="match_parent" android:layout_height="wrap_content" android:gravity="center" android:padding="16dp" android:layout_margin="16dp" android:text="List View" /> </LinearLayout>
activity_sgv_empy_view.xml
<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android" xmlns:app="http://schemas.android.com/apk/res-auto" android:layout_width="match_parent" android:layout_height="match_parent"> <com.android.grid.StaggeredGridView android:id="@+id/grid_view" android:layout_width="match_parent" android:layout_height="match_parent" app:item_margin="8dp" app:column_count="@integer/grid_column_count" /> <TextView android:id="@android:id/empty" android:layout_width="match_parent" android:layout_height="match_parent" android:gravity="center" android:textAppearance="?android:textAppearanceMedium" android:text="Loading data..."/> </FrameLayout>
activity_sgv.xml
<com.android.grid.StaggeredGridView xmlns:android="http://schemas.android.com/apk/res/android" xmlns:app="http://schemas.android.com/apk/res-auto" android:id="@+id/grid_view" android:layout_width="match_parent" android:layout_height="match_parent" app:item_margin="8dp" app:column_count="@integer/grid_column_count" />
list_item_header_footer.xml
<?xml version="1.0" encoding="utf-8"?> <LinearLayout xmlns:android="http://schemas.android.com/apk/res/android" android:orientation="horizontal" android:layout_width="match_parent" android:layout_height="wrap_content" android:background="@color/red"> <TextView android:id="@+id/txt_title" android:layout_width="match_parent" android:layout_height="48dp" android:layout_weight="1" android:gravity="center" /> </LinearLayout>
list_item_sample.xml
<?xml version="1.0" encoding="utf-8"?> <FrameLayout xmlns:android="http://schemas.android.com/apk/res/android" android:orientation="horizontal" android:id="@+id/panel_content" android:layout_width="match_parent" android:layout_height="wrap_content" android:descendantFocusability="blocksDescendants"> <com.android.grid.util.DynamicHeightTextView android:id="@+id/txt_line1" android:layout_width="match_parent" android:layout_height="wrap_content" android:gravity="center"/> <Button android:id="@+id/btn_go" android:layout_width="wrap_content" android:layout_height="wrap_content" android:layout_gravity="top|right" android:text="Go" /> </FrameLayout>
IN drawable Folder
list_item_selector.xml
<?xml version="1.0" encoding="utf-8"?> <selector xmlns:android="http://schemas.android.com/apk/res/android"> <!-- shapes defined for Android 2.3 drawable issues --> <item android:state_pressed="true" > <shape android:shape="rectangle"> <solid android:color="@color/list_item_pressed" /> </shape> </item> <!-- default --> <item> <shape android:shape="rectangle"> <solid android:color="@android:color/transparent" /> </shape> </item> </selector>
ListViewActivity.java
package com.android.sample; import android.app.Activity; import android.os.Bundle; import android.view.LayoutInflater; import android.view.View; import android.widget.*; import java.util.List; public class ListViewActivity extends Activity implements AdapterView.OnItemClickListener { @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_list_view); setTitle("ListView"); final ListView listView = (ListView) findViewById(R.id.list_view); LayoutInflater layoutInflater = getLayoutInflater(); View header = layoutInflater.inflate(R.layout.list_item_header_footer, null); View footer = layoutInflater.inflate(R.layout.list_item_header_footer, null); TextView txtHeaderTitle = (TextView) header.findViewById(R.id.txt_title); TextView txtFooterTitle = (TextView) footer.findViewById(R.id.txt_title); txtHeaderTitle.setText("THE HEADER!"); txtFooterTitle.setText("THE FOOTER!"); listView.addHeaderView(header); listView.addFooterView(footer); final SampleAdapter adapter = new SampleAdapter(this, R.id.txt_line1); listView.setAdapter(adapter); listView.setOnItemClickListener(this); final List<String> sampleData = SampleData.generateSampleData(); for (String data : sampleData) { adapter.add(data); } } @Override public void onItemClick(AdapterView<?> adapterView, View view, int position, long id) { Toast.makeText(this, "Item Clicked: " + position, Toast.LENGTH_SHORT).show(); } }
MainActivity.java
package com.android.sample; import android.app.Activity; import android.content.Intent; import android.os.Bundle; import android.view.View; public class MainActivity extends Activity implements View.OnClickListener { @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setTitle("SGV Sample"); setContentView(R.layout.activity_main); findViewById(R.id.btn_sgv).setOnClickListener(this); findViewById(R.id.btn_sgv_fragment).setOnClickListener(this); findViewById(R.id.btn_sgv_empty_view).setOnClickListener(this); findViewById(R.id.btn_listview).setOnClickListener(this); } @Override public void onClick(final View v) { if (v.getId() == R.id.btn_sgv) { startActivity(new Intent(this, StaggeredGridActivity.class)); } else if (v.getId() == R.id.btn_sgv_fragment) { startActivity(new Intent(this, StaggeredGridActivityFragment.class)); } else if (v.getId() == R.id.btn_sgv_empty_view) { startActivity(new Intent(this, StaggeredGridEmptyViewActivity.class)); } else if (v.getId() == R.id.btn_listview) { startActivity(new Intent(this, ListViewActivity.class)); } } }
SampleAdapter.java
package com.android.sample; import java.util.ArrayList; import java.util.Random; import com.android.grid.util.DynamicHeightTextView; import android.content.Context; import android.util.Log; import android.util.SparseArray; import android.view.LayoutInflater; import android.view.View; import android.view.ViewGroup; import android.widget.ArrayAdapter; import android.widget.Button; import android.widget.Toast; public class SampleAdapter extends ArrayAdapter<String> { private static final String TAG = "SampleAdapter"; static class ViewHolder { DynamicHeightTextView txtLineOne; Button btnGo; } private final LayoutInflater mLayoutInflater; private final Random mRandom; private final ArrayList<Integer> mBackgroundColors; private static final SparseArray<Double> sPositionHeightRatios = new SparseArray<Double>(); public SampleAdapter(final Context context, final int textViewResourceId) { super(context, textViewResourceId); mLayoutInflater = LayoutInflater.from(context); mRandom = new Random(); mBackgroundColors = new ArrayList<Integer>(); mBackgroundColors.add(R.color.orange); mBackgroundColors.add(R.color.green); mBackgroundColors.add(R.color.blue); mBackgroundColors.add(R.color.yellow); mBackgroundColors.add(R.color.grey); } @Override public View getView(final int position, View convertView, final ViewGroup parent) { ViewHolder vh; if (convertView == null) { convertView = mLayoutInflater.inflate(R.layout.list_item_sample, parent, false); vh = new ViewHolder(); vh.txtLineOne = (DynamicHeightTextView) convertView.findViewById(R.id.txt_line1); vh.btnGo = (Button) convertView.findViewById(R.id.btn_go); convertView.setTag(vh); } else { vh = (ViewHolder) convertView.getTag(); } double positionHeight = getPositionRatio(position); int backgroundIndex = position >= mBackgroundColors.size() ? position % mBackgroundColors.size() : position; convertView.setBackgroundResource(mBackgroundColors.get(backgroundIndex)); Log.d(TAG, "getView position:" + position + " h:" + positionHeight); vh.txtLineOne.setHeightRatio(positionHeight); vh.txtLineOne.setText(getItem(position) + position); vh.btnGo.setOnClickListener(new View.OnClickListener() { @Override public void onClick(final View v) { Toast.makeText(getContext(), "Button Clicked Position " + position, Toast.LENGTH_SHORT).show(); } }); return convertView; } private double getPositionRatio(final int position) { double ratio = sPositionHeightRatios.get(position, 0.0); if (ratio == 0) { ratio = getRandomHeightRatio(); sPositionHeightRatios.append(position, ratio); Log.d(TAG, "getPositionRatio:" + position + " ratio:" + ratio); } return ratio; } private double getRandomHeightRatio() { return (mRandom.nextDouble() / 2.0) + 1.0; // height will be 1.0 - 1.5 the width } }
SampleData.java
package com.android.sample; import java.util.ArrayList; import java.util.List; public class SampleData { public static final int SAMPLE_DATA_ITEM_COUNT = 30; public static ArrayList<String> generateSampleData() { final ArrayList<String> data = new ArrayList<String>(SAMPLE_DATA_ITEM_COUNT); for (int i = 0; i < SAMPLE_DATA_ITEM_COUNT; i++) { data.add("SAMPLE #"); } return data; } }
StaggeredGridActivity.java
package com.android.sample; import java.util.ArrayList; import android.app.Activity; import android.os.Bundle; import android.util.Log; import android.view.LayoutInflater; import android.view.Menu; import android.view.MenuItem; import android.view.View; import android.widget.AbsListView; import android.widget.AdapterView; import android.widget.TextView; import android.widget.Toast; import com.android.grid.StaggeredGridView; public class StaggeredGridActivity extends Activity implements AbsListView.OnScrollListener, AbsListView.OnItemClickListener, AdapterView.OnItemLongClickListener { private static final String TAG = "StaggeredGridActivity"; public static final String SAVED_DATA_KEY = "SAVED_DATA"; private StaggeredGridView mGridView; private boolean mHasRequestedMore; private SampleAdapter mAdapter; private ArrayList<String> mData; @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_sgv); setTitle("SGV"); mGridView = (StaggeredGridView) findViewById(R.id.grid_view); LayoutInflater layoutInflater = getLayoutInflater(); View header = layoutInflater.inflate(R.layout.list_item_header_footer, null); View footer = layoutInflater.inflate(R.layout.list_item_header_footer, null); TextView txtHeaderTitle = (TextView) header.findViewById(R.id.txt_title); TextView txtFooterTitle = (TextView) footer.findViewById(R.id.txt_title); txtHeaderTitle.setText("THE HEADER!"); txtFooterTitle.setText("THE FOOTER!"); mGridView.addHeaderView(header); mGridView.addFooterView(footer); mAdapter = new SampleAdapter(this, R.id.txt_line1); // do we have saved data? if (savedInstanceState != null) { mData = savedInstanceState.getStringArrayList(SAVED_DATA_KEY); } if (mData == null) { mData = SampleData.generateSampleData(); } for (String data : mData) { mAdapter.add(data); } mGridView.setAdapter(mAdapter); mGridView.setOnScrollListener(this); mGridView.setOnItemClickListener(this); mGridView.setOnItemLongClickListener(this); } @Override public boolean onCreateOptionsMenu(Menu menu) { getMenuInflater().inflate(R.menu.menu_sgv_dynamic, menu); return true; } @Override public boolean onOptionsItemSelected(MenuItem item) { switch (item.getItemId()) { case R.id.col1: mGridView.setColumnCount(1); break; case R.id.col2: mGridView.setColumnCount(2); break; case R.id.col3: mGridView.setColumnCount(3); break; } return true; } @Override protected void onSaveInstanceState(final Bundle outState) { super.onSaveInstanceState(outState); outState.putStringArrayList(SAVED_DATA_KEY, mData); } @Override public void onScrollStateChanged(final AbsListView view, final int scrollState) { Log.d(TAG, "onScrollStateChanged:" + scrollState); } @Override public void onScroll(final AbsListView view, final int firstVisibleItem, final int visibleItemCount, final int totalItemCount) { Log.d(TAG, "onScroll firstVisibleItem:" + firstVisibleItem + " visibleItemCount:" + visibleItemCount + " totalItemCount:" + totalItemCount); // our handling if (!mHasRequestedMore) { int lastInScreen = firstVisibleItem + visibleItemCount; if (lastInScreen >= totalItemCount) { Log.d(TAG, "onScroll lastInScreen - so load more"); mHasRequestedMore = true; onLoadMoreItems(); } } } private void onLoadMoreItems() { final ArrayList<String> sampleData = SampleData.generateSampleData(); for (String data : sampleData) { mAdapter.add(data); } // stash all the data in our backing store mData.addAll(sampleData); // notify the adapter that we can update now mAdapter.notifyDataSetChanged(); mHasRequestedMore = false; } @Override public void onItemClick(AdapterView<?> adapterView, View view, int position, long id) { Toast.makeText(this, "Item Clicked: " + position, Toast.LENGTH_SHORT).show(); } @Override public boolean onItemLongClick(AdapterView<?> parent, View view, int position, long id) { Toast.makeText(this, "Item Long Clicked: " + position, Toast.LENGTH_SHORT).show(); return true; } }
StaggeredGridActivityFragment.java
package com.android.sample; import android.os.Bundle; import android.support.v4.app.Fragment; import android.support.v4.app.FragmentActivity; import android.support.v4.app.FragmentManager; import android.util.Log; import android.view.*; import android.widget.AbsListView; import android.widget.AdapterView; import android.widget.TextView; import android.widget.Toast; import com.android.grid.StaggeredGridView; import java.util.ArrayList; public class StaggeredGridActivityFragment extends FragmentActivity { private static final String TAG = "StaggeredGridActivityFragment"; @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setTitle("SGV"); final FragmentManager fm = getSupportFragmentManager(); // Create the list fragment and add it as our sole content. if (fm.findFragmentById(android.R.id.content) == null) { final StaggeredGridFragment fragment = new StaggeredGridFragment(); fm.beginTransaction().add(android.R.id.content, fragment).commit(); } } private class StaggeredGridFragment extends Fragment implements AbsListView.OnScrollListener, AbsListView.OnItemClickListener { private StaggeredGridView mGridView; private boolean mHasRequestedMore; private SampleAdapter mAdapter; private ArrayList<String> mData; @Override public void onCreate(final Bundle savedInstanceState) { super.onCreate(savedInstanceState); setRetainInstance(true); } @Override public View onCreateView(final LayoutInflater inflater, final ViewGroup container, final Bundle savedInstanceState) { return inflater.inflate(R.layout.activity_sgv, container, false); } @Override public void onActivityCreated(final Bundle savedInstanceState) { super.onActivityCreated(savedInstanceState); mGridView = (StaggeredGridView) getView().findViewById(R.id.grid_view); if (savedInstanceState == null) { final LayoutInflater layoutInflater = getActivity().getLayoutInflater(); View header = layoutInflater.inflate(R.layout.list_item_header_footer, null); View footer = layoutInflater.inflate(R.layout.list_item_header_footer, null); TextView txtHeaderTitle = (TextView) header.findViewById(R.id.txt_title); TextView txtFooterTitle = (TextView) footer.findViewById(R.id.txt_title); txtHeaderTitle.setText("THE HEADER!"); txtFooterTitle.setText("THE FOOTER!"); mGridView.addHeaderView(header); mGridView.addFooterView(footer); } if (mAdapter == null) { mAdapter = new SampleAdapter(getActivity(), R.id.txt_line1); } if (mData == null) { mData = SampleData.generateSampleData(); } for (String data : mData) { mAdapter.add(data); } mGridView.setAdapter(mAdapter); mGridView.setOnScrollListener(this); mGridView.setOnItemClickListener(this); } @Override public void onScrollStateChanged(final AbsListView view, final int scrollState) { Log.d(TAG, "onScrollStateChanged:" + scrollState); } @Override public void onScroll(final AbsListView view, final int firstVisibleItem, final int visibleItemCount, final int totalItemCount) { Log.d(TAG, "onScroll firstVisibleItem:" + firstVisibleItem + " visibleItemCount:" + visibleItemCount + " totalItemCount:" + totalItemCount); // our handling if (!mHasRequestedMore) { int lastInScreen = firstVisibleItem + visibleItemCount; if (lastInScreen >= totalItemCount) { Log.d(TAG, "onScroll lastInScreen - so load more"); mHasRequestedMore = true; onLoadMoreItems(); } } } private void onLoadMoreItems() { final ArrayList<String> sampleData = SampleData.generateSampleData(); for (String data : sampleData) { mAdapter.add(data); } // stash all the data in our backing store mData.addAll(sampleData); // notify the adapter that we can update now mAdapter.notifyDataSetChanged(); mHasRequestedMore = false; } @Override public void onItemClick(AdapterView<?> adapterView, View view, int position, long id) { Toast.makeText(getActivity(), "Item Clicked: " + position, Toast.LENGTH_SHORT).show(); } } }
StaggeredGridEmptyViewActivity.java
package com.android.sample; import android.app.Activity; import android.os.AsyncTask; import android.os.Bundle; import android.os.SystemClock; import android.view.LayoutInflater; import android.view.Menu; import android.view.MenuItem; import android.view.View; import android.widget.AbsListView; import android.widget.AdapterView; import android.widget.TextView; import android.widget.Toast; import com.android.grid.StaggeredGridView; import java.util.ArrayList; public class StaggeredGridEmptyViewActivity extends Activity implements AbsListView.OnItemClickListener { public static final String SAVED_DATA_KEY = "SAVED_DATA"; private static final int FETCH_DATA_TASK_DURATION = 2000; private StaggeredGridView mGridView; private SampleAdapter mAdapter; private ArrayList<String> mData; @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_sgv_empy_view); setTitle("SGV"); mGridView = (StaggeredGridView) findViewById(R.id.grid_view); LayoutInflater layoutInflater = getLayoutInflater(); View header = layoutInflater.inflate(R.layout.list_item_header_footer, null); View footer = layoutInflater.inflate(R.layout.list_item_header_footer, null); TextView txtHeaderTitle = (TextView) header.findViewById(R.id.txt_title); TextView txtFooterTitle = (TextView) footer.findViewById(R.id.txt_title); txtHeaderTitle.setText("THE HEADER!"); txtFooterTitle.setText("THE FOOTER!"); mGridView.addHeaderView(header); mGridView.addFooterView(footer); mGridView.setEmptyView(findViewById(android.R.id.empty)); mAdapter = new SampleAdapter(this, R.id.txt_line1); // do we have saved data? if (savedInstanceState != null) { mData = savedInstanceState.getStringArrayList(SAVED_DATA_KEY); fillAdapter(); } if (mData == null) { mData = SampleData.generateSampleData(); } mGridView.setAdapter(mAdapter); mGridView.setOnItemClickListener(this); fetchData(); } private void fillAdapter() { for (String data : mData) { mAdapter.add(data); } } private void fetchData() { new AsyncTask<Void, Void, Void>() { @Override protected Void doInBackground(Void... params) { SystemClock.sleep(FETCH_DATA_TASK_DURATION); return null; } @Override protected void onPostExecute(Void aVoid) { fillAdapter(); } }.execute(); } @Override public boolean onCreateOptionsMenu(Menu menu) { getMenuInflater().inflate(R.menu.activity_sgv_empty_view, menu); return true; } @Override public boolean onOptionsItemSelected(MenuItem item) { mAdapter.clear(); fetchData(); return true; } @Override public void onItemClick(AdapterView<?> adapterView, View view, int position, long id) { Toast.makeText(this, "Item Clicked: " + position, Toast.LENGTH_SHORT).show(); } @Override protected void onSaveInstanceState(final Bundle outState) { super.onSaveInstanceState(outState); outState.putStringArrayList(SAVED_DATA_KEY, mData); } }
Leave a Reply