Home | History | Annotate | Download | only in views
      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 
     17 package com.android.photos.views;
     18 
     19 import android.content.Context;
     20 import android.content.res.TypedArray;
     21 import android.database.DataSetObserver;
     22 import android.graphics.Canvas;
     23 import androidx.core.view.MotionEventCompat;
     24 import androidx.core.view.VelocityTrackerCompat;
     25 import androidx.core.view.ViewCompat;
     26 import androidx.core.widget.EdgeEffectCompat;
     27 import android.util.AttributeSet;
     28 import android.util.Log;
     29 import android.util.SparseArray;
     30 import android.view.MotionEvent;
     31 import android.view.VelocityTracker;
     32 import android.view.View;
     33 import android.view.ViewConfiguration;
     34 import android.view.ViewGroup;
     35 import android.widget.ListAdapter;
     36 import android.widget.OverScroller;
     37 
     38 import java.util.ArrayList;
     39 
     40 public 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