• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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          */
getIntrinsicAspectRatio(int position)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 
GalleryThumbnailView(Context context)94     public GalleryThumbnailView(Context context) {
95         this(context, null);
96     }
97 
GalleryThumbnailView(Context context, AttributeSet attrs)98     public GalleryThumbnailView(Context context, AttributeSet attrs) {
99         this(context, attrs, 0);
100     }
101 
GalleryThumbnailView(Context context, AttributeSet attrs, int defStyle)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
requestLayout()118     public void requestLayout() {
119         if (!mPopulating) {
120             super.requestLayout();
121         }
122     }
123 
124     @Override
onMeasure(int widthMeasureSpec, int heightMeasureSpec)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
onLayout(boolean changed, int l, int t, int r, int b)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 
populate()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 
layoutChildren(boolean queryAdapter)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      */
makeAndAddColumn(int startPosition, int x, boolean forward)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
onInterceptTouchEvent(MotionEvent ev)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
onTouchEvent(MotionEvent ev)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      */
trackMotionScroll(int deltaX, boolean allowOverScroll)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      */
recycleOffscreenViews()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 
offsetChildren(int offset)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 
contentFits()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 
recycleAllViews()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 
fillRight(int pos, int overhang)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 
fillLeft(int pos, int overhang)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
computeScroll()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
draw(Canvas canvas)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      */
obtainView(int position, View optScrap)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 
getAdapter()639     public GalleryThumbnailAdapter getAdapter() {
640         return mAdapter;
641     }
642 
setAdapter(GalleryThumbnailAdapter adapter)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      */
clearAllState()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
generateDefaultLayoutParams()678     protected LayoutParams generateDefaultLayoutParams() {
679         return new LayoutParams(LayoutParams.WRAP_CONTENT);
680     }
681 
682     @Override
generateLayoutParams(ViewGroup.LayoutParams lp)683     protected LayoutParams generateLayoutParams(ViewGroup.LayoutParams lp) {
684         return new LayoutParams(lp);
685     }
686 
687     @Override
checkLayoutParams(ViewGroup.LayoutParams lp)688     protected boolean checkLayoutParams(ViewGroup.LayoutParams lp) {
689         return lp instanceof LayoutParams;
690     }
691 
692     @Override
generateLayoutParams(AttributeSet attrs)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 
LayoutParams(int height)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 
LayoutParams(Context c, AttributeSet attrs)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 
LayoutParams(ViewGroup.LayoutParams other)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 
setViewTypeCount(int viewTypeCount)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 
clear()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 
clearTransientViews()808         public void clearTransientViews() {
809             if (mTransientStateViews != null) {
810                 mTransientStateViews.clear();
811             }
812         }
813 
addScrap(View v)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 
getTransientStateView(int position)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 
getScrapView(int type)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
onChanged()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
onInvalidated()880         public void onInvalidated() {
881         }
882     }
883 }
884