1/*
2 * Copyright (C) 2013 The Android Open Source Project
3 *
4 * Licensed under the Apache License, Version 2.0 (the "License");
5 * you may not use this file except in compliance with the License.
6 * You may obtain a copy of the License at
7 *
8 *      http://www.apache.org/licenses/LICENSE-2.0
9 *
10 * Unless required by applicable law or agreed to in writing, software
11 * distributed under the License is distributed on an "AS IS" BASIS,
12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 * See the License for the specific language governing permissions and
14 * limitations under the License.
15 */
16
17package com.android.photos.views;
18
19import android.content.Context;
20import android.content.res.TypedArray;
21import android.database.DataSetObserver;
22import android.graphics.Canvas;
23import android.support.v4.view.MotionEventCompat;
24import android.support.v4.view.VelocityTrackerCompat;
25import android.support.v4.view.ViewCompat;
26import android.support.v4.widget.EdgeEffectCompat;
27import android.util.AttributeSet;
28import android.util.Log;
29import android.util.SparseArray;
30import android.view.MotionEvent;
31import android.view.VelocityTracker;
32import android.view.View;
33import android.view.ViewConfiguration;
34import android.view.ViewGroup;
35import android.widget.ListAdapter;
36import android.widget.OverScroller;
37
38import java.util.ArrayList;
39
40public class GalleryThumbnailView extends ViewGroup {
41
42    public interface GalleryThumbnailAdapter extends ListAdapter {
43        /**
44         * @param position Position to get the intrinsic aspect ratio for
45         * @return width / height
46         */
47        float getIntrinsicAspectRatio(int position);
48    }
49
50    private static final String TAG = "GalleryThumbnailView";
51    private static final float ASPECT_RATIO = (float) Math.sqrt(1.5f);
52    private static final int LAND_UNITS = 2;
53    private static final int PORT_UNITS = 3;
54
55    private GalleryThumbnailAdapter mAdapter;
56
57    private final RecycleBin mRecycler = new RecycleBin();
58
59    private final AdapterDataSetObserver mObserver = new AdapterDataSetObserver();
60
61    private boolean mDataChanged;
62    private int mOldItemCount;
63    private int mItemCount;
64    private boolean mHasStableIds;
65
66    private int mFirstPosition;
67
68    private boolean mPopulating;
69    private boolean mInLayout;
70
71    private int mTouchSlop;
72    private int mMaximumVelocity;
73    private int mFlingVelocity;
74    private float mLastTouchX;
75    private float mTouchRemainderX;
76    private int mActivePointerId;
77
78    private static final int TOUCH_MODE_IDLE = 0;
79    private static final int TOUCH_MODE_DRAGGING = 1;
80    private static final int TOUCH_MODE_FLINGING = 2;
81
82    private int mTouchMode;
83    private final VelocityTracker mVelocityTracker = VelocityTracker.obtain();
84    private final OverScroller mScroller;
85
86    private final EdgeEffectCompat mLeftEdge;
87    private final EdgeEffectCompat mRightEdge;
88
89    private int mLargeColumnWidth;
90    private int mSmallColumnWidth;
91    private int mLargeColumnUnitCount = 8;
92    private int mSmallColumnUnitCount = 10;
93
94    public GalleryThumbnailView(Context context) {
95        this(context, null);
96    }
97
98    public GalleryThumbnailView(Context context, AttributeSet attrs) {
99        this(context, attrs, 0);
100    }
101
102    public GalleryThumbnailView(Context context, AttributeSet attrs, int defStyle) {
103        super(context, attrs, defStyle);
104
105        final ViewConfiguration vc = ViewConfiguration.get(context);
106        mTouchSlop = vc.getScaledTouchSlop();
107        mMaximumVelocity = vc.getScaledMaximumFlingVelocity();
108        mFlingVelocity = vc.getScaledMinimumFlingVelocity();
109        mScroller = new OverScroller(context);
110
111        mLeftEdge = new EdgeEffectCompat(context);
112        mRightEdge = new EdgeEffectCompat(context);
113        setWillNotDraw(false);
114        setClipToPadding(false);
115    }
116
117    @Override
118    public void requestLayout() {
119        if (!mPopulating) {
120            super.requestLayout();
121        }
122    }
123
124    @Override
125    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
126        int widthMode = MeasureSpec.getMode(widthMeasureSpec);
127        int heightMode = MeasureSpec.getMode(heightMeasureSpec);
128        int widthSize = MeasureSpec.getSize(widthMeasureSpec);
129        int heightSize = MeasureSpec.getSize(heightMeasureSpec);
130
131        if (widthMode != MeasureSpec.EXACTLY) {
132            Log.e(TAG, "onMeasure: must have an exact width or match_parent! " +
133                    "Using fallback spec of EXACTLY " + widthSize);
134        }
135        if (heightMode != MeasureSpec.EXACTLY) {
136            Log.e(TAG, "onMeasure: must have an exact height or match_parent! " +
137                    "Using fallback spec of EXACTLY " + heightSize);
138        }
139
140        setMeasuredDimension(widthSize, heightSize);
141
142        float portSpaces = mLargeColumnUnitCount / PORT_UNITS;
143        float height = getMeasuredHeight() / portSpaces;
144        mLargeColumnWidth = (int) (height / ASPECT_RATIO);
145        portSpaces++;
146        height = getMeasuredHeight() / portSpaces;
147        mSmallColumnWidth = (int) (height / ASPECT_RATIO);
148    }
149
150    @Override
151    protected void onLayout(boolean changed, int l, int t, int r, int b) {
152        mInLayout = true;
153        populate();
154        mInLayout = false;
155
156        final int width = r - l;
157        final int height = b - t;
158        mLeftEdge.setSize(width, height);
159        mRightEdge.setSize(width, height);
160    }
161
162    private void populate() {
163        if (getWidth() == 0 || getHeight() == 0) {
164            return;
165        }
166
167        // TODO: Handle size changing
168//        final int colCount = mColCount;
169//        if (mItemTops == null || mItemTops.length != colCount) {
170//            mItemTops = new int[colCount];
171//            mItemBottoms = new int[colCount];
172//            final int top = getPaddingTop();
173//            final int offset = top + Math.min(mRestoreOffset, 0);
174//            Arrays.fill(mItemTops, offset);
175//            Arrays.fill(mItemBottoms, offset);
176//            mLayoutRecords.clear();
177//            if (mInLayout) {
178//                removeAllViewsInLayout();
179//            } else {
180//                removeAllViews();
181//            }
182//            mRestoreOffset = 0;
183//        }
184
185        mPopulating = true;
186        layoutChildren(mDataChanged);
187        fillRight(mFirstPosition + getChildCount(), 0);
188        fillLeft(mFirstPosition - 1, 0);
189        mPopulating = false;
190        mDataChanged = false;
191    }
192
193    final void layoutChildren(boolean queryAdapter) {
194// TODO
195//        final int childCount = getChildCount();
196//        for (int i = 0; i < childCount; i++) {
197//            View child = getChildAt(i);
198//
199//            if (child.isLayoutRequested()) {
200//                final int widthSpec = MeasureSpec.makeMeasureSpec(child.getMeasuredWidth(), MeasureSpec.EXACTLY);
201//                final int heightSpec = MeasureSpec.makeMeasureSpec(child.getMeasuredHeight(), MeasureSpec.EXACTLY);
202//                child.measure(widthSpec, heightSpec);
203//                child.layout(child.getLeft(), child.getTop(), child.getRight(), child.getBottom());
204//            }
205//
206//            int childTop = mItemBottoms[col] > Integer.MIN_VALUE ?
207//                    mItemBottoms[col] + mItemMargin : child.getTop();
208//            if (span > 1) {
209//                int lowest = childTop;
210//                for (int j = col + 1; j < col + span; j++) {
211//                    final int bottom = mItemBottoms[j] + mItemMargin;
212//                    if (bottom > lowest) {
213//                        lowest = bottom;
214//                    }
215//                }
216//                childTop = lowest;
217//            }
218//            final int childHeight = child.getMeasuredHeight();
219//            final int childBottom = childTop + childHeight;
220//            final int childLeft = paddingLeft + col * (colWidth + itemMargin);
221//            final int childRight = childLeft + child.getMeasuredWidth();
222//            child.layout(childLeft, childTop, childRight, childBottom);
223//        }
224    }
225
226    /**
227     * Obtain the view and add it to our list of children. The view can be made
228     * fresh, converted from an unused view, or used as is if it was in the
229     * recycle bin.
230     *
231     * @param startPosition Logical position in the list to start from
232     * @param x Left or right edge of the view to add
233     * @param forward If true, align left edge to x and increase position.
234     *                If false, align right edge to x and decrease position.
235     * @return Number of views added
236     */
237    private int makeAndAddColumn(int startPosition, int x, boolean forward) {
238        int columnWidth = mLargeColumnWidth;
239        int addViews = 0;
240        for (int remaining = mLargeColumnUnitCount, i = 0;
241                remaining > 0 && startPosition + i >= 0 && startPosition + i < mItemCount;
242                i += forward ? 1 : -1, addViews++) {
243            if (mAdapter.getIntrinsicAspectRatio(startPosition + i) >= 1f) {
244                // landscape
245                remaining -= LAND_UNITS;
246            } else {
247                // portrait
248                remaining -= PORT_UNITS;
249                if (remaining < 0) {
250                    remaining += (mSmallColumnUnitCount - mLargeColumnUnitCount);
251                    columnWidth = mSmallColumnWidth;
252                }
253            }
254        }
255        int nextTop = 0;
256        for (int i = 0; i < addViews; i++) {
257            int position = startPosition + (forward ? i : -i);
258            View child = obtainView(position, null);
259            if (child.getParent() != this) {
260                if (mInLayout) {
261                    addViewInLayout(child, forward ? -1 : 0, child.getLayoutParams());
262                } else {
263                    addView(child, forward ? -1 : 0);
264                }
265            }
266            int heightSize = (int) (.5f + (mAdapter.getIntrinsicAspectRatio(position) >= 1f
267                    ? columnWidth / ASPECT_RATIO
268                    : columnWidth * ASPECT_RATIO));
269            int heightSpec = MeasureSpec.makeMeasureSpec(heightSize, MeasureSpec.EXACTLY);
270            int widthSpec = MeasureSpec.makeMeasureSpec(columnWidth, MeasureSpec.EXACTLY);
271            child.measure(widthSpec, heightSpec);
272            int childLeft = forward ? x : x - columnWidth;
273            child.layout(childLeft, nextTop, childLeft + columnWidth, nextTop + heightSize);
274            nextTop += heightSize;
275        }
276        return addViews;
277    }
278
279    @Override
280    public boolean onInterceptTouchEvent(MotionEvent ev) {
281        mVelocityTracker.addMovement(ev);
282        final int action = ev.getAction() & MotionEventCompat.ACTION_MASK;
283        switch (action) {
284            case MotionEvent.ACTION_DOWN:
285                mVelocityTracker.clear();
286                mScroller.abortAnimation();
287                mLastTouchX = ev.getX();
288                mActivePointerId = MotionEventCompat.getPointerId(ev, 0);
289                mTouchRemainderX = 0;
290                if (mTouchMode == TOUCH_MODE_FLINGING) {
291                    // Catch!
292                    mTouchMode = TOUCH_MODE_DRAGGING;
293                    return true;
294                }
295                break;
296
297            case MotionEvent.ACTION_MOVE: {
298                final int index = MotionEventCompat.findPointerIndex(ev, mActivePointerId);
299                if (index < 0) {
300                    Log.e(TAG, "onInterceptTouchEvent could not find pointer with id " +
301                            mActivePointerId + " - did StaggeredGridView receive an inconsistent " +
302                            "event stream?");
303                    return false;
304                }
305                final float x = MotionEventCompat.getX(ev, index);
306                final float dx = x - mLastTouchX + mTouchRemainderX;
307                final int deltaY = (int) dx;
308                mTouchRemainderX = dx - deltaY;
309
310                if (Math.abs(dx) > mTouchSlop) {
311                    mTouchMode = TOUCH_MODE_DRAGGING;
312                    return true;
313                }
314            }
315        }
316
317        return false;
318    }
319
320    @Override
321    public boolean onTouchEvent(MotionEvent ev) {
322        mVelocityTracker.addMovement(ev);
323        final int action = ev.getAction() & MotionEventCompat.ACTION_MASK;
324        switch (action) {
325            case MotionEvent.ACTION_DOWN:
326                mVelocityTracker.clear();
327                mScroller.abortAnimation();
328                mLastTouchX = ev.getX();
329                mActivePointerId = MotionEventCompat.getPointerId(ev, 0);
330                mTouchRemainderX = 0;
331                break;
332
333            case MotionEvent.ACTION_MOVE: {
334                final int index = MotionEventCompat.findPointerIndex(ev, mActivePointerId);
335                if (index < 0) {
336                    Log.e(TAG, "onInterceptTouchEvent could not find pointer with id " +
337                            mActivePointerId + " - did StaggeredGridView receive an inconsistent " +
338                            "event stream?");
339                    return false;
340                }
341                final float x = MotionEventCompat.getX(ev, index);
342                final float dx = x - mLastTouchX + mTouchRemainderX;
343                final int deltaX = (int) dx;
344                mTouchRemainderX = dx - deltaX;
345
346                if (Math.abs(dx) > mTouchSlop) {
347                    mTouchMode = TOUCH_MODE_DRAGGING;
348                }
349
350                if (mTouchMode == TOUCH_MODE_DRAGGING) {
351                    mLastTouchX = x;
352
353                    if (!trackMotionScroll(deltaX, true)) {
354                        // Break fling velocity if we impacted an edge.
355                        mVelocityTracker.clear();
356                    }
357                }
358            } break;
359
360            case MotionEvent.ACTION_CANCEL:
361                mTouchMode = TOUCH_MODE_IDLE;
362                break;
363
364            case MotionEvent.ACTION_UP: {
365                mVelocityTracker.computeCurrentVelocity(1000, mMaximumVelocity);
366                final float velocity = VelocityTrackerCompat.getXVelocity(mVelocityTracker,
367                        mActivePointerId);
368                if (Math.abs(velocity) > mFlingVelocity) { // TODO
369                    mTouchMode = TOUCH_MODE_FLINGING;
370                    mScroller.fling(0, 0, (int) velocity, 0,
371                            Integer.MIN_VALUE, Integer.MAX_VALUE, 0, 0);
372                    mLastTouchX = 0;
373                    ViewCompat.postInvalidateOnAnimation(this);
374                } else {
375                    mTouchMode = TOUCH_MODE_IDLE;
376                }
377
378            } break;
379        }
380        return true;
381    }
382
383    /**
384     *
385     * @param deltaX Pixels that content should move by
386     * @return true if the movement completed, false if it was stopped prematurely.
387     */
388    private boolean trackMotionScroll(int deltaX, boolean allowOverScroll) {
389        final boolean contentFits = contentFits();
390        final int allowOverhang = Math.abs(deltaX);
391
392        final int overScrolledBy;
393        final int movedBy;
394        if (!contentFits) {
395            final int overhang;
396            final boolean up;
397            mPopulating = true;
398            if (deltaX > 0) {
399                overhang = fillLeft(mFirstPosition - 1, allowOverhang);
400                up = true;
401            } else {
402                overhang = fillRight(mFirstPosition + getChildCount(), allowOverhang);
403                up = false;
404            }
405            movedBy = Math.min(overhang, allowOverhang);
406            offsetChildren(up ? movedBy : -movedBy);
407            recycleOffscreenViews();
408            mPopulating = false;
409            overScrolledBy = allowOverhang - overhang;
410        } else {
411            overScrolledBy = allowOverhang;
412            movedBy = 0;
413        }
414
415        if (allowOverScroll) {
416            final int overScrollMode = ViewCompat.getOverScrollMode(this);
417
418            if (overScrollMode == ViewCompat.OVER_SCROLL_ALWAYS ||
419                    (overScrollMode == ViewCompat.OVER_SCROLL_IF_CONTENT_SCROLLS && !contentFits)) {
420
421                if (overScrolledBy > 0) {
422                    EdgeEffectCompat edge = deltaX > 0 ? mLeftEdge : mRightEdge;
423                    edge.onPull((float) Math.abs(deltaX) / getWidth());
424                    ViewCompat.postInvalidateOnAnimation(this);
425                }
426            }
427        }
428
429        return deltaX == 0 || movedBy != 0;
430    }
431
432    /**
433     * Important: this method will leave offscreen views attached if they
434     * are required to maintain the invariant that child view with index i
435     * is always the view corresponding to position mFirstPosition + i.
436     */
437    private void recycleOffscreenViews() {
438        final int height = getHeight();
439        final int clearAbove = 0;
440        final int clearBelow = height;
441        for (int i = getChildCount() - 1; i >= 0; i--) {
442            final View child = getChildAt(i);
443            if (child.getTop() <= clearBelow)  {
444                // There may be other offscreen views, but we need to maintain
445                // the invariant documented above.
446                break;
447            }
448
449            if (mInLayout) {
450                removeViewsInLayout(i, 1);
451            } else {
452                removeViewAt(i);
453            }
454
455            mRecycler.addScrap(child);
456        }
457
458        while (getChildCount() > 0) {
459            final View child = getChildAt(0);
460            if (child.getBottom() >= clearAbove) {
461                // There may be other offscreen views, but we need to maintain
462                // the invariant documented above.
463                break;
464            }
465
466            if (mInLayout) {
467                removeViewsInLayout(0, 1);
468            } else {
469                removeViewAt(0);
470            }
471
472            mRecycler.addScrap(child);
473            mFirstPosition++;
474        }
475    }
476
477    final void offsetChildren(int offset) {
478        final int childCount = getChildCount();
479        for (int i = 0; i < childCount; i++) {
480            final View child = getChildAt(i);
481            child.layout(child.getLeft() + offset, child.getTop(),
482                    child.getRight() + offset, child.getBottom());
483        }
484    }
485
486    private boolean contentFits() {
487        final int childCount = getChildCount();
488        if (childCount == 0) return true;
489        if (childCount != mItemCount) return false;
490
491        return getChildAt(0).getLeft() >= getPaddingLeft() &&
492                getChildAt(childCount - 1).getRight() <= getWidth() - getPaddingRight();
493    }
494
495    private void recycleAllViews() {
496        for (int i = 0; i < getChildCount(); i++) {
497            mRecycler.addScrap(getChildAt(i));
498        }
499
500        if (mInLayout) {
501            removeAllViewsInLayout();
502        } else {
503            removeAllViews();
504        }
505    }
506
507    private int fillRight(int pos, int overhang) {
508        int end = (getRight() - getLeft()) + overhang;
509
510        int nextLeft = getChildCount() == 0 ? 0 : getChildAt(getChildCount() - 1).getRight();
511        while (nextLeft < end && pos < mItemCount) {
512            pos += makeAndAddColumn(pos, nextLeft, true);
513            nextLeft = getChildAt(getChildCount() - 1).getRight();
514        }
515        final int gridRight = getWidth() - getPaddingRight();
516        return getChildAt(getChildCount() - 1).getRight() - gridRight;
517    }
518
519    private int fillLeft(int pos, int overhang) {
520        int end = getPaddingLeft() - overhang;
521
522        int nextRight = getChildAt(0).getLeft();
523        while (nextRight > end && pos >= 0) {
524            pos -= makeAndAddColumn(pos, nextRight, false);
525            nextRight = getChildAt(0).getLeft();
526        }
527
528        mFirstPosition = pos + 1;
529        return getPaddingLeft() - getChildAt(0).getLeft();
530    }
531
532    @Override
533    public void computeScroll() {
534        if (mScroller.computeScrollOffset()) {
535            final int x = mScroller.getCurrX();
536            final int dx = (int) (x - mLastTouchX);
537            mLastTouchX = x;
538            final boolean stopped = !trackMotionScroll(dx, false);
539
540            if (!stopped && !mScroller.isFinished()) {
541                ViewCompat.postInvalidateOnAnimation(this);
542            } else {
543                if (stopped) {
544                    final int overScrollMode = ViewCompat.getOverScrollMode(this);
545                    if (overScrollMode != ViewCompat.OVER_SCROLL_NEVER) {
546                        final EdgeEffectCompat edge;
547                        if (dx > 0) {
548                            edge = mLeftEdge;
549                        } else {
550                            edge = mRightEdge;
551                        }
552                        edge.onAbsorb(Math.abs((int) mScroller.getCurrVelocity()));
553                        ViewCompat.postInvalidateOnAnimation(this);
554                    }
555                    mScroller.abortAnimation();
556                }
557                mTouchMode = TOUCH_MODE_IDLE;
558            }
559        }
560    }
561
562    @Override
563    public void draw(Canvas canvas) {
564        super.draw(canvas);
565
566        if (!mLeftEdge.isFinished()) {
567            final int restoreCount = canvas.save();
568            final int height = getHeight() - getPaddingTop() - getPaddingBottom();
569
570            canvas.rotate(270);
571            canvas.translate(-height + getPaddingTop(), 0);
572            mLeftEdge.setSize(height, getWidth());
573            if (mLeftEdge.draw(canvas)) {
574                postInvalidateOnAnimation();
575            }
576            canvas.restoreToCount(restoreCount);
577        }
578        if (!mRightEdge.isFinished()) {
579            final int restoreCount = canvas.save();
580            final int width = getWidth();
581            final int height = getHeight() - getPaddingTop() - getPaddingBottom();
582
583            canvas.rotate(90);
584            canvas.translate(-getPaddingTop(), width);
585            mRightEdge.setSize(height, width);
586            if (mRightEdge.draw(canvas)) {
587                postInvalidateOnAnimation();
588            }
589            canvas.restoreToCount(restoreCount);
590        }
591    }
592
593    /**
594     * Obtain a populated view from the adapter. If optScrap is non-null and is not
595     * reused it will be placed in the recycle bin.
596     *
597     * @param position position to get view for
598     * @param optScrap Optional scrap view; will be reused if possible
599     * @return A new view, a recycled view from mRecycler, or optScrap
600     */
601    private final View obtainView(int position, View optScrap) {
602        View view = mRecycler.getTransientStateView(position);
603        if (view != null) {
604            return view;
605        }
606
607        // Reuse optScrap if it's of the right type (and not null)
608        final int optType = optScrap != null ?
609                ((LayoutParams) optScrap.getLayoutParams()).viewType : -1;
610        final int positionViewType = mAdapter.getItemViewType(position);
611        final View scrap = optType == positionViewType ?
612                optScrap : mRecycler.getScrapView(positionViewType);
613
614        view = mAdapter.getView(position, scrap, this);
615
616        if (view != scrap && scrap != null) {
617            // The adapter didn't use it; put it back.
618            mRecycler.addScrap(scrap);
619        }
620
621        ViewGroup.LayoutParams lp = view.getLayoutParams();
622
623        if (view.getParent() != this) {
624            if (lp == null) {
625                lp = generateDefaultLayoutParams();
626            } else if (!checkLayoutParams(lp)) {
627                lp = generateLayoutParams(lp);
628            }
629            view.setLayoutParams(lp);
630        }
631
632        final LayoutParams sglp = (LayoutParams) lp;
633        sglp.position = position;
634        sglp.viewType = positionViewType;
635
636        return view;
637    }
638
639    public GalleryThumbnailAdapter getAdapter() {
640        return mAdapter;
641    }
642
643    public void setAdapter(GalleryThumbnailAdapter adapter) {
644        if (mAdapter != null) {
645            mAdapter.unregisterDataSetObserver(mObserver);
646        }
647        // TODO: If the new adapter says that there are stable IDs, remove certain layout records
648        // and onscreen views if they have changed instead of removing all of the state here.
649        clearAllState();
650        mAdapter = adapter;
651        mDataChanged = true;
652        mOldItemCount = mItemCount = adapter != null ? adapter.getCount() : 0;
653        if (adapter != null) {
654            adapter.registerDataSetObserver(mObserver);
655            mRecycler.setViewTypeCount(adapter.getViewTypeCount());
656            mHasStableIds = adapter.hasStableIds();
657        } else {
658            mHasStableIds = false;
659        }
660        populate();
661    }
662
663    /**
664     * Clear all state because the grid will be used for a completely different set of data.
665     */
666    private void clearAllState() {
667        // Clear all layout records and views
668        removeAllViews();
669
670        // Reset to the top of the grid
671        mFirstPosition = 0;
672
673        // Clear recycler because there could be different view types now
674        mRecycler.clear();
675    }
676
677    @Override
678    protected LayoutParams generateDefaultLayoutParams() {
679        return new LayoutParams(LayoutParams.WRAP_CONTENT);
680    }
681
682    @Override
683    protected LayoutParams generateLayoutParams(ViewGroup.LayoutParams lp) {
684        return new LayoutParams(lp);
685    }
686
687    @Override
688    protected boolean checkLayoutParams(ViewGroup.LayoutParams lp) {
689        return lp instanceof LayoutParams;
690    }
691
692    @Override
693    public ViewGroup.LayoutParams generateLayoutParams(AttributeSet attrs) {
694        return new LayoutParams(getContext(), attrs);
695    }
696
697    public static class LayoutParams extends ViewGroup.LayoutParams {
698        private static final int[] LAYOUT_ATTRS = new int[] {
699                android.R.attr.layout_span
700        };
701
702        private static final int SPAN_INDEX = 0;
703
704        /**
705         * The number of columns this item should span
706         */
707        public int span = 1;
708
709        /**
710         * Item position this view represents
711         */
712        int position;
713
714        /**
715         * Type of this view as reported by the adapter
716         */
717        int viewType;
718
719        /**
720         * The column this view is occupying
721         */
722        int column;
723
724        /**
725         * The stable ID of the item this view displays
726         */
727        long id = -1;
728
729        public LayoutParams(int height) {
730            super(MATCH_PARENT, height);
731
732            if (this.height == MATCH_PARENT) {
733                Log.w(TAG, "Constructing LayoutParams with height MATCH_PARENT - " +
734                        "impossible! Falling back to WRAP_CONTENT");
735                this.height = WRAP_CONTENT;
736            }
737        }
738
739        public LayoutParams(Context c, AttributeSet attrs) {
740            super(c, attrs);
741
742            if (this.width != MATCH_PARENT) {
743                Log.w(TAG, "Inflation setting LayoutParams width to " + this.width +
744                        " - must be MATCH_PARENT");
745                this.width = MATCH_PARENT;
746            }
747            if (this.height == MATCH_PARENT) {
748                Log.w(TAG, "Inflation setting LayoutParams height to MATCH_PARENT - " +
749                        "impossible! Falling back to WRAP_CONTENT");
750                this.height = WRAP_CONTENT;
751            }
752
753            TypedArray a = c.obtainStyledAttributes(attrs, LAYOUT_ATTRS);
754            span = a.getInteger(SPAN_INDEX, 1);
755            a.recycle();
756        }
757
758        public LayoutParams(ViewGroup.LayoutParams other) {
759            super(other);
760
761            if (this.width != MATCH_PARENT) {
762                Log.w(TAG, "Constructing LayoutParams with width " + this.width +
763                        " - must be MATCH_PARENT");
764                this.width = MATCH_PARENT;
765            }
766            if (this.height == MATCH_PARENT) {
767                Log.w(TAG, "Constructing LayoutParams with height MATCH_PARENT - " +
768                        "impossible! Falling back to WRAP_CONTENT");
769                this.height = WRAP_CONTENT;
770            }
771        }
772    }
773
774    private class RecycleBin {
775        private ArrayList<View>[] mScrapViews;
776        private int mViewTypeCount;
777        private int mMaxScrap;
778
779        private SparseArray<View> mTransientStateViews;
780
781        public void setViewTypeCount(int viewTypeCount) {
782            if (viewTypeCount < 1) {
783                throw new IllegalArgumentException("Must have at least one view type (" +
784                        viewTypeCount + " types reported)");
785            }
786            if (viewTypeCount == mViewTypeCount) {
787                return;
788            }
789
790            ArrayList<View>[] scrapViews = new ArrayList[viewTypeCount];
791            for (int i = 0; i < viewTypeCount; i++) {
792                scrapViews[i] = new ArrayList<View>();
793            }
794            mViewTypeCount = viewTypeCount;
795            mScrapViews = scrapViews;
796        }
797
798        public void clear() {
799            final int typeCount = mViewTypeCount;
800            for (int i = 0; i < typeCount; i++) {
801                mScrapViews[i].clear();
802            }
803            if (mTransientStateViews != null) {
804                mTransientStateViews.clear();
805            }
806        }
807
808        public void clearTransientViews() {
809            if (mTransientStateViews != null) {
810                mTransientStateViews.clear();
811            }
812        }
813
814        public void addScrap(View v) {
815            final LayoutParams lp = (LayoutParams) v.getLayoutParams();
816            if (ViewCompat.hasTransientState(v)) {
817                if (mTransientStateViews == null) {
818                    mTransientStateViews = new SparseArray<View>();
819                }
820                mTransientStateViews.put(lp.position, v);
821                return;
822            }
823
824            final int childCount = getChildCount();
825            if (childCount > mMaxScrap) {
826                mMaxScrap = childCount;
827            }
828
829            ArrayList<View> scrap = mScrapViews[lp.viewType];
830            if (scrap.size() < mMaxScrap) {
831                scrap.add(v);
832            }
833        }
834
835        public View getTransientStateView(int position) {
836            if (mTransientStateViews == null) {
837                return null;
838            }
839
840            final View result = mTransientStateViews.get(position);
841            if (result != null) {
842                mTransientStateViews.remove(position);
843            }
844            return result;
845        }
846
847        public View getScrapView(int type) {
848            ArrayList<View> scrap = mScrapViews[type];
849            if (scrap.isEmpty()) {
850                return null;
851            }
852
853            final int index = scrap.size() - 1;
854            final View result = scrap.get(index);
855            scrap.remove(index);
856            return result;
857        }
858    }
859
860    private class AdapterDataSetObserver extends DataSetObserver {
861        @Override
862        public void onChanged() {
863            mDataChanged = true;
864            mOldItemCount = mItemCount;
865            mItemCount = mAdapter.getCount();
866
867            // TODO: Consider matching these back up if we have stable IDs.
868            mRecycler.clearTransientViews();
869
870            if (!mHasStableIds) {
871                recycleAllViews();
872            }
873
874            // TODO: consider repopulating in a deferred runnable instead
875            // (so that successive changes may still be batched)
876            requestLayout();
877        }
878
879        @Override
880        public void onInvalidated() {
881        }
882    }
883}
884