com.android.photos.views.GalleryThumbnailView.java Source code

Java tutorial

Introduction

Here is the source code for com.android.photos.views.GalleryThumbnailView.java

Source

/*
 * Copyright (C) 2013 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.photos.views;

import android.content.Context;
import android.content.res.TypedArray;
import android.database.DataSetObserver;
import android.graphics.Canvas;
import android.support.v4.view.MotionEventCompat;
import android.support.v4.view.VelocityTrackerCompat;
import android.support.v4.view.ViewCompat;
import android.support.v4.widget.EdgeEffectCompat;
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 android.widget.OverScroller;

import java.util.ArrayList;

public class GalleryThumbnailView extends ViewGroup {

    public interface GalleryThumbnailAdapter extends ListAdapter {
        /**
         * @param position Position to get the intrinsic aspect ratio for
         * @return width / height
         */
        float getIntrinsicAspectRatio(int position);
    }

    private static final String TAG = "GalleryThumbnailView";
    private static final float ASPECT_RATIO = (float) Math.sqrt(1.5f);
    private static final int LAND_UNITS = 2;
    private static final int PORT_UNITS = 3;

    private GalleryThumbnailAdapter mAdapter;

    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 boolean mPopulating;
    private boolean mInLayout;

    private int mTouchSlop;
    private int mMaximumVelocity;
    private int mFlingVelocity;
    private float mLastTouchX;
    private float mTouchRemainderX;
    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 OverScroller mScroller;

    private final EdgeEffectCompat mLeftEdge;
    private final EdgeEffectCompat mRightEdge;

    private int mLargeColumnWidth;
    private int mSmallColumnWidth;
    private int mLargeColumnUnitCount = 8;
    private int mSmallColumnUnitCount = 10;

    public GalleryThumbnailView(Context context) {
        this(context, null);
    }

    public GalleryThumbnailView(Context context, AttributeSet attrs) {
        this(context, attrs, 0);
    }

    public GalleryThumbnailView(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 = new OverScroller(context);

        mLeftEdge = new EdgeEffectCompat(context);
        mRightEdge = new EdgeEffectCompat(context);
        setWillNotDraw(false);
        setClipToPadding(false);
    }

    @Override
    public void requestLayout() {
        if (!mPopulating) {
            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);
        }
        if (heightMode != MeasureSpec.EXACTLY) {
            Log.e(TAG, "onMeasure: must have an exact height or match_parent! " + "Using fallback spec of EXACTLY "
                    + heightSize);
        }

        setMeasuredDimension(widthSize, heightSize);

        float portSpaces = mLargeColumnUnitCount / PORT_UNITS;
        float height = getMeasuredHeight() / portSpaces;
        mLargeColumnWidth = (int) (height / ASPECT_RATIO);
        portSpaces++;
        height = getMeasuredHeight() / portSpaces;
        mSmallColumnWidth = (int) (height / ASPECT_RATIO);
    }

    @Override
    protected void onLayout(boolean changed, int l, int t, int r, int b) {
        mInLayout = true;
        populate();
        mInLayout = false;

        final int width = r - l;
        final int height = b - t;
        mLeftEdge.setSize(width, height);
        mRightEdge.setSize(width, height);
    }

    private void populate() {
        if (getWidth() == 0 || getHeight() == 0) {
            return;
        }

        // TODO: Handle size changing
        //        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);
        fillRight(mFirstPosition + getChildCount(), 0);
        fillLeft(mFirstPosition - 1, 0);
        mPopulating = false;
        mDataChanged = false;
    }

    final void layoutChildren(boolean queryAdapter) {
        // TODO
        //        final int childCount = getChildCount();
        //        for (int i = 0; i < childCount; i++) {
        //            View child = getChildAt(i);
        //
        //            if (child.isLayoutRequested()) {
        //                final int widthSpec = MeasureSpec.makeMeasureSpec(child.getMeasuredWidth(), MeasureSpec.EXACTLY);
        //                final int heightSpec = MeasureSpec.makeMeasureSpec(child.getMeasuredHeight(), MeasureSpec.EXACTLY);
        //                child.measure(widthSpec, heightSpec);
        //                child.layout(child.getLeft(), child.getTop(), child.getRight(), child.getBottom());
        //            }
        //
        //            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);
        //        }
    }

    /**
     * Obtain the view and add it to our list of children. The view can be made
     * fresh, converted from an unused view, or used as is if it was in the
     * recycle bin.
     *
     * @param startPosition Logical position in the list to start from
     * @param x Left or right edge of the view to add
     * @param forward If true, align left edge to x and increase position.
     *                If false, align right edge to x and decrease position.
     * @return Number of views added
     */
    private int makeAndAddColumn(int startPosition, int x, boolean forward) {
        int columnWidth = mLargeColumnWidth;
        int addViews = 0;
        for (int remaining = mLargeColumnUnitCount, i = 0; remaining > 0 && startPosition + i >= 0
                && startPosition + i < mItemCount; i += forward ? 1 : -1, addViews++) {
            if (mAdapter.getIntrinsicAspectRatio(startPosition + i) >= 1f) {
                // landscape
                remaining -= LAND_UNITS;
            } else {
                // portrait
                remaining -= PORT_UNITS;
                if (remaining < 0) {
                    remaining += (mSmallColumnUnitCount - mLargeColumnUnitCount);
                    columnWidth = mSmallColumnWidth;
                }
            }
        }
        int nextTop = 0;
        for (int i = 0; i < addViews; i++) {
            int position = startPosition + (forward ? i : -i);
            View child = obtainView(position, null);
            if (child.getParent() != this) {
                if (mInLayout) {
                    addViewInLayout(child, forward ? -1 : 0, child.getLayoutParams());
                } else {
                    addView(child, forward ? -1 : 0);
                }
            }
            int heightSize = (int) (.5f
                    + (mAdapter.getIntrinsicAspectRatio(position) >= 1f ? columnWidth / ASPECT_RATIO
                            : columnWidth * ASPECT_RATIO));
            int heightSpec = MeasureSpec.makeMeasureSpec(heightSize, MeasureSpec.EXACTLY);
            int widthSpec = MeasureSpec.makeMeasureSpec(columnWidth, MeasureSpec.EXACTLY);
            child.measure(widthSpec, heightSpec);
            int childLeft = forward ? x : x - columnWidth;
            child.layout(childLeft, nextTop, childLeft + columnWidth, nextTop + heightSize);
            nextTop += heightSize;
        }
        return addViews;
    }

    @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();
            mLastTouchX = ev.getX();
            mActivePointerId = MotionEventCompat.getPointerId(ev, 0);
            mTouchRemainderX = 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 x = MotionEventCompat.getX(ev, index);
            final float dx = x - mLastTouchX + mTouchRemainderX;
            final int deltaY = (int) dx;
            mTouchRemainderX = dx - deltaY;

            if (Math.abs(dx) > 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();
            mLastTouchX = ev.getX();
            mActivePointerId = MotionEventCompat.getPointerId(ev, 0);
            mTouchRemainderX = 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 x = MotionEventCompat.getX(ev, index);
            final float dx = x - mLastTouchX + mTouchRemainderX;
            final int deltaX = (int) dx;
            mTouchRemainderX = dx - deltaX;

            if (Math.abs(dx) > mTouchSlop) {
                mTouchMode = TOUCH_MODE_DRAGGING;
            }

            if (mTouchMode == TOUCH_MODE_DRAGGING) {
                mLastTouchX = x;

                if (!trackMotionScroll(deltaX, 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.getXVelocity(mVelocityTracker, mActivePointerId);
            if (Math.abs(velocity) > mFlingVelocity) { // TODO
                mTouchMode = TOUCH_MODE_FLINGING;
                mScroller.fling(0, 0, (int) velocity, 0, Integer.MIN_VALUE, Integer.MAX_VALUE, 0, 0);
                mLastTouchX = 0;
                ViewCompat.postInvalidateOnAnimation(this);
            } else {
                mTouchMode = TOUCH_MODE_IDLE;
            }

        }
            break;
        }
        return true;
    }

    /**
     *
     * @param deltaX Pixels that content should move by
     * @return true if the movement completed, false if it was stopped prematurely.
     */
    private boolean trackMotionScroll(int deltaX, boolean allowOverScroll) {
        final boolean contentFits = contentFits();
        final int allowOverhang = Math.abs(deltaX);

        final int overScrolledBy;
        final int movedBy;
        if (!contentFits) {
            final int overhang;
            final boolean up;
            mPopulating = true;
            if (deltaX > 0) {
                overhang = fillLeft(mFirstPosition - 1, allowOverhang);
                up = true;
            } else {
                overhang = fillRight(mFirstPosition + getChildCount(), allowOverhang);
                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 = deltaX > 0 ? mLeftEdge : mRightEdge;
                    edge.onPull((float) Math.abs(deltaX) / getWidth());
                    ViewCompat.postInvalidateOnAnimation(this);
                }
            }
        }

        return deltaX == 0 || movedBy != 0;
    }

    /**
     * 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 = 0;
        final int clearBelow = height;
        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 void offsetChildren(int offset) {
        final int childCount = getChildCount();
        for (int i = 0; i < childCount; i++) {
            final View child = getChildAt(i);
            child.layout(child.getLeft() + offset, child.getTop(), child.getRight() + offset, child.getBottom());
        }
    }

    private boolean contentFits() {
        final int childCount = getChildCount();
        if (childCount == 0)
            return true;
        if (childCount != mItemCount)
            return false;

        return getChildAt(0).getLeft() >= getPaddingLeft()
                && getChildAt(childCount - 1).getRight() <= getWidth() - getPaddingRight();
    }

    private void recycleAllViews() {
        for (int i = 0; i < getChildCount(); i++) {
            mRecycler.addScrap(getChildAt(i));
        }

        if (mInLayout) {
            removeAllViewsInLayout();
        } else {
            removeAllViews();
        }
    }

    private int fillRight(int pos, int overhang) {
        int end = (getRight() - getLeft()) + overhang;

        int nextLeft = getChildCount() == 0 ? 0 : getChildAt(getChildCount() - 1).getRight();
        while (nextLeft < end && pos < mItemCount) {
            pos += makeAndAddColumn(pos, nextLeft, true);
            nextLeft = getChildAt(getChildCount() - 1).getRight();
        }
        final int gridRight = getWidth() - getPaddingRight();
        return getChildAt(getChildCount() - 1).getRight() - gridRight;
    }

    private int fillLeft(int pos, int overhang) {
        int end = getPaddingLeft() - overhang;

        int nextRight = getChildAt(0).getLeft();
        while (nextRight > end && pos >= 0) {
            pos -= makeAndAddColumn(pos, nextRight, false);
            nextRight = getChildAt(0).getLeft();
        }

        mFirstPosition = pos + 1;
        return getPaddingLeft() - getChildAt(0).getLeft();
    }

    @Override
    public void computeScroll() {
        if (mScroller.computeScrollOffset()) {
            final int x = mScroller.getCurrX();
            final int dx = (int) (x - mLastTouchX);
            mLastTouchX = x;
            final boolean stopped = !trackMotionScroll(dx, 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 (dx > 0) {
                            edge = mLeftEdge;
                        } else {
                            edge = mRightEdge;
                        }
                        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 (!mLeftEdge.isFinished()) {
            final int restoreCount = canvas.save();
            final int height = getHeight() - getPaddingTop() - getPaddingBottom();

            canvas.rotate(270);
            canvas.translate(-height + getPaddingTop(), 0);
            mLeftEdge.setSize(height, getWidth());
            if (mLeftEdge.draw(canvas)) {
                postInvalidateOnAnimation();
            }
            canvas.restoreToCount(restoreCount);
        }
        if (!mRightEdge.isFinished()) {
            final int restoreCount = canvas.save();
            final int width = getWidth();
            final int height = getHeight() - getPaddingTop() - getPaddingBottom();

            canvas.rotate(90);
            canvas.translate(-getPaddingTop(), width);
            mRightEdge.setSize(height, width);
            if (mRightEdge.draw(canvas)) {
                postInvalidateOnAnimation();
            }
            canvas.restoreToCount(restoreCount);
        }
    }

    /**
     * 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
     */
    private 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);
            }
            view.setLayoutParams(lp);
        }

        final LayoutParams sglp = (LayoutParams) lp;
        sglp.position = position;
        sglp.viewType = positionViewType;

        return view;
    }

    public GalleryThumbnailAdapter getAdapter() {
        return mAdapter;
    }

    public void setAdapter(GalleryThumbnailAdapter 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
        removeAllViews();

        // Reset to the top of the grid
        mFirstPosition = 0;

        // Clear recycler because there could be different view types now
        mRecycler.clear();
    }

    @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);
    }

    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(MATCH_PARENT, height);

            if (this.height == MATCH_PARENT) {
                Log.w(TAG, "Constructing LayoutParams with height MATCH_PARENT - "
                        + "impossible! Falling back to WRAP_CONTENT");
                this.height = WRAP_CONTENT;
            }
        }

        public LayoutParams(Context c, AttributeSet attrs) {
            super(c, attrs);

            if (this.width != MATCH_PARENT) {
                Log.w(TAG, "Inflation setting LayoutParams width to " + this.width + " - must be MATCH_PARENT");
                this.width = MATCH_PARENT;
            }
            if (this.height == MATCH_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 != MATCH_PARENT) {
                Log.w(TAG, "Constructing LayoutParams with width " + this.width + " - must be MATCH_PARENT");
                this.width = MATCH_PARENT;
            }
            if (this.height == MATCH_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) {
                recycleAllViews();
            }

            // TODO: consider repopulating in a deferred runnable instead
            // (so that successive changes may still be batched)
            requestLayout();
        }

        @Override
        public void onInvalidated() {
        }
    }
}