| /* |
| * Copyright (C) 2012 The Android Open Source Project |
| * |
| * Licensed under the Apache License, Version 2.0 (the "License"); |
| * you may not use this file except in compliance with the License. |
| * You may obtain a copy of the License at |
| * |
| * http://www.apache.org/licenses/LICENSE-2.0 |
| * |
| * Unless required by applicable law or agreed to in writing, software |
| * distributed under the License is distributed on an "AS IS" BASIS, |
| * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
| * See the License for the specific language governing permissions and |
| * limitations under the License. |
| */ |
| |
| package com.android.ex.widget; |
| |
| import android.content.Context; |
| import android.content.res.TypedArray; |
| import android.database.DataSetObserver; |
| import android.graphics.Canvas; |
| import android.os.Parcel; |
| import android.os.Parcelable; |
| import android.support.v4.util.SparseArrayCompat; |
| import android.support.v4.view.MotionEventCompat; |
| import android.support.v4.view.VelocityTrackerCompat; |
| import android.support.v4.view.ViewCompat; |
| import android.util.AttributeSet; |
| import android.util.Log; |
| import android.util.SparseArray; |
| import android.view.MotionEvent; |
| import android.view.VelocityTracker; |
| import android.view.View; |
| import android.view.ViewConfiguration; |
| import android.view.ViewGroup; |
| import android.widget.ListAdapter; |
| |
| import java.util.ArrayList; |
| import java.util.Arrays; |
| |
| /** |
| * ListView and GridView just not complex enough? Try StaggeredGridView! |
| * |
| * <p>StaggeredGridView presents a multi-column grid with consistent column sizes |
| * but varying row sizes between the columns. Each successive item from a |
| * {@link android.widget.ListAdapter ListAdapter} will be arranged from top to bottom, |
| * left to right. The largest vertical gap is always filled first.</p> |
| * |
| * <p>Item views may span multiple columns as specified by their {@link LayoutParams}. |
| * The attribute <code>android:layout_span</code> may be used when inflating |
| * item views from xml.</p> |
| * |
| * <p>This class is still under development and is not fully functional yet.</p> |
| */ |
| public class StaggeredGridView extends ViewGroup { |
| private static final String TAG = "StaggeredGridView"; |
| private static final boolean DEBUG = false; |
| |
| /* |
| * There are a few things you should know if you're going to make modifications |
| * to StaggeredGridView. |
| * |
| * Like ListView, SGV populates from an adapter and recycles views that fall out |
| * of the visible boundaries of the grid. A few invariants always hold: |
| * |
| * - mFirstPosition is the adapter position of the View returned by getChildAt(0). |
| * - Any child index can be translated to an adapter position by adding mFirstPosition. |
| * - Any adapter position can be translated to a child index by subtracting mFirstPosition. |
| * - Views for items in the range [mFirstPosition, mFirstPosition + getChildCount()) are |
| * currently attached to the grid as children. All other adapter positions do not have |
| * active views. |
| * |
| * This means a few things thanks to the staggered grid's nature. Some views may stay attached |
| * long after they have scrolled offscreen if removing and recycling them would result in |
| * breaking one of the invariants above. |
| * |
| * LayoutRecords are used to track data about a particular item's layout after the associated |
| * view has been removed. These let positioning and the choice of column for an item |
| * remain consistent even though the rules for filling content up vs. filling down vary. |
| * |
| * Whenever layout parameters for a known LayoutRecord change, other LayoutRecords before |
| * or after it may need to be invalidated. e.g. if the item's height or the number |
| * of columns it spans changes, all bets for other items in the same direction are off |
| * since the cached information no longer applies. |
| */ |
| |
| private ListAdapter mAdapter; |
| |
| public static final int COLUMN_COUNT_AUTO = -1; |
| |
| private int mColCountSetting = 2; |
| private int mColCount = 2; |
| private int mMinColWidth = 0; |
| private int mItemMargin; |
| |
| private int[] mItemTops; |
| private int[] mItemBottoms; |
| |
| private boolean mFastChildLayout; |
| private boolean mPopulating; |
| private boolean mForcePopulateOnLayout; |
| private boolean mInLayout; |
| private int mRestoreOffset; |
| |
| private final RecycleBin mRecycler = new RecycleBin(); |
| |
| private final AdapterDataSetObserver mObserver = new AdapterDataSetObserver(); |
| |
| private boolean mDataChanged; |
| private int mOldItemCount; |
| private int mItemCount; |
| private boolean mHasStableIds; |
| |
| private int mFirstPosition; |
| |
| private int mTouchSlop; |
| private int mMaximumVelocity; |
| private int mFlingVelocity; |
| private float mLastTouchY; |
| private float mTouchRemainderY; |
| private int mActivePointerId; |
| |
| private static final int TOUCH_MODE_IDLE = 0; |
| private static final int TOUCH_MODE_DRAGGING = 1; |
| private static final int TOUCH_MODE_FLINGING = 2; |
| |
| private int mTouchMode; |
| private final VelocityTracker mVelocityTracker = VelocityTracker.obtain(); |
| private final ScrollerCompat mScroller; |
| |
| private final EdgeEffectCompat mTopEdge; |
| private final EdgeEffectCompat mBottomEdge; |
| |
| private static final class LayoutRecord { |
| public int column; |
| public long id = -1; |
| public int height; |
| public int span; |
| private int[] mMargins; |
| |
| private final void ensureMargins() { |
| if (mMargins == null) { |
| // Don't need to confirm length; |
| // all layoutrecords are purged when column count changes. |
| mMargins = new int[span * 2]; |
| } |
| } |
| |
| public final int getMarginAbove(int col) { |
| if (mMargins == null) { |
| return 0; |
| } |
| return mMargins[col * 2]; |
| } |
| |
| public final int getMarginBelow(int col) { |
| if (mMargins == null) { |
| return 0; |
| } |
| return mMargins[col * 2 + 1]; |
| } |
| |
| public final void setMarginAbove(int col, int margin) { |
| if (mMargins == null && margin == 0) { |
| return; |
| } |
| ensureMargins(); |
| mMargins[col * 2] = margin; |
| } |
| |
| public final void setMarginBelow(int col, int margin) { |
| if (mMargins == null && margin == 0) { |
| return; |
| } |
| ensureMargins(); |
| mMargins[col * 2 + 1] = margin; |
| } |
| |
| @Override |
| public String toString() { |
| String result = "LayoutRecord{c=" + column + ", id=" + id + " h=" + height + |
| " s=" + span; |
| if (mMargins != null) { |
| result += " margins[above, below]("; |
| for (int i = 0; i < mMargins.length; i += 2) { |
| result += "[" + mMargins[i] + ", " + mMargins[i+1] + "]"; |
| } |
| result += ")"; |
| } |
| return result + "}"; |
| } |
| } |
| private final SparseArrayCompat<LayoutRecord> mLayoutRecords = |
| new SparseArrayCompat<LayoutRecord>(); |
| |
| public StaggeredGridView(Context context) { |
| this(context, null); |
| } |
| |
| public StaggeredGridView(Context context, AttributeSet attrs) { |
| this(context, attrs, 0); |
| } |
| |
| public StaggeredGridView(Context context, AttributeSet attrs, int defStyle) { |
| super(context, attrs, defStyle); |
| |
| final ViewConfiguration vc = ViewConfiguration.get(context); |
| mTouchSlop = vc.getScaledTouchSlop(); |
| mMaximumVelocity = vc.getScaledMaximumFlingVelocity(); |
| mFlingVelocity = vc.getScaledMinimumFlingVelocity(); |
| mScroller = ScrollerCompat.from(context); |
| |
| mTopEdge = new EdgeEffectCompat(context); |
| mBottomEdge = new EdgeEffectCompat(context); |
| setWillNotDraw(false); |
| setClipToPadding(false); |
| } |
| |
| /** |
| * Set a fixed number of columns for this grid. Space will be divided evenly |
| * among all columns, respecting the item margin between columns. |
| * The default is 2. (If it were 1, perhaps you should be using a |
| * {@link android.widget.ListView ListView}.) |
| * |
| * @param colCount Number of columns to display. |
| * @see #setMinColumnWidth(int) |
| */ |
| public void setColumnCount(int colCount) { |
| if (colCount < 1 && colCount != COLUMN_COUNT_AUTO) { |
| throw new IllegalArgumentException("Column count must be at least 1 - received " + |
| colCount); |
| } |
| final boolean needsPopulate = colCount != mColCount; |
| mColCount = mColCountSetting = colCount; |
| if (needsPopulate) { |
| populate(); |
| } |
| } |
| |
| public int getColumnCount() { |
| return mColCount; |
| } |
| |
| /** |
| * Set a minimum column width for |
| * @param minColWidth |
| */ |
| public void setMinColumnWidth(int minColWidth) { |
| mMinColWidth = minColWidth; |
| setColumnCount(COLUMN_COUNT_AUTO); |
| } |
| |
| /** |
| * Set the margin between items in pixels. This margin is applied |
| * both vertically and horizontally. |
| * |
| * @param marginPixels Spacing between items in pixels |
| */ |
| public void setItemMargin(int marginPixels) { |
| final boolean needsPopulate = marginPixels != mItemMargin; |
| mItemMargin = marginPixels; |
| if (needsPopulate) { |
| populate(); |
| } |
| } |
| |
| /** |
| * Return the first adapter position with a view currently attached as |
| * a child view of this grid. |
| * |
| * @return the adapter position represented by the view at getChildAt(0). |
| */ |
| public int getFirstPosition() { |
| return mFirstPosition; |
| } |
| |
| @Override |
| public boolean onInterceptTouchEvent(MotionEvent ev) { |
| mVelocityTracker.addMovement(ev); |
| final int action = ev.getAction() & MotionEventCompat.ACTION_MASK; |
| switch (action) { |
| case MotionEvent.ACTION_DOWN: |
| mVelocityTracker.clear(); |
| mScroller.abortAnimation(); |
| mLastTouchY = ev.getY(); |
| mActivePointerId = MotionEventCompat.getPointerId(ev, 0); |
| mTouchRemainderY = 0; |
| if (mTouchMode == TOUCH_MODE_FLINGING) { |
| // Catch! |
| mTouchMode = TOUCH_MODE_DRAGGING; |
| return true; |
| } |
| break; |
| |
| case MotionEvent.ACTION_MOVE: { |
| final int index = MotionEventCompat.findPointerIndex(ev, mActivePointerId); |
| if (index < 0) { |
| Log.e(TAG, "onInterceptTouchEvent could not find pointer with id " + |
| mActivePointerId + " - did StaggeredGridView receive an inconsistent " + |
| "event stream?"); |
| return false; |
| } |
| final float y = MotionEventCompat.getY(ev, index); |
| final float dy = y - mLastTouchY + mTouchRemainderY; |
| final int deltaY = (int) dy; |
| mTouchRemainderY = dy - deltaY; |
| |
| if (Math.abs(dy) > mTouchSlop) { |
| mTouchMode = TOUCH_MODE_DRAGGING; |
| return true; |
| } |
| } |
| } |
| |
| return false; |
| } |
| |
| @Override |
| public boolean onTouchEvent(MotionEvent ev) { |
| mVelocityTracker.addMovement(ev); |
| final int action = ev.getAction() & MotionEventCompat.ACTION_MASK; |
| switch (action) { |
| case MotionEvent.ACTION_DOWN: |
| mVelocityTracker.clear(); |
| mScroller.abortAnimation(); |
| mLastTouchY = ev.getY(); |
| mActivePointerId = MotionEventCompat.getPointerId(ev, 0); |
| mTouchRemainderY = 0; |
| break; |
| |
| case MotionEvent.ACTION_MOVE: { |
| final int index = MotionEventCompat.findPointerIndex(ev, mActivePointerId); |
| if (index < 0) { |
| Log.e(TAG, "onInterceptTouchEvent could not find pointer with id " + |
| mActivePointerId + " - did StaggeredGridView receive an inconsistent " + |
| "event stream?"); |
| return false; |
| } |
| final float y = MotionEventCompat.getY(ev, index); |
| final float dy = y - mLastTouchY + mTouchRemainderY; |
| final int deltaY = (int) dy; |
| mTouchRemainderY = dy - deltaY; |
| |
| if (Math.abs(dy) > mTouchSlop) { |
| mTouchMode = TOUCH_MODE_DRAGGING; |
| } |
| |
| if (mTouchMode == TOUCH_MODE_DRAGGING) { |
| mLastTouchY = y; |
| |
| if (!trackMotionScroll(deltaY, true)) { |
| // Break fling velocity if we impacted an edge. |
| mVelocityTracker.clear(); |
| } |
| } |
| } break; |
| |
| case MotionEvent.ACTION_CANCEL: |
| mTouchMode = TOUCH_MODE_IDLE; |
| break; |
| |
| case MotionEvent.ACTION_UP: { |
| mVelocityTracker.computeCurrentVelocity(1000, mMaximumVelocity); |
| final float velocity = VelocityTrackerCompat.getYVelocity(mVelocityTracker, |
| mActivePointerId); |
| if (Math.abs(velocity) > mFlingVelocity) { // TODO |
| mTouchMode = TOUCH_MODE_FLINGING; |
| mScroller.fling(0, 0, 0, (int) velocity, 0, 0, |
| Integer.MIN_VALUE, Integer.MAX_VALUE); |
| mLastTouchY = 0; |
| ViewCompat.postInvalidateOnAnimation(this); |
| } else { |
| mTouchMode = TOUCH_MODE_IDLE; |
| } |
| |
| } break; |
| } |
| return true; |
| } |
| |
| /** |
| * |
| * @param deltaY Pixels that content should move by |
| * @return true if the movement completed, false if it was stopped prematurely. |
| */ |
| private boolean trackMotionScroll(int deltaY, boolean allowOverScroll) { |
| final boolean contentFits = contentFits(); |
| final int allowOverhang = Math.abs(deltaY); |
| |
| final int overScrolledBy; |
| final int movedBy; |
| if (!contentFits) { |
| final int overhang; |
| final boolean up; |
| mPopulating = true; |
| if (deltaY > 0) { |
| overhang = fillUp(mFirstPosition - 1, allowOverhang); |
| up = true; |
| } else { |
| overhang = fillDown(mFirstPosition + getChildCount(), allowOverhang) + mItemMargin; |
| up = false; |
| } |
| movedBy = Math.min(overhang, allowOverhang); |
| offsetChildren(up ? movedBy : -movedBy); |
| recycleOffscreenViews(); |
| mPopulating = false; |
| overScrolledBy = allowOverhang - overhang; |
| } else { |
| overScrolledBy = allowOverhang; |
| movedBy = 0; |
| } |
| |
| if (allowOverScroll) { |
| final int overScrollMode = ViewCompat.getOverScrollMode(this); |
| |
| if (overScrollMode == ViewCompat.OVER_SCROLL_ALWAYS || |
| (overScrollMode == ViewCompat.OVER_SCROLL_IF_CONTENT_SCROLLS && !contentFits)) { |
| |
| if (overScrolledBy > 0) { |
| EdgeEffectCompat edge = deltaY > 0 ? mTopEdge : mBottomEdge; |
| edge.onPull((float) Math.abs(deltaY) / getHeight()); |
| ViewCompat.postInvalidateOnAnimation(this); |
| } |
| } |
| } |
| |
| return deltaY == 0 || movedBy != 0; |
| } |
| |
| private final boolean contentFits() { |
| if (mFirstPosition != 0 || getChildCount() != mItemCount) { |
| return false; |
| } |
| |
| int topmost = Integer.MAX_VALUE; |
| int bottommost = Integer.MIN_VALUE; |
| for (int i = 0; i < mColCount; i++) { |
| if (mItemTops[i] < topmost) { |
| topmost = mItemTops[i]; |
| } |
| if (mItemBottoms[i] > bottommost) { |
| bottommost = mItemBottoms[i]; |
| } |
| } |
| |
| return topmost >= getPaddingTop() && bottommost <= getHeight() - getPaddingBottom(); |
| } |
| |
| private void recycleAllViews() { |
| for (int i = 0; i < getChildCount(); i++) { |
| mRecycler.addScrap(getChildAt(i)); |
| } |
| |
| if (mInLayout) { |
| removeAllViewsInLayout(); |
| } else { |
| removeAllViews(); |
| } |
| } |
| |
| /** |
| * Important: this method will leave offscreen views attached if they |
| * are required to maintain the invariant that child view with index i |
| * is always the view corresponding to position mFirstPosition + i. |
| */ |
| private void recycleOffscreenViews() { |
| final int height = getHeight(); |
| final int clearAbove = -mItemMargin; |
| final int clearBelow = height + mItemMargin; |
| for (int i = getChildCount() - 1; i >= 0; i--) { |
| final View child = getChildAt(i); |
| if (child.getTop() <= clearBelow) { |
| // There may be other offscreen views, but we need to maintain |
| // the invariant documented above. |
| break; |
| } |
| |
| if (mInLayout) { |
| removeViewsInLayout(i, 1); |
| } else { |
| removeViewAt(i); |
| } |
| |
| mRecycler.addScrap(child); |
| } |
| |
| while (getChildCount() > 0) { |
| final View child = getChildAt(0); |
| if (child.getBottom() >= clearAbove) { |
| // There may be other offscreen views, but we need to maintain |
| // the invariant documented above. |
| break; |
| } |
| |
| if (mInLayout) { |
| removeViewsInLayout(0, 1); |
| } else { |
| removeViewAt(0); |
| } |
| |
| mRecycler.addScrap(child); |
| mFirstPosition++; |
| } |
| |
| final int childCount = getChildCount(); |
| if (childCount > 0) { |
| // Repair the top and bottom column boundaries from the views we still have |
| Arrays.fill(mItemTops, Integer.MAX_VALUE); |
| Arrays.fill(mItemBottoms, Integer.MIN_VALUE); |
| |
| for (int i = 0; i < childCount; i++){ |
| final View child = getChildAt(i); |
| final LayoutParams lp = (LayoutParams) child.getLayoutParams(); |
| final int top = child.getTop() - mItemMargin; |
| final int bottom = child.getBottom(); |
| final LayoutRecord rec = mLayoutRecords.get(mFirstPosition + i); |
| |
| final int colEnd = lp.column + Math.min(mColCount, lp.span); |
| for (int col = lp.column; col < colEnd; col++) { |
| final int colTop = top - rec.getMarginAbove(col - lp.column); |
| final int colBottom = bottom + rec.getMarginBelow(col - lp.column); |
| if (colTop < mItemTops[col]) { |
| mItemTops[col] = colTop; |
| } |
| if (colBottom > mItemBottoms[col]) { |
| mItemBottoms[col] = colBottom; |
| } |
| } |
| } |
| |
| for (int col = 0; col < mColCount; col++) { |
| if (mItemTops[col] == Integer.MAX_VALUE) { |
| // If one was untouched, both were. |
| mItemTops[col] = 0; |
| mItemBottoms[col] = 0; |
| } |
| } |
| } |
| } |
| |
| public void computeScroll() { |
| if (mScroller.computeScrollOffset()) { |
| final int y = mScroller.getCurrY(); |
| final int dy = (int) (y - mLastTouchY); |
| mLastTouchY = y; |
| final boolean stopped = !trackMotionScroll(dy, false); |
| |
| if (!stopped && !mScroller.isFinished()) { |
| ViewCompat.postInvalidateOnAnimation(this); |
| } else { |
| if (stopped) { |
| final int overScrollMode = ViewCompat.getOverScrollMode(this); |
| if (overScrollMode != ViewCompat.OVER_SCROLL_NEVER) { |
| final EdgeEffectCompat edge; |
| if (dy > 0) { |
| edge = mTopEdge; |
| } else { |
| edge = mBottomEdge; |
| } |
| edge.onAbsorb(Math.abs((int) mScroller.getCurrVelocity())); |
| ViewCompat.postInvalidateOnAnimation(this); |
| } |
| mScroller.abortAnimation(); |
| } |
| mTouchMode = TOUCH_MODE_IDLE; |
| } |
| } |
| } |
| |
| @Override |
| public void draw(Canvas canvas) { |
| super.draw(canvas); |
| |
| if (mTopEdge != null) { |
| boolean needsInvalidate = false; |
| if (!mTopEdge.isFinished()) { |
| mTopEdge.draw(canvas); |
| needsInvalidate = true; |
| } |
| if (!mBottomEdge.isFinished()) { |
| final int restoreCount = canvas.save(); |
| final int width = getWidth(); |
| canvas.translate(-width, getHeight()); |
| canvas.rotate(180, width, 0); |
| mBottomEdge.draw(canvas); |
| canvas.restoreToCount(restoreCount); |
| needsInvalidate = true; |
| } |
| |
| if (needsInvalidate) { |
| ViewCompat.postInvalidateOnAnimation(this); |
| } |
| } |
| } |
| |
| public void beginFastChildLayout() { |
| mFastChildLayout = true; |
| } |
| |
| public void endFastChildLayout() { |
| mFastChildLayout = false; |
| populate(); |
| } |
| |
| @Override |
| public void requestLayout() { |
| if (!mPopulating && !mFastChildLayout) { |
| super.requestLayout(); |
| } |
| } |
| |
| @Override |
| protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { |
| int widthMode = MeasureSpec.getMode(widthMeasureSpec); |
| int heightMode = MeasureSpec.getMode(heightMeasureSpec); |
| int widthSize = MeasureSpec.getSize(widthMeasureSpec); |
| int heightSize = MeasureSpec.getSize(heightMeasureSpec); |
| |
| if (widthMode != MeasureSpec.EXACTLY) { |
| Log.e(TAG, "onMeasure: must have an exact width or match_parent! " + |
| "Using fallback spec of EXACTLY " + widthSize); |
| widthMode = MeasureSpec.EXACTLY; |
| } |
| if (heightMode != MeasureSpec.EXACTLY) { |
| Log.e(TAG, "onMeasure: must have an exact height or match_parent! " + |
| "Using fallback spec of EXACTLY " + heightSize); |
| heightMode = MeasureSpec.EXACTLY; |
| } |
| |
| setMeasuredDimension(widthSize, heightSize); |
| |
| if (mColCountSetting == COLUMN_COUNT_AUTO) { |
| final int colCount = widthSize / mMinColWidth; |
| if (colCount != mColCount) { |
| mColCount = colCount; |
| mForcePopulateOnLayout = true; |
| } |
| } |
| } |
| |
| @Override |
| protected void onLayout(boolean changed, int l, int t, int r, int b) { |
| mInLayout = true; |
| populate(); |
| mInLayout = false; |
| mForcePopulateOnLayout = false; |
| |
| final int width = r - l; |
| final int height = b - t; |
| mTopEdge.setSize(width, height); |
| mBottomEdge.setSize(width, height); |
| } |
| |
| private void populate() { |
| if (getWidth() == 0 || getHeight() == 0) { |
| return; |
| } |
| |
| if (mColCount == COLUMN_COUNT_AUTO) { |
| final int colCount = getWidth() / mMinColWidth; |
| if (colCount != mColCount) { |
| mColCount = colCount; |
| } |
| } |
| |
| final int colCount = mColCount; |
| if (mItemTops == null || mItemTops.length != colCount) { |
| mItemTops = new int[colCount]; |
| mItemBottoms = new int[colCount]; |
| final int top = getPaddingTop(); |
| final int offset = top + Math.min(mRestoreOffset, 0); |
| Arrays.fill(mItemTops, offset); |
| Arrays.fill(mItemBottoms, offset); |
| mLayoutRecords.clear(); |
| if (mInLayout) { |
| removeAllViewsInLayout(); |
| } else { |
| removeAllViews(); |
| } |
| mRestoreOffset = 0; |
| } |
| |
| mPopulating = true; |
| layoutChildren(mDataChanged); |
| fillDown(mFirstPosition + getChildCount(), 0); |
| fillUp(mFirstPosition - 1, 0); |
| mPopulating = false; |
| mDataChanged = false; |
| } |
| |
| private void dumpItemPositions() { |
| final int childCount = getChildCount(); |
| Log.d(TAG, "dumpItemPositions:"); |
| Log.d(TAG, " => Tops:"); |
| for (int i = 0; i < mColCount; i++) { |
| Log.d(TAG, " => " + mItemTops[i]); |
| boolean found = false; |
| for (int j = 0; j < childCount; j++) { |
| final View child = getChildAt(j); |
| if (mItemTops[i] == child.getTop() - mItemMargin) { |
| found = true; |
| } |
| } |
| if (!found) { |
| Log.d(TAG, "!!! No top item found for column " + i + " value " + mItemTops[i]); |
| } |
| } |
| Log.d(TAG, " => Bottoms:"); |
| for (int i = 0; i < mColCount; i++) { |
| Log.d(TAG, " => " + mItemBottoms[i]); |
| boolean found = false; |
| for (int j = 0; j < childCount; j++) { |
| final View child = getChildAt(j); |
| if (mItemBottoms[i] == child.getBottom()) { |
| found = true; |
| } |
| } |
| if (!found) { |
| Log.d(TAG, "!!! No bottom item found for column " + i + " value " + mItemBottoms[i]); |
| } |
| } |
| } |
| |
| final void offsetChildren(int offset) { |
| final int childCount = getChildCount(); |
| for (int i = 0; i < childCount; i++) { |
| final View child = getChildAt(i); |
| child.layout(child.getLeft(), child.getTop() + offset, |
| child.getRight(), child.getBottom() + offset); |
| } |
| |
| final int colCount = mColCount; |
| for (int i = 0; i < colCount; i++) { |
| mItemTops[i] += offset; |
| mItemBottoms[i] += offset; |
| } |
| } |
| |
| /** |
| * Measure and layout all currently visible children. |
| * |
| * @param queryAdapter true to requery the adapter for view data |
| */ |
| final void layoutChildren(boolean queryAdapter) { |
| final int paddingLeft = getPaddingLeft(); |
| final int paddingRight = getPaddingRight(); |
| final int itemMargin = mItemMargin; |
| final int colWidth = |
| (getWidth() - paddingLeft - paddingRight - itemMargin * (mColCount - 1)) / mColCount; |
| int rebuildLayoutRecordsBefore = -1; |
| int rebuildLayoutRecordsAfter = -1; |
| |
| Arrays.fill(mItemBottoms, Integer.MIN_VALUE); |
| |
| final int childCount = getChildCount(); |
| for (int i = 0; i < childCount; i++) { |
| View child = getChildAt(i); |
| LayoutParams lp = (LayoutParams) child.getLayoutParams(); |
| final int col = lp.column; |
| final int position = mFirstPosition + i; |
| final boolean needsLayout = queryAdapter || child.isLayoutRequested(); |
| |
| if (queryAdapter) { |
| View newView = obtainView(position, child); |
| if (newView != child) { |
| removeViewAt(i); |
| addView(newView, i); |
| child = newView; |
| } |
| lp = (LayoutParams) child.getLayoutParams(); // Might have changed |
| } |
| |
| final int span = Math.min(mColCount, lp.span); |
| final int widthSize = colWidth * span + itemMargin * (span - 1); |
| |
| if (needsLayout) { |
| final int widthSpec = MeasureSpec.makeMeasureSpec(widthSize, MeasureSpec.EXACTLY); |
| |
| final int heightSpec; |
| if (lp.height == LayoutParams.WRAP_CONTENT) { |
| heightSpec = MeasureSpec.makeMeasureSpec(0, MeasureSpec.UNSPECIFIED); |
| } else { |
| heightSpec = MeasureSpec.makeMeasureSpec(lp.height, MeasureSpec.EXACTLY); |
| } |
| |
| child.measure(widthSpec, heightSpec); |
| } |
| |
| int childTop = mItemBottoms[col] > Integer.MIN_VALUE ? |
| mItemBottoms[col] + mItemMargin : child.getTop(); |
| if (span > 1) { |
| int lowest = childTop; |
| for (int j = col + 1; j < col + span; j++) { |
| final int bottom = mItemBottoms[j] + mItemMargin; |
| if (bottom > lowest) { |
| lowest = bottom; |
| } |
| } |
| childTop = lowest; |
| } |
| final int childHeight = child.getMeasuredHeight(); |
| final int childBottom = childTop + childHeight; |
| final int childLeft = paddingLeft + col * (colWidth + itemMargin); |
| final int childRight = childLeft + child.getMeasuredWidth(); |
| child.layout(childLeft, childTop, childRight, childBottom); |
| |
| for (int j = col; j < col + span; j++) { |
| mItemBottoms[j] = childBottom; |
| } |
| |
| final LayoutRecord rec = mLayoutRecords.get(position); |
| if (rec != null && rec.height != childHeight) { |
| // Invalidate our layout records for everything before this. |
| rec.height = childHeight; |
| rebuildLayoutRecordsBefore = position; |
| } |
| |
| if (rec != null && rec.span != span) { |
| // Invalidate our layout records for everything after this. |
| rec.span = span; |
| rebuildLayoutRecordsAfter = position; |
| } |
| } |
| |
| // Update mItemBottoms for any empty columns |
| for (int i = 0; i < mColCount; i++) { |
| if (mItemBottoms[i] == Integer.MIN_VALUE) { |
| mItemBottoms[i] = mItemTops[i]; |
| } |
| } |
| |
| if (rebuildLayoutRecordsBefore >= 0 || rebuildLayoutRecordsAfter >= 0) { |
| if (rebuildLayoutRecordsBefore >= 0) { |
| invalidateLayoutRecordsBeforePosition(rebuildLayoutRecordsBefore); |
| } |
| if (rebuildLayoutRecordsAfter >= 0) { |
| invalidateLayoutRecordsAfterPosition(rebuildLayoutRecordsAfter); |
| } |
| for (int i = 0; i < childCount; i++) { |
| final int position = mFirstPosition + i; |
| final View child = getChildAt(i); |
| final LayoutParams lp = (LayoutParams) child.getLayoutParams(); |
| LayoutRecord rec = mLayoutRecords.get(position); |
| if (rec == null) { |
| rec = new LayoutRecord(); |
| mLayoutRecords.put(position, rec); |
| } |
| rec.column = lp.column; |
| rec.height = child.getHeight(); |
| rec.id = lp.id; |
| rec.span = Math.min(mColCount, lp.span); |
| } |
| } |
| } |
| |
| final void invalidateLayoutRecordsBeforePosition(int position) { |
| int endAt = 0; |
| while (endAt < mLayoutRecords.size() && mLayoutRecords.keyAt(endAt) < position) { |
| endAt++; |
| } |
| mLayoutRecords.removeAtRange(0, endAt); |
| } |
| |
| final void invalidateLayoutRecordsAfterPosition(int position) { |
| int beginAt = mLayoutRecords.size() - 1; |
| while (beginAt >= 0 && mLayoutRecords.keyAt(beginAt) > position) { |
| beginAt--; |
| } |
| beginAt++; |
| mLayoutRecords.removeAtRange(beginAt + 1, mLayoutRecords.size() - beginAt); |
| } |
| |
| /** |
| * Should be called with mPopulating set to true |
| * |
| * @param fromPosition Position to start filling from |
| * @param overhang the number of extra pixels to fill beyond the current top edge |
| * @return the max overhang beyond the beginning of the view of any added items at the top |
| */ |
| final int fillUp(int fromPosition, int overhang) { |
| final int paddingLeft = getPaddingLeft(); |
| final int paddingRight = getPaddingRight(); |
| final int itemMargin = mItemMargin; |
| final int colWidth = |
| (getWidth() - paddingLeft - paddingRight - itemMargin * (mColCount - 1)) / mColCount; |
| final int gridTop = getPaddingTop(); |
| final int fillTo = gridTop - overhang; |
| int nextCol = getNextColumnUp(); |
| int position = fromPosition; |
| |
| while (nextCol >= 0 && mItemTops[nextCol] > fillTo && position >= 0) { |
| final View child = obtainView(position, null); |
| LayoutParams lp = (LayoutParams) child.getLayoutParams(); |
| |
| if (child.getParent() != this) { |
| if (mInLayout) { |
| addViewInLayout(child, 0, lp); |
| } else { |
| addView(child, 0); |
| } |
| } |
| |
| final int span = Math.min(mColCount, lp.span); |
| final int widthSize = colWidth * span + itemMargin * (span - 1); |
| final int widthSpec = MeasureSpec.makeMeasureSpec(widthSize, MeasureSpec.EXACTLY); |
| |
| LayoutRecord rec; |
| if (span > 1) { |
| rec = getNextRecordUp(position, span); |
| nextCol = rec.column; |
| } else { |
| rec = mLayoutRecords.get(position); |
| } |
| |
| boolean invalidateBefore = false; |
| if (rec == null) { |
| rec = new LayoutRecord(); |
| mLayoutRecords.put(position, rec); |
| rec.column = nextCol; |
| rec.span = span; |
| } else if (span != rec.span) { |
| rec.span = span; |
| rec.column = nextCol; |
| invalidateBefore = true; |
| } else { |
| nextCol = rec.column; |
| } |
| |
| if (mHasStableIds) { |
| final long id = mAdapter.getItemId(position); |
| rec.id = id; |
| lp.id = id; |
| } |
| |
| lp.column = nextCol; |
| |
| final int heightSpec; |
| if (lp.height == LayoutParams.WRAP_CONTENT) { |
| heightSpec = MeasureSpec.makeMeasureSpec(0, MeasureSpec.UNSPECIFIED); |
| } else { |
| heightSpec = MeasureSpec.makeMeasureSpec(lp.height, MeasureSpec.EXACTLY); |
| } |
| child.measure(widthSpec, heightSpec); |
| |
| final int childHeight = child.getMeasuredHeight(); |
| if (invalidateBefore || (childHeight != rec.height && rec.height > 0)) { |
| invalidateLayoutRecordsBeforePosition(position); |
| } |
| rec.height = childHeight; |
| |
| final int startFrom; |
| if (span > 1) { |
| int highest = mItemTops[nextCol]; |
| for (int i = nextCol + 1; i < nextCol + span; i++) { |
| final int top = mItemTops[i]; |
| if (top < highest) { |
| highest = top; |
| } |
| } |
| startFrom = highest; |
| } else { |
| startFrom = mItemTops[nextCol]; |
| } |
| final int childBottom = startFrom; |
| final int childTop = childBottom - childHeight; |
| final int childLeft = paddingLeft + nextCol * (colWidth + itemMargin); |
| final int childRight = childLeft + child.getMeasuredWidth(); |
| child.layout(childLeft, childTop, childRight, childBottom); |
| |
| for (int i = nextCol; i < nextCol + span; i++) { |
| mItemTops[i] = childTop - rec.getMarginAbove(i - nextCol) - itemMargin; |
| } |
| |
| nextCol = getNextColumnUp(); |
| mFirstPosition = position--; |
| } |
| |
| int highestView = getHeight(); |
| for (int i = 0; i < mColCount; i++) { |
| if (mItemTops[i] < highestView) { |
| highestView = mItemTops[i]; |
| } |
| } |
| return gridTop - highestView; |
| } |
| |
| /** |
| * Should be called with mPopulating set to true |
| * |
| * @param fromPosition Position to start filling from |
| * @param overhang the number of extra pixels to fill beyond the current bottom edge |
| * @return the max overhang beyond the end of the view of any added items at the bottom |
| */ |
| final int fillDown(int fromPosition, int overhang) { |
| final int paddingLeft = getPaddingLeft(); |
| final int paddingRight = getPaddingRight(); |
| final int itemMargin = mItemMargin; |
| final int colWidth = |
| (getWidth() - paddingLeft - paddingRight - itemMargin * (mColCount - 1)) / mColCount; |
| final int gridBottom = getHeight() - getPaddingBottom(); |
| final int fillTo = gridBottom + overhang; |
| int nextCol = getNextColumnDown(); |
| int position = fromPosition; |
| |
| while (nextCol >= 0 && mItemBottoms[nextCol] < fillTo && position < mItemCount) { |
| final View child = obtainView(position, null); |
| LayoutParams lp = (LayoutParams) child.getLayoutParams(); |
| |
| if (child.getParent() != this) { |
| if (mInLayout) { |
| addViewInLayout(child, -1, lp); |
| } else { |
| addView(child); |
| } |
| } |
| |
| final int span = Math.min(mColCount, lp.span); |
| final int widthSize = colWidth * span + itemMargin * (span - 1); |
| final int widthSpec = MeasureSpec.makeMeasureSpec(widthSize, MeasureSpec.EXACTLY); |
| |
| LayoutRecord rec; |
| if (span > 1) { |
| rec = getNextRecordDown(position, span); |
| nextCol = rec.column; |
| } else { |
| rec = mLayoutRecords.get(position); |
| } |
| |
| boolean invalidateAfter = false; |
| if (rec == null) { |
| rec = new LayoutRecord(); |
| mLayoutRecords.put(position, rec); |
| rec.column = nextCol; |
| rec.span = span; |
| } else if (span != rec.span) { |
| rec.span = span; |
| rec.column = nextCol; |
| invalidateAfter = true; |
| } else { |
| nextCol = rec.column; |
| } |
| |
| if (mHasStableIds) { |
| final long id = mAdapter.getItemId(position); |
| rec.id = id; |
| lp.id = id; |
| } |
| |
| lp.column = nextCol; |
| |
| final int heightSpec; |
| if (lp.height == LayoutParams.WRAP_CONTENT) { |
| heightSpec = MeasureSpec.makeMeasureSpec(0, MeasureSpec.UNSPECIFIED); |
| } else { |
| heightSpec = MeasureSpec.makeMeasureSpec(lp.height, MeasureSpec.EXACTLY); |
| } |
| child.measure(widthSpec, heightSpec); |
| |
| final int childHeight = child.getMeasuredHeight(); |
| if (invalidateAfter || (childHeight != rec.height && rec.height > 0)) { |
| invalidateLayoutRecordsAfterPosition(position); |
| } |
| rec.height = childHeight; |
| |
| final int startFrom; |
| if (span > 1) { |
| int lowest = mItemBottoms[nextCol]; |
| for (int i = nextCol + 1; i < nextCol + span; i++) { |
| final int bottom = mItemBottoms[i]; |
| if (bottom > lowest) { |
| lowest = bottom; |
| } |
| } |
| startFrom = lowest; |
| } else { |
| startFrom = mItemBottoms[nextCol]; |
| } |
| final int childTop = startFrom + itemMargin; |
| final int childBottom = childTop + childHeight; |
| final int childLeft = paddingLeft + nextCol * (colWidth + itemMargin); |
| final int childRight = childLeft + child.getMeasuredWidth(); |
| child.layout(childLeft, childTop, childRight, childBottom); |
| |
| for (int i = nextCol; i < nextCol + span; i++) { |
| mItemBottoms[i] = childBottom + rec.getMarginBelow(i - nextCol); |
| } |
| |
| nextCol = getNextColumnDown(); |
| position++; |
| } |
| |
| int lowestView = 0; |
| for (int i = 0; i < mColCount; i++) { |
| if (mItemBottoms[i] > lowestView) { |
| lowestView = mItemBottoms[i]; |
| } |
| } |
| return lowestView - gridBottom; |
| } |
| |
| /** |
| * @return column that the next view filling upwards should occupy. This is the bottom-most |
| * position available for a single-column item. |
| */ |
| final int getNextColumnUp() { |
| int result = -1; |
| int bottomMost = Integer.MIN_VALUE; |
| |
| final int colCount = mColCount; |
| for (int i = colCount - 1; i >= 0; i--) { |
| final int top = mItemTops[i]; |
| if (top > bottomMost) { |
| bottomMost = top; |
| result = i; |
| } |
| } |
| return result; |
| } |
| |
| /** |
| * Return a LayoutRecord for the given position |
| * @param position |
| * @param span |
| * @return |
| */ |
| final LayoutRecord getNextRecordUp(int position, int span) { |
| LayoutRecord rec = mLayoutRecords.get(position); |
| if (rec == null) { |
| rec = new LayoutRecord(); |
| rec.span = span; |
| mLayoutRecords.put(position, rec); |
| } else if (rec.span != span) { |
| throw new IllegalStateException("Invalid LayoutRecord! Record had span=" + rec.span + |
| " but caller requested span=" + span + " for position=" + position); |
| } |
| int targetCol = -1; |
| int bottomMost = Integer.MIN_VALUE; |
| |
| final int colCount = mColCount; |
| for (int i = colCount - span; i >= 0; i--) { |
| int top = Integer.MAX_VALUE; |
| for (int j = i; j < i + span; j++) { |
| final int singleTop = mItemTops[j]; |
| if (singleTop < top) { |
| top = singleTop; |
| } |
| } |
| if (top > bottomMost) { |
| bottomMost = top; |
| targetCol = i; |
| } |
| } |
| |
| rec.column = targetCol; |
| |
| for (int i = 0; i < span; i++) { |
| rec.setMarginBelow(i, mItemTops[i + targetCol] - bottomMost); |
| } |
| |
| return rec; |
| } |
| |
| /** |
| * @return column that the next view filling downwards should occupy. This is the top-most |
| * position available. |
| */ |
| final int getNextColumnDown() { |
| int result = -1; |
| int topMost = Integer.MAX_VALUE; |
| |
| final int colCount = mColCount; |
| for (int i = 0; i < colCount; i++) { |
| final int bottom = mItemBottoms[i]; |
| if (bottom < topMost) { |
| topMost = bottom; |
| result = i; |
| } |
| } |
| return result; |
| } |
| |
| final LayoutRecord getNextRecordDown(int position, int span) { |
| LayoutRecord rec = mLayoutRecords.get(position); |
| if (rec == null) { |
| rec = new LayoutRecord(); |
| rec.span = span; |
| mLayoutRecords.put(position, rec); |
| } else if (rec.span != span) { |
| throw new IllegalStateException("Invalid LayoutRecord! Record had span=" + rec.span + |
| " but caller requested span=" + span + " for position=" + position); |
| } |
| int targetCol = -1; |
| int topMost = Integer.MAX_VALUE; |
| |
| final int colCount = mColCount; |
| for (int i = 0; i <= colCount - span; i++) { |
| int bottom = Integer.MIN_VALUE; |
| for (int j = i; j < i + span; j++) { |
| final int singleBottom = mItemBottoms[j]; |
| if (singleBottom > bottom) { |
| bottom = singleBottom; |
| } |
| } |
| if (bottom < topMost) { |
| topMost = bottom; |
| targetCol = i; |
| } |
| } |
| |
| rec.column = targetCol; |
| |
| for (int i = 0; i < span; i++) { |
| rec.setMarginAbove(i, topMost - mItemBottoms[i + targetCol]); |
| } |
| |
| return rec; |
| } |
| |
| /** |
| * Obtain a populated view from the adapter. If optScrap is non-null and is not |
| * reused it will be placed in the recycle bin. |
| * |
| * @param position position to get view for |
| * @param optScrap Optional scrap view; will be reused if possible |
| * @return A new view, a recycled view from mRecycler, or optScrap |
| */ |
| final View obtainView(int position, View optScrap) { |
| View view = mRecycler.getTransientStateView(position); |
| if (view != null) { |
| return view; |
| } |
| |
| // Reuse optScrap if it's of the right type (and not null) |
| final int optType = optScrap != null ? |
| ((LayoutParams) optScrap.getLayoutParams()).viewType : -1; |
| final int positionViewType = mAdapter.getItemViewType(position); |
| final View scrap = optType == positionViewType ? |
| optScrap : mRecycler.getScrapView(positionViewType); |
| |
| view = mAdapter.getView(position, scrap, this); |
| |
| if (view != scrap && scrap != null) { |
| // The adapter didn't use it; put it back. |
| mRecycler.addScrap(scrap); |
| } |
| |
| ViewGroup.LayoutParams lp = view.getLayoutParams(); |
| |
| if (view.getParent() != this) { |
| if (lp == null) { |
| lp = generateDefaultLayoutParams(); |
| } else if (!checkLayoutParams(lp)) { |
| lp = generateLayoutParams(lp); |
| } |
| } |
| |
| final LayoutParams sglp = (LayoutParams) lp; |
| sglp.position = position; |
| sglp.viewType = positionViewType; |
| |
| return view; |
| } |
| |
| public ListAdapter getAdapter() { |
| return mAdapter; |
| } |
| |
| public void setAdapter(ListAdapter adapter) { |
| if (mAdapter != null) { |
| mAdapter.unregisterDataSetObserver(mObserver); |
| } |
| // TODO: If the new adapter says that there are stable IDs, remove certain layout records |
| // and onscreen views if they have changed instead of removing all of the state here. |
| clearAllState(); |
| mAdapter = adapter; |
| mDataChanged = true; |
| mOldItemCount = mItemCount = adapter != null ? adapter.getCount() : 0; |
| if (adapter != null) { |
| adapter.registerDataSetObserver(mObserver); |
| mRecycler.setViewTypeCount(adapter.getViewTypeCount()); |
| mHasStableIds = adapter.hasStableIds(); |
| } else { |
| mHasStableIds = false; |
| } |
| populate(); |
| } |
| |
| /** |
| * Clear all state because the grid will be used for a completely different set of data. |
| */ |
| private void clearAllState() { |
| // Clear all layout records and views |
| mLayoutRecords.clear(); |
| removeAllViews(); |
| |
| // Reset to the top of the grid |
| resetStateForGridTop(); |
| |
| // Clear recycler because there could be different view types now |
| mRecycler.clear(); |
| } |
| |
| /** |
| * Reset all internal state to be at the top of the grid. |
| */ |
| private void resetStateForGridTop() { |
| // Reset mItemTops and mItemBottoms |
| final int colCount = mColCount; |
| if (mItemTops == null || mItemTops.length != colCount) { |
| mItemTops = new int[colCount]; |
| mItemBottoms = new int[colCount]; |
| } |
| final int top = getPaddingTop(); |
| Arrays.fill(mItemTops, top); |
| Arrays.fill(mItemBottoms, top); |
| |
| // Reset the first visible position in the grid to be item 0 |
| mFirstPosition = 0; |
| mRestoreOffset = 0; |
| } |
| |
| /** |
| * Scroll the list so the first visible position in the grid is the first item in the adapter. |
| */ |
| public void setSelectionToTop() { |
| // Clear out the views (but don't clear out the layout records or recycler because the data |
| // has not changed) |
| removeAllViews(); |
| |
| // Reset to top of grid |
| resetStateForGridTop(); |
| |
| // Start populating again |
| populate(); |
| } |
| |
| @Override |
| protected LayoutParams generateDefaultLayoutParams() { |
| return new LayoutParams(LayoutParams.WRAP_CONTENT); |
| } |
| |
| @Override |
| protected LayoutParams generateLayoutParams(ViewGroup.LayoutParams lp) { |
| return new LayoutParams(lp); |
| } |
| |
| @Override |
| protected boolean checkLayoutParams(ViewGroup.LayoutParams lp) { |
| return lp instanceof LayoutParams; |
| } |
| |
| @Override |
| public ViewGroup.LayoutParams generateLayoutParams(AttributeSet attrs) { |
| return new LayoutParams(getContext(), attrs); |
| } |
| |
| @Override |
| public Parcelable onSaveInstanceState() { |
| final Parcelable superState = super.onSaveInstanceState(); |
| final SavedState ss = new SavedState(superState); |
| final int position = mFirstPosition; |
| ss.position = position; |
| if (position >= 0 && mAdapter != null && position < mAdapter.getCount()) { |
| ss.firstId = mAdapter.getItemId(position); |
| } |
| if (getChildCount() > 0) { |
| ss.topOffset = getChildAt(0).getTop() - mItemMargin - getPaddingTop(); |
| } |
| return ss; |
| } |
| |
| @Override |
| public void onRestoreInstanceState(Parcelable state) { |
| SavedState ss = (SavedState) state; |
| super.onRestoreInstanceState(ss.getSuperState()); |
| mDataChanged = true; |
| mFirstPosition = ss.position; |
| mRestoreOffset = ss.topOffset; |
| requestLayout(); |
| } |
| |
| public static class LayoutParams extends ViewGroup.LayoutParams { |
| private static final int[] LAYOUT_ATTRS = new int[] { |
| android.R.attr.layout_span |
| }; |
| |
| private static final int SPAN_INDEX = 0; |
| |
| /** |
| * The number of columns this item should span |
| */ |
| public int span = 1; |
| |
| /** |
| * Item position this view represents |
| */ |
| int position; |
| |
| /** |
| * Type of this view as reported by the adapter |
| */ |
| int viewType; |
| |
| /** |
| * The column this view is occupying |
| */ |
| int column; |
| |
| /** |
| * The stable ID of the item this view displays |
| */ |
| long id = -1; |
| |
| public LayoutParams(int height) { |
| super(FILL_PARENT, height); |
| |
| if (this.height == FILL_PARENT) { |
| Log.w(TAG, "Constructing LayoutParams with height FILL_PARENT - " + |
| "impossible! Falling back to WRAP_CONTENT"); |
| this.height = WRAP_CONTENT; |
| } |
| } |
| |
| public LayoutParams(Context c, AttributeSet attrs) { |
| super(c, attrs); |
| |
| if (this.width != FILL_PARENT) { |
| Log.w(TAG, "Inflation setting LayoutParams width to " + this.width + |
| " - must be MATCH_PARENT"); |
| this.width = FILL_PARENT; |
| } |
| if (this.height == FILL_PARENT) { |
| Log.w(TAG, "Inflation setting LayoutParams height to MATCH_PARENT - " + |
| "impossible! Falling back to WRAP_CONTENT"); |
| this.height = WRAP_CONTENT; |
| } |
| |
| TypedArray a = c.obtainStyledAttributes(attrs, LAYOUT_ATTRS); |
| span = a.getInteger(SPAN_INDEX, 1); |
| a.recycle(); |
| } |
| |
| public LayoutParams(ViewGroup.LayoutParams other) { |
| super(other); |
| |
| if (this.width != FILL_PARENT) { |
| Log.w(TAG, "Constructing LayoutParams with width " + this.width + |
| " - must be MATCH_PARENT"); |
| this.width = FILL_PARENT; |
| } |
| if (this.height == FILL_PARENT) { |
| Log.w(TAG, "Constructing LayoutParams with height MATCH_PARENT - " + |
| "impossible! Falling back to WRAP_CONTENT"); |
| this.height = WRAP_CONTENT; |
| } |
| } |
| } |
| |
| private class RecycleBin { |
| private ArrayList<View>[] mScrapViews; |
| private int mViewTypeCount; |
| private int mMaxScrap; |
| |
| private SparseArray<View> mTransientStateViews; |
| |
| public void setViewTypeCount(int viewTypeCount) { |
| if (viewTypeCount < 1) { |
| throw new IllegalArgumentException("Must have at least one view type (" + |
| viewTypeCount + " types reported)"); |
| } |
| if (viewTypeCount == mViewTypeCount) { |
| return; |
| } |
| |
| ArrayList<View>[] scrapViews = new ArrayList[viewTypeCount]; |
| for (int i = 0; i < viewTypeCount; i++) { |
| scrapViews[i] = new ArrayList<View>(); |
| } |
| mViewTypeCount = viewTypeCount; |
| mScrapViews = scrapViews; |
| } |
| |
| public void clear() { |
| final int typeCount = mViewTypeCount; |
| for (int i = 0; i < typeCount; i++) { |
| mScrapViews[i].clear(); |
| } |
| if (mTransientStateViews != null) { |
| mTransientStateViews.clear(); |
| } |
| } |
| |
| public void clearTransientViews() { |
| if (mTransientStateViews != null) { |
| mTransientStateViews.clear(); |
| } |
| } |
| |
| public void addScrap(View v) { |
| final LayoutParams lp = (LayoutParams) v.getLayoutParams(); |
| if (ViewCompat.hasTransientState(v)) { |
| if (mTransientStateViews == null) { |
| mTransientStateViews = new SparseArray<View>(); |
| } |
| mTransientStateViews.put(lp.position, v); |
| return; |
| } |
| |
| final int childCount = getChildCount(); |
| if (childCount > mMaxScrap) { |
| mMaxScrap = childCount; |
| } |
| |
| ArrayList<View> scrap = mScrapViews[lp.viewType]; |
| if (scrap.size() < mMaxScrap) { |
| scrap.add(v); |
| } |
| } |
| |
| public View getTransientStateView(int position) { |
| if (mTransientStateViews == null) { |
| return null; |
| } |
| |
| final View result = mTransientStateViews.get(position); |
| if (result != null) { |
| mTransientStateViews.remove(position); |
| } |
| return result; |
| } |
| |
| public View getScrapView(int type) { |
| ArrayList<View> scrap = mScrapViews[type]; |
| if (scrap.isEmpty()) { |
| return null; |
| } |
| |
| final int index = scrap.size() - 1; |
| final View result = scrap.get(index); |
| scrap.remove(index); |
| return result; |
| } |
| } |
| |
| private class AdapterDataSetObserver extends DataSetObserver { |
| @Override |
| public void onChanged() { |
| mDataChanged = true; |
| mOldItemCount = mItemCount; |
| mItemCount = mAdapter.getCount(); |
| |
| // TODO: Consider matching these back up if we have stable IDs. |
| mRecycler.clearTransientViews(); |
| |
| if (!mHasStableIds) { |
| // Clear all layout records and recycle the views |
| mLayoutRecords.clear(); |
| recycleAllViews(); |
| |
| // Reset item bottoms to be equal to item tops |
| final int colCount = mColCount; |
| for (int i = 0; i < colCount; i++) { |
| mItemBottoms[i] = mItemTops[i]; |
| } |
| } |
| |
| // TODO: consider repopulating in a deferred runnable instead |
| // (so that successive changes may still be batched) |
| requestLayout(); |
| } |
| |
| @Override |
| public void onInvalidated() { |
| } |
| } |
| |
| static class SavedState extends BaseSavedState { |
| long firstId = -1; |
| int position; |
| int topOffset; |
| |
| SavedState(Parcelable superState) { |
| super(superState); |
| } |
| |
| private SavedState(Parcel in) { |
| super(in); |
| firstId = in.readLong(); |
| position = in.readInt(); |
| topOffset = in.readInt(); |
| } |
| |
| @Override |
| public void writeToParcel(Parcel out, int flags) { |
| super.writeToParcel(out, flags); |
| out.writeLong(firstId); |
| out.writeInt(position); |
| out.writeInt(topOffset); |
| } |
| |
| @Override |
| public String toString() { |
| return "StaggereGridView.SavedState{" |
| + Integer.toHexString(System.identityHashCode(this)) |
| + " firstId=" + firstId |
| + " position=" + position + "}"; |
| } |
| |
| public static final Parcelable.Creator<SavedState> CREATOR |
| = new Parcelable.Creator<SavedState>() { |
| public SavedState createFromParcel(Parcel in) { |
| return new SavedState(in); |
| } |
| |
| public SavedState[] newArray(int size) { |
| return new SavedState[size]; |
| } |
| }; |
| } |
| } |