1 /*
2  * Copyright (C) 2012 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.deskclock.widget.sgv;
18 
19 import android.animation.Animator;
20 import android.animation.AnimatorListenerAdapter;
21 import android.animation.AnimatorSet;
22 import android.animation.ValueAnimator;
23 import android.annotation.SuppressLint;
24 import android.content.Context;
25 import android.content.res.TypedArray;
26 import android.database.DataSetObserver;
27 import android.graphics.Bitmap;
28 import android.graphics.Canvas;
29 import android.graphics.PixelFormat;
30 import android.graphics.Point;
31 import android.graphics.Rect;
32 import android.os.Handler;
33 import android.os.Parcel;
34 import android.os.Parcelable;
35 import android.support.v4.util.SparseArrayCompat;
36 import android.support.v4.view.MotionEventCompat;
37 import android.support.v4.view.VelocityTrackerCompat;
38 import android.support.v4.view.ViewCompat;
39 import android.support.v4.widget.EdgeEffectCompat;
40 import android.util.AttributeSet;
41 import android.util.Log;
42 import android.util.SparseArray;
43 import android.view.DragEvent;
44 import android.view.Gravity;
45 import android.view.MotionEvent;
46 import android.view.VelocityTracker;
47 import android.view.View;
48 import android.view.ViewConfiguration;
49 import android.view.ViewGroup;
50 import android.view.WindowManager;
51 import android.widget.GridView;
52 import android.widget.ImageView;
53 import android.widget.ScrollView;
54 
55 import com.android.deskclock.widget.sgv.SgvAnimationHelper.AnimationIn;
56 import com.android.deskclock.widget.sgv.SgvAnimationHelper.AnimationOut;
57 
58 import java.util.ArrayList;
59 import java.util.Arrays;
60 import java.util.HashMap;
61 import java.util.List;
62 import java.util.Map;
63 
64 /**
65  * Temporarily copied from support v4 library so that StaggeredGridView can access
66  * animation APIs on the current SDK version.
67  */
68 /**
69  * ListView and GridView just not complex enough? Try StaggeredGridView!
70  *
71  * <p>StaggeredGridView presents a multi-column grid with consistent column sizes
72  * but varying row sizes between the columns. Each successive item from a
73  * {@link android.widget.ListAdapter ListAdapter} will be arranged from top to bottom,
74  * left to right. The largest vertical gap is always filled first.</p>
75  *
76  * <p>Item views may span multiple columns as specified by their {@link LayoutParams}.
77  * The attribute <code>android:layout_span</code> may be used when inflating
78  * item views from xml.</p>
79  */
80 public class StaggeredGridView extends ViewGroup {
81 
82     private static final String TAG = "Clock-" + StaggeredGridView.class.getSimpleName();
83 
84     /*
85      * There are a few things you should know if you're going to make modifications
86      * to StaggeredGridView.
87      *
88      * Like ListView, SGV populates from an adapter and recycles views that fall out
89      * of the visible boundaries of the grid. A few invariants always hold:
90      *
91      * - mFirstPosition is the adapter position of the View returned by getChildAt(0).
92      * - Any child index can be translated to an adapter position by adding mFirstPosition.
93      * - Any adapter position can be translated to a child index by subtracting mFirstPosition.
94      * - Views for items in the range [mFirstPosition, mFirstPosition + getChildCount()) are
95      *   currently attached to the grid as children. All other adapter positions do not have
96      *   active views.
97      *
98      * This means a few things thanks to the staggered grid's nature. Some views may stay attached
99      * long after they have scrolled offscreen if removing and recycling them would result in
100      * breaking one of the invariants above.
101      *
102      * LayoutRecords are used to track data about a particular item's layout after the associated
103      * view has been removed. These let positioning and the choice of column for an item
104      * remain consistent even though the rules for filling content up vs. filling down vary.
105      *
106      * Whenever layout parameters for a known LayoutRecord change, other LayoutRecords before
107      * or after it may need to be invalidated. e.g. if the item's height or the number
108      * of columns it spans changes, all bets for other items in the same direction are off
109      * since the cached information no longer applies.
110      */
111 
112     private GridAdapter mAdapter;
113 
114     public static final int COLUMN_COUNT_AUTO = -1;
115 
116     /**
117      * The window size to search for a specific item when restoring scroll position.
118      */
119     private final int SCROLL_RESTORE_WINDOW_SIZE = 10;
120 
121     private static final int CHILD_TO_REORDER_AREA_RATIO = 4;
122 
123     private static final int SINGLE_COL_REORDERING_AREA_SIZE = 30;
124 
125     // Time delay in milliseconds between posting each scroll runnables.
126     private static final int SCROLL_HANDLER_DELAY = 5;
127 
128     // The default rate of pixels to scroll by when a child view is dragged towards the
129     // upper and lower bound of this view.
130     private static final int DRAG_SCROLL_RATE = 10;
131 
132     public static final int ANIMATION_DELAY_IN_MS = 50;
133 
134     private AnimationIn mAnimationInMode = AnimationIn.NONE;
135     private AnimationOut mAnimationOutMode = AnimationOut.NONE;
136 
137     private AnimatorSet mCurrentRunningAnimatorSet = null;
138 
139     /**
140      * Flag to indicate whether the current running animator set was canceled before it reaching
141      * the end of the animations.  This flag is used to help indicate whether the next set of
142      * animators should resume from where the last animator set left off.
143      */
144     boolean mIsCurrentAnimationCanceled = false;
145 
146     private int mColCountSetting = 2;
147     private int mColCount = 2;
148     private int mMinColWidth = 0;
149     private int mItemMargin = 0;
150 
151     private int[] mItemTops;
152     private int[] mItemBottoms;
153 
154     private final Rect mTempRect = new Rect();
155 
156     private boolean mFastChildLayout;
157     private boolean mPopulating;
158     private boolean mInLayout;
159 
160     private boolean mIsRtlLayout;
161 
162     private final RecycleBin mRecycler = new RecycleBin();
163 
164     private final AdapterDataSetObserver mObserver = new AdapterDataSetObserver();
165 
166     private boolean mDataChanged;
167     private int mItemCount;
168 
169     /**
170      * After data set change, we ask adapter the first view that changed.
171      * Any view from 0 to mFirstChangedPosition - 1 is not changed.
172      */
173     private int mFirstChangedPosition;
174 
175     /**
176      * If set to true, then we guard against jagged edges in the grid by doing expensive
177      * computation. Otherwise if this is false, we skip the computation.
178      */
179     private boolean mGuardAgainstJaggedEdges;
180 
181     private boolean mHasStableIds;
182 
183     /**
184      * List of all views to animate out.  This is used when we need to animate out stale views.
185      */
186     private final List<View> mViewsToAnimateOut = new ArrayList<View>();
187 
188     private int mFirstPosition;
189 
190     private long mFocusedChildIdToScrollIntoView;
191     private ScrollState mCurrentScrollState;
192 
193     private final int mTouchSlop;
194     private final int mMaximumVelocity;
195     private final int mFlingVelocity;
196     private float mLastTouchY = 0;
197     private float mTouchRemainderY;
198     private int mActivePointerId;
199 
200     private static final int TOUCH_MODE_IDLE = 0;
201     private static final int TOUCH_MODE_DRAGGING = 1;
202     private static final int TOUCH_MODE_FLINGING = 2;
203     private static final int TOUCH_MODE_OVERFLING = 3;
204 
205     // Value used to estimate the range of scroll and scroll position
206     final static int SCROLLING_ESTIMATED_ITEM_HEIGHT = 100;
207 
208     private int mTouchMode;
209     private final VelocityTracker mVelocityTracker = VelocityTracker.obtain();
210     private final OverScrollerSGV mScroller;
211 
212     private final EdgeEffectCompat mTopEdge;
213     private final EdgeEffectCompat mBottomEdge;
214 
215     private boolean mIsDragReorderingEnabled;
216 
217     private ScrollListener mScrollListener;
218     private OnSizeChangedListener mOnSizeChangedListener;
219 
220     // The view to show when the adapter is empty.
221     private View mEmptyView;
222 
223     // The size of the region at location relative to the child's edges where reordering
224     // can happen if another child view is dragged and dropped over it.
225     private int mHorizontalReorderingAreaSize;
226 
227     // TODO: Put these states into a ReorderingParam object for maintainability.
228     private ImageView mDragView;
229 
230     // X and Y positions of the touch down event that started the drag
231     private int mTouchDownForDragStartX;
232     private int mTouchDownForDragStartY;
233 
234     // X and Y offsets inside the item from where the user grabbed to the
235     // child's left coordinate.
236     // This is used to aid in the drawing of the drag shadow.
237     private int mTouchOffsetToChildLeft;
238     private int mTouchOffsetToChildTop;
239 
240     // Difference between screen coordinates and coordinates in this view.
241     private int mOffsetToAbsoluteX;
242     private int mOffsetToAbsoluteY;
243 
244     // the cached positions of the drag view when released.
245     private Rect mCachedDragViewRect;
246 
247     // the current drag state
248     private int mDragState;
249 
250     // the height of this view
251     private int mHeight;
252 
253     // The bounds of the screen that should initiate scrolling when a view
254     // is dragged past these positions.
255     private int mUpperScrollBound;
256     private int mLowerScrollBound;
257 
258     // The Bitmap that contains the drag shadow.
259     private Bitmap mDragBitmap;
260     private final int mOverscrollDistance;
261 
262     private final WindowManager mWindowManager;
263     private WindowManager.LayoutParams mWindowParams;
264     private static final int mWindowManagerLayoutFlags =
265             WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE |
266             WindowManager.LayoutParams.FLAG_NOT_TOUCHABLE |
267             WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON |
268             WindowManager.LayoutParams.FLAG_LAYOUT_IN_SCREEN |
269             WindowManager.LayoutParams.FLAG_LAYOUT_NO_LIMITS;
270 
271     private ReorderHelper mReorderHelper;
272 
273     /**
274      * Indicates whether to use pixels-based or position-based scrollbar
275      * properties.
276      * This property is borrow from AbsListView
277      */
278     private boolean mSmoothScrollbarEnabled = false;
279 
280     private static final class LayoutRecord {
281         public int column;
282         public long id = -1;
283         public int height;
284         public int span;
285         private int[] mMargins;
286 
ensureMargins()287         private final void ensureMargins() {
288             if (mMargins == null) {
289                 // Don't need to confirm length;
290                 // all layoutrecords are purged when column count changes.
291                 mMargins = new int[span * 2];
292             }
293         }
294 
getMarginAbove(int col)295         public final int getMarginAbove(int col) {
296             if (mMargins == null) {
297                 return 0;
298             }
299             return mMargins[col * 2];
300         }
301 
getMarginBelow(int col)302         public final int getMarginBelow(int col) {
303             if (mMargins == null) {
304                 return 0;
305             }
306             return mMargins[col * 2 + 1];
307         }
308 
setMarginAbove(int col, int margin)309         public final void setMarginAbove(int col, int margin) {
310             if (mMargins == null && margin == 0) {
311                 return;
312             }
313             ensureMargins();
314             mMargins[col * 2] = margin;
315         }
316 
setMarginBelow(int col, int margin)317         public final void setMarginBelow(int col, int margin) {
318             if (mMargins == null && margin == 0) {
319                 return;
320             }
321             ensureMargins();
322             mMargins[col * 2 + 1] = margin;
323         }
324 
325         @Override
toString()326         public String toString() {
327             String result = "LayoutRecord{c=" + column + ", id=" + id + " h=" + height +
328                     " s=" + span;
329             if (mMargins != null) {
330                 result += " margins[above, below](";
331                 for (int i = 0; i < mMargins.length; i += 2) {
332                     result += "[" + mMargins[i] + ", " + mMargins[i+1] + "]";
333                 }
334                 result += ")";
335             }
336             return result + "}";
337         }
338     }
339 
340     private final Map<Long, ViewRectPair> mChildRectsForAnimation =
341             new HashMap<Long, ViewRectPair>();
342 
343     private final SparseArrayCompat<LayoutRecord> mLayoutRecords =
344             new SparseArrayCompat<LayoutRecord>();
345 
346     // Handler for executing the scroll runnable
347     private Handler mScrollHandler;
348 
349     // Boolean is true when the {@link #mDragScroller} scroll runanbled has been kicked off.
350     // This is set back to false when it is removed from the handler.
351     private boolean mIsDragScrollerRunning;
352 
353     /**
354      * Scroller runnable to invoke scrolling when user is holding a dragged view over the upper
355      * or lower bounds of the screen.
356      */
357     private final Runnable mDragScroller = new Runnable() {
358         @Override
359         public void run() {
360             if (mDragState == ReorderUtils.DRAG_STATE_NONE) {
361                 return;
362             }
363 
364             boolean enableUpdate = true;
365             if (mLastTouchY >= mLowerScrollBound) {
366                 // scroll the list up a bit if we're past the lower bound, and the direction
367                 // of the movement is towards the bottom of the view.
368                 if (trackMotionScroll(-DRAG_SCROLL_RATE, false)) {
369                     // Disable reordering if the view is scrolling
370                     enableUpdate = false;
371                 }
372             } else if (mLastTouchY <= mUpperScrollBound) {
373                 // scroll the list down a bit if we're past the upper bound, and the direction
374                 // of the movement is towards the top of the view.
375                 if (trackMotionScroll(DRAG_SCROLL_RATE, false)) {
376                     // Disable reordering if the view is scrolling
377                     enableUpdate = false;
378                 }
379             }
380 
381             mReorderHelper.enableUpdatesOnDrag(enableUpdate);
382 
383             mScrollHandler.postDelayed(this, SCROLL_HANDLER_DELAY);
384         }
385     };
386 
StaggeredGridView(Context context)387     public StaggeredGridView(Context context) {
388         this(context, null);
389     }
390 
StaggeredGridView(Context context, AttributeSet attrs)391     public StaggeredGridView(Context context, AttributeSet attrs) {
392         this(context, attrs, 0);
393     }
394 
StaggeredGridView(Context context, AttributeSet attrs, int defStyle)395     public StaggeredGridView(Context context, AttributeSet attrs, int defStyle) {
396         super(context, attrs, defStyle);
397 
398         final ViewConfiguration vc = ViewConfiguration.get(context);
399         mTouchSlop = vc.getScaledTouchSlop();
400         mMaximumVelocity = vc.getScaledMaximumFlingVelocity();
401         mFlingVelocity = vc.getScaledMinimumFlingVelocity();
402         mScroller = new OverScrollerSGV(context);
403 
404         mTopEdge = new EdgeEffectCompat(context);
405         mBottomEdge = new EdgeEffectCompat(context);
406         setWillNotDraw(false);
407         setClipToPadding(false);
408 
409         SgvAnimationHelper.initialize(context);
410 
411         mDragState = ReorderUtils.DRAG_STATE_NONE;
412         mIsDragReorderingEnabled = true;
413         mWindowManager = (WindowManager) context.getSystemService(Context.WINDOW_SERVICE);
414         final ViewConfiguration configuration = ViewConfiguration.get(context);
415         mOverscrollDistance = configuration.getScaledOverflingDistance();
416         // Disable splitting event. Only one of the children can handle motion event.
417         setMotionEventSplittingEnabled(false);
418     }
419 
420     /**
421      * Check to see if the current layout is Right-to-Left.  This check is only supported for
422      * API 17+.  For earlier versions, this method will just return false.
423      *
424      * NOTE:  This is based on the private API method in {@link View} class.
425      *
426      * @return boolean Boolean indicating whether the currently locale is RTL.
427      */
428     @SuppressLint("NewApi")
isLayoutRtl()429     private boolean isLayoutRtl() {
430         if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.JELLY_BEAN_MR1) {
431             return View.LAYOUT_DIRECTION_RTL == getLayoutDirection();
432         } else {
433             return false;
434         }
435     }
436 
437     /**
438      * Set a fixed number of columns for this grid. Space will be divided evenly
439      * among all columns, respecting the item margin between columns.
440      * The default is 2. (If it were 1, perhaps you should be using a
441      * {@link android.widget.ListView ListView}.)
442      *
443      * @param colCount Number of columns to display.
444      * @see #setMinColumnWidth(int)
445      */
setColumnCount(int colCount)446     public void setColumnCount(int colCount) {
447         if (colCount < 1 && colCount != COLUMN_COUNT_AUTO) {
448             throw new IllegalArgumentException("Column count must be at least 1 - received " +
449                     colCount);
450         }
451         final boolean needsPopulate = colCount != mColCount;
452         mColCount = mColCountSetting = colCount;
453         if (needsPopulate) {
454             // When switching column count, for now, don't restore scroll position, and just
455             // start layout fresh again.
456             clearAllState();
457 
458             mHorizontalReorderingAreaSize = 0;
459             populate();
460         }
461     }
462 
getColumnCount()463     public int getColumnCount() {
464         return mColCount;
465     }
466 
467     /**
468      * Set whether or not to explicitly guard against "jagged edges" in the grid
469      * (meaning that the top edge of the children views in the first row of the grid can be
470      * horizontally misaligned).
471      *
472      * If guardAgainstJaggedEdges is true, then we prevent jagged edges by computing the heights of
473      * all views starting at the 0th position of the adapter to figure out the proper offset of the
474      * views currently on screen. This is an expensive operation and should be avoided if possible.
475      *
476      * If guardAgainstJaggedEdges is false, then we can skip the expensive computation that
477      * guards against jagged edges and just layout views on the screen starting from mFirstPosition
478      * (ignoring what came before it).
479      */
setGuardAgainstJaggedEdges(boolean guardAgainstJaggedEdges)480     public void setGuardAgainstJaggedEdges(boolean guardAgainstJaggedEdges) {
481         mGuardAgainstJaggedEdges = guardAgainstJaggedEdges;
482     }
483 
484     /**
485      * Set a minimum column width for
486      * @param minColWidth
487      */
setMinColumnWidth(int minColWidth)488     public void setMinColumnWidth(int minColWidth) {
489         mMinColWidth = minColWidth;
490         setColumnCount(COLUMN_COUNT_AUTO);
491     }
492 
493     /**
494      * Set the margin between items in pixels. This margin is applied
495      * both vertically and horizontally.
496      *
497      * @param marginPixels Spacing between items in pixels
498      */
setItemMargin(int marginPixels)499     public void setItemMargin(int marginPixels) {
500         // We only need to {@link #populate()} if the margin has been changed.
501         if (marginPixels != mItemMargin) {
502             mItemMargin = marginPixels;
503             populate();
504         }
505     }
506 
getItemMargin()507     public int getItemMargin() {
508         return mItemMargin;
509     }
510 
511     /**
512      * When smooth scrollbar is enabled, the position and size of the scrollbar thumb
513      * is computed based on the number of visible pixels in the visible items. This
514      * however assumes that all list items have the same height. If you use a list in
515      * which items have different heights, the scrollbar will change appearance as the
516      * user scrolls through the list. To avoid this issue, you need to disable this
517      * property.
518      *
519      * When smooth scrollbar is disabled, the position and size of the scrollbar thumb
520      * is based solely on the number of items in the adapter and the position of the
521      * visible items inside the adapter. This provides a stable scrollbar as the user
522      * navigates through a list of items with varying heights.
523      *
524      * @param enabled Whether or not to enable smooth scrollbar.
525      *
526      * @see #setSmoothScrollbarEnabled(boolean)
527      * @attr ref android.R.styleable#AbsListView_smoothScrollbar
528      */
setSmoothScrollbarEnabled(boolean enabled)529     public void setSmoothScrollbarEnabled(boolean enabled) {
530         mSmoothScrollbarEnabled = enabled;
531     }
532 
533     /**
534      * Returns the current state of the fast scroll feature.
535      *
536      * @return True if smooth scrollbar is enabled is enabled, false otherwise.
537      *
538      * @see #setSmoothScrollbarEnabled(boolean)
539      */
isSmoothScrollbarEnabled()540     public boolean isSmoothScrollbarEnabled() {
541         return mSmoothScrollbarEnabled;
542     }
543 
544 
545     /**
546      * Return the child view specified by the coordinates if
547      * there exists a child there.
548      *
549      * @return the child in this StaggeredGridView at the coordinates, null otherwise.
550      */
getChildAtCoordinate(int x, int y)551     private View getChildAtCoordinate(int x, int y) {
552         if (y < 0) {
553             // TODO: If we've dragged off the screen, return null for now until we know what
554             // we'd like the experience to be like.
555             return null;
556         }
557 
558         final Rect frame = new Rect();
559         final int count = getChildCount();
560         for (int i = 0; i < count; i++) {
561 
562             final View childView = getChildAt(i);
563             childView.getHitRect(frame);
564             if (frame.contains(x, y)) {
565                 return getChildAt(i);
566             }
567         }
568 
569         // No child view at this coordinate.
570         return null;
571     }
572 
573     /**
574      * Get the last Y coordinate on this grid where the last touch was made
575      */
getLastTouchY()576     public float getLastTouchY() {
577         return mLastTouchY;
578     }
579 
580     /**
581      * Enable drag reordering of child items.
582      */
enableDragReordering()583     public void enableDragReordering() {
584         mIsDragReorderingEnabled = true;
585     }
586 
587     /**
588      * Disable drag reordering of child items.
589      */
disableDragReordering()590     public void disableDragReordering() {
591         mIsDragReorderingEnabled = false;
592     }
593 
594     /**
595      * Check to see if drag reordering is supported.  The switch must be flipped to true, and there
596      * must be a {@link ReorderListener} registered to listen for reordering events.
597      *
598      * @return boolean indicating whether drag reordering is currently supported.
599      */
isDragReorderingSupported()600     private boolean isDragReorderingSupported() {
601         return mIsDragReorderingEnabled && mReorderHelper != null &&
602                 mReorderHelper.hasReorderListener();
603     }
604 
605     /**
606      * Calculate bounds to assist in scrolling during a drag
607      * @param y The y coordinate of the current drag.
608      */
initializeDragScrollParameters(int y)609     private void initializeDragScrollParameters(int y) {
610         // Calculate the upper and lower bound of the screen to support drag scrolling
611         mHeight = getHeight();
612         mUpperScrollBound = Math.min(y - mTouchSlop, mHeight / 5);
613         mLowerScrollBound = Math.max(y + mTouchSlop, mHeight * 4 / 5);
614     }
615 
616     /**
617      * Initiate the dragging process. Create a bitmap that is displayed as the dragging event
618      * happens and is moved around across the screen.  This function is called once for each time
619      * that a dragging event is initiated.
620      *
621      * The logic to this method was borrowed from the TouchInterceptor.java class from the
622      * music app.
623      *
624      * @param draggedChild The child view being dragged
625      * @param x The x coordinate of this view where dragging began
626      * @param y The y coordinate of this view where dragging began
627      */
startDragging(final View draggedChild, final int x, final int y)628     private void startDragging(final View draggedChild, final int x, final int y) {
629         if (!isDragReorderingSupported()) {
630             return;
631         }
632 
633         mDragBitmap = createDraggedChildBitmap(draggedChild);
634         if (mDragBitmap == null) {
635             // It appears that creating bitmaps for large views fail. For now, don't allow
636             // dragging in this scenario.  When using the framework's drag and drop implementation,
637             // drag shadow also fails with a OutofResourceException when trying to draw the drag
638             // shadow onto a Surface.
639             mReorderHelper.handleDragCancelled(draggedChild);
640             return;
641         }
642         mTouchOffsetToChildLeft = x - draggedChild.getLeft();
643         mTouchOffsetToChildTop = y - draggedChild.getTop();
644         updateReorderStates(ReorderUtils.DRAG_STATE_DRAGGING);
645 
646         initializeDragScrollParameters(y);
647 
648         final LayoutParams params = (LayoutParams) draggedChild.getLayoutParams();
649         mReorderHelper.handleDragStart(draggedChild, params.position, params.id,
650                 new Point(mTouchDownForDragStartX, mTouchDownForDragStartY));
651 
652         // TODO: Reconsider using the framework's DragShadow support for dragging,
653         // and only draw the bitmap in onDrop for animation.
654         final Context context = getContext();
655         mDragView = new ImageView(context);
656         mDragView.setImageBitmap(mDragBitmap);
657         mDragView.setAlpha(160);
658 
659         mWindowParams = new WindowManager.LayoutParams();
660         mWindowParams.gravity = Gravity.TOP | Gravity.START;
661 
662         mWindowParams.height = WindowManager.LayoutParams.WRAP_CONTENT;
663         mWindowParams.width = WindowManager.LayoutParams.WRAP_CONTENT;
664         mWindowParams.flags = mWindowManagerLayoutFlags;
665         mWindowParams.format = PixelFormat.TRANSLUCENT;
666         // Use WindowManager to overlay a transparent image on drag
667         mWindowManager.addView(mDragView, mWindowParams);
668         updateDraggedBitmapLocation(x, y);
669     }
670 
createDraggedChildBitmap(View view)671     private Bitmap createDraggedChildBitmap(View view) {
672         view.setDrawingCacheEnabled(true);
673         final Bitmap cache = view.getDrawingCache();
674 
675         Bitmap bitmap = null;
676         if (cache != null) {
677             try {
678                 bitmap = cache.copy(Bitmap.Config.ARGB_8888, false);
679             } catch (final OutOfMemoryError e) {
680                 Log.w(TAG, "Failed to copy bitmap from Drawing cache", e);
681                 bitmap = null;
682             }
683         }
684 
685         view.destroyDrawingCache();
686         view.setDrawingCacheEnabled(false);
687 
688         return bitmap;
689     }
690 
691     /**
692      * Updates the current drag state and the UI appropriately.
693      * @param state the new drag state to update to.
694      */
updateReorderStates(int state)695     private void updateReorderStates(int state) throws IllegalStateException {
696         boolean resetDraggedChildView = false;
697         boolean resetDragProperties = false;
698 
699         mDragState = state;
700 
701         switch (state) {
702             case ReorderUtils.DRAG_STATE_NONE:
703             case ReorderUtils.DRAG_STATE_DRAGGING:
704                 // reset all states when a drag is complete or when we're starting a new drag.
705                 resetDraggedChildView = true;
706                 resetDragProperties = true;
707                 break;
708 
709             case ReorderUtils.DRAG_STATE_RELEASED_REORDER:
710                 // In a release over a valid reordering zone, don't reset any UI.  Let
711                 // LayoutChildren() take care of doing the appropriate animation
712                 // based on the result
713                 break;
714 
715             case ReorderUtils.DRAG_STATE_RELEASED_HOVER:
716                 // When a dragged child is released over another child, the dragged child will
717                 // remain hidden.  It is up to the ReorderListener to refresh the UI state
718                 // of the child if it does not handle the drop.
719                 resetDragProperties = true;
720                 break;
721 
722             default:
723                 throw new IllegalStateException("Illegal drag state: " + mDragState);
724         }
725 
726         if (resetDraggedChildView && mReorderHelper.getDraggedChild() != null) {
727             // DraggedChildId and mCachedDragViewRect need to stay around longer than
728             // the other properties because on the next data change, as we lay out, we'll need
729             // mCachedDragViewRect to position the view's animation start position, and
730             // draggedChildId to check if the current was the dragged view.
731             // For the other properties - DraggedOverChildView, DraggedChildView, etc.,
732             // as soon as drag is released, we can reset them because they have no impact on the
733             // next layout pass.
734             mReorderHelper.clearDraggedChildId();
735             mCachedDragViewRect = null;
736         }
737 
738         if (resetDragProperties) {
739             if (mDragView != null) {
740                 mDragView.setVisibility(INVISIBLE);
741                 mWindowManager.removeView(mDragView);
742                 mDragView.setImageDrawable(null);
743                 mDragView = null;
744 
745                 if (mDragBitmap != null) {
746                     mDragBitmap.recycle();
747                     mDragBitmap = null;
748                 }
749             }
750 
751             // We don't reset DraggedChildId here because it may still be in used.
752             // Let LayoutChildren reset it when it's done with it.
753             mReorderHelper.clearDraggedChild();
754             mReorderHelper.clearDraggedOverChild();
755         }
756     }
757 
758     /**
759      * Redraw the dragged child's bitmap based on the new coordinates.  If the reordering direction
760      * is {@link ReorderUtils#REORDER_DIRECTION_VERTICAL}, then ignore the x coordinate, as
761      * only vertical movement is allowed.  Similarly, if reordering direction is
762      * {@link ReorderUtils#REORDER_DIRECTION_HORIZONTAL}.  Even though this class does not manage
763      * drag shadow directly, we need to make sure we position the dragged bitmap at where the
764      * drag shadow is so that when drag ends, we can swap the shadow and the bitmap to animate
765      * the view into place.
766      * @param x The updated x coordinate of the drag shadow.
767      * @param y THe updated y coordinate of the drag shadow.
768      */
updateDraggedBitmapLocation(int x, int y)769     private void updateDraggedBitmapLocation(int x, int y) {
770         final int direction = mAdapter.getReorderingDirection();
771         if ((direction & ReorderUtils.REORDER_DIRECTION_HORIZONTAL) ==
772                 ReorderUtils.REORDER_DIRECTION_HORIZONTAL) {
773             if (mDragBitmap != null && mDragBitmap.getWidth() > getWidth()) {
774                 // If the bitmap is wider than the width of the screen, then some parts of the view
775                 // are off screen.  In this case, just set the drag shadow to start at x = 0
776                 // (adjusted to the absolute position on screen) so that at least the beginning of
777                 // the drag shadow is guaranteed to be within view.
778                 mWindowParams.x = mOffsetToAbsoluteX;
779             } else {
780                 // WindowParams is RTL agnostic and operates on raw coordinates.  So in an RTL
781                 // layout, we would still want to find the view's left coordinate for the
782                 // drag shadow, rather than the view's start.
783                 mWindowParams.x = x - mTouchOffsetToChildLeft + mOffsetToAbsoluteX;
784             }
785         } else {
786             mWindowParams.x = mOffsetToAbsoluteX;
787         }
788 
789         if ((direction & ReorderUtils.REORDER_DIRECTION_VERTICAL) ==
790                 ReorderUtils.REORDER_DIRECTION_VERTICAL) {
791             mWindowParams.y = y - mTouchOffsetToChildTop + mOffsetToAbsoluteY;
792         } else {
793             mWindowParams.y = mOffsetToAbsoluteY;
794         }
795 
796         mWindowManager.updateViewLayout(mDragView, mWindowParams);
797     }
798 
799     /**
800      * Update the visual state of the drag event based on the current drag location.  If the user
801      * has attempted to re-order by dragging a child over another child's drop zone, call the
802      * appropriate {@link ReorderListener} callback.
803      *
804      * @param x The current x coordinate of the drag event
805      * @param y The current y coordinate of the drag event
806      */
handleDrag(int x, int y)807     private void handleDrag(int x, int y) {
808         if (mDragState != ReorderUtils.DRAG_STATE_DRAGGING) {
809             return;
810         }
811 
812         // TODO: Consider moving drag shadow management logic into mReorderHelper as well, or
813         // scrap the custom logic and use the framework's drag-and-drop support now that we're not
814         // doing anything special to the drag shadow.
815         updateDraggedBitmapLocation(x, y);
816 
817         if (mCurrentRunningAnimatorSet == null) {
818             // If the current animator set is not null, then animation is running, in which case,
819             // we shouldn't do any reordering processing, as views will be moving around, and
820             // interfering with drag target calculations.
821             mReorderHelper.handleDrag(new Point(x, y));
822         }
823     }
824 
825     /**
826      * Check if a view is reorderable.
827      * @param i the child index in view group
828      */
isChildReorderable(int i)829     public boolean isChildReorderable(int i) {
830         return mAdapter.isDraggable(mFirstPosition + i);
831     }
832 
833     /**
834      * Handle the the release of a dragged view.
835      * @param x The current x coordinate where the drag was released.
836      * @param y The current y coordinate where the drag was released.
837      */
handleDrop(int x, int y)838     private void handleDrop(int x, int y) {
839         if (!mReorderHelper.hasReorderListener()) {
840             updateReorderStates(ReorderUtils.DRAG_STATE_NONE);
841             return;
842         }
843 
844         if (mReorderHelper.isOverReorderingArea()) {
845             // Store the location of the drag shadow at where dragging stopped
846             // for animation if a reordering has just happened. Since the drag
847             // shadow is drawn as a WindowManager view, its coordinates are
848             // absolute. However, for views inside the grid, we need to operate
849             // with coordinate values that's relative to this grid, so we need
850             // to subtract the offset to absolute screen coordinates that have
851             // been added to mWindowParams.
852             final int left = mWindowParams.x - mOffsetToAbsoluteX;
853             final int top = mWindowParams.y - mOffsetToAbsoluteY;
854 
855             mCachedDragViewRect = new Rect(
856                     left, top, left + mDragView.getWidth(), top + mDragView.getHeight());
857             if (getChildCount() > 0) {
858                 final View view = getChildAt(0);
859                 final LayoutParams lp = (LayoutParams) view.getLayoutParams();
860                 if (lp.position > mReorderHelper.getDraggedChildPosition()) {
861                     // If the adapter position of the first child in view is
862                     // greater than the position of the original dragged child,
863                     // this means that the user has scrolled the child out of
864                     // view. Those off screen views would have been recycled. If
865                     // mFirstPosition is currently x, after the reordering
866                     // operation, the child[mFirstPosition] will be
867                     // at mFirstPosition-1. We want to adjust mFirstPosition so
868                     // that we render the view in the correct location after
869                     // reordering completes.
870                     //
871                     // If the user has not scrolled the original dragged child
872                     // out of view, then the view has not been recycled and is
873                     // still in view.
874                     // When onLayout() gets called, we'll automatically fill in
875                     // the empty space that the child leaves behind from the
876                     // reordering operation.
877                     mFirstPosition--;
878                 }
879             }
880 
881             // Get the current scroll position so that after reordering
882             // completes, we can restore the scroll position of mFirstPosition.
883             mCurrentScrollState = getScrollState();
884         }
885 
886         final boolean reordered = mReorderHelper.handleDrop(new Point(x, y));
887         if (reordered) {
888             updateReorderStates(ReorderUtils.DRAG_STATE_RELEASED_REORDER);
889         } else {
890             updateReorderStates(ReorderUtils.DRAG_STATE_NONE);
891         }
892     }
893 
894     @Override
onInterceptTouchEvent(MotionEvent ev)895     public boolean onInterceptTouchEvent(MotionEvent ev) {
896         mVelocityTracker.addMovement(ev);
897         final int action = ev.getAction() & MotionEventCompat.ACTION_MASK;
898         switch (action) {
899             case MotionEvent.ACTION_DOWN: {
900                 mOffsetToAbsoluteX = (int)(ev.getRawX() - ev.getX());
901                 mOffsetToAbsoluteY = (int)(ev.getRawY() - ev.getY());
902 
903                 // Per bug 7377413, event.getX() and getY() returns rawX and rawY when accessed in
904                 // dispatchDragEvent, so since an action down is required before a drag can be
905                 // initiated, initialize mTouchDownForDragStartX/Y here for the most accurate value.
906                 mTouchDownForDragStartX = (int) ev.getX();
907                 mTouchDownForDragStartY = (int) ev.getY();
908 
909                 mVelocityTracker.clear();
910                 mScroller.abortAnimation();
911                 mLastTouchY = ev.getY();
912                 mActivePointerId = MotionEventCompat.getPointerId(ev, 0);
913                 mTouchRemainderY = 0;
914                 if (mTouchMode == TOUCH_MODE_FLINGING) {
915                     // Catch!
916                     mTouchMode = TOUCH_MODE_DRAGGING;
917                     return true;
918                 }
919                 break;
920             }
921             case MotionEvent.ACTION_MOVE: {
922                 final int index = MotionEventCompat.findPointerIndex(ev, mActivePointerId);
923                 if (index < 0) {
924                     Log.e(TAG, "onInterceptTouchEvent could not find pointer with id " +
925                             mActivePointerId + " - did StaggeredGridView receive an inconsistent " +
926                             "event stream?");
927                     return false;
928                 }
929                 final float y = MotionEventCompat.getY(ev, index);
930                 final float dy = y - mLastTouchY + mTouchRemainderY;
931                 final int deltaY = (int) dy;
932                 mTouchRemainderY = dy - deltaY;
933 
934                 if (Math.abs(dy) > mTouchSlop) {
935                     mTouchMode = TOUCH_MODE_DRAGGING;
936                     return true;
937                 }
938             }
939         }
940 
941         return false;
942     }
943 
944     @Override
onTouchEvent(MotionEvent ev)945     public boolean onTouchEvent(MotionEvent ev) {
946         mVelocityTracker.addMovement(ev);
947         final int action = ev.getAction() & MotionEventCompat.ACTION_MASK;
948         switch (action) {
949             case MotionEvent.ACTION_DOWN:
950                 resetScroller();
951                 mVelocityTracker.clear();
952                 mScroller.abortAnimation();
953                 mLastTouchY = ev.getY();
954                 mActivePointerId = MotionEventCompat.getPointerId(ev, 0);
955                 mTouchRemainderY = 0;
956                 break;
957 
958             case MotionEvent.ACTION_MOVE: {
959                 final int index = MotionEventCompat.findPointerIndex(ev, mActivePointerId);
960                 if (index < 0) {
961                     Log.e(TAG, "onInterceptTouchEvent could not find pointer with id " +
962                             mActivePointerId + " - did StaggeredGridView receive an inconsistent " +
963                             "event stream?");
964                     return false;
965                 }
966 
967                 final float y = MotionEventCompat.getY(ev, index);
968                 final float dy = y - mLastTouchY + mTouchRemainderY;
969                 final int deltaY = (int) dy;
970                 mTouchRemainderY = dy - deltaY;
971 
972                 if (Math.abs(dy) > mTouchSlop) {
973                     mTouchMode = TOUCH_MODE_DRAGGING;
974                 }
975 
976                 if (mTouchMode == TOUCH_MODE_DRAGGING) {
977                     mLastTouchY = y;
978                     if (!trackMotionScroll(deltaY, true)) {
979                         // Break fling velocity if we impacted an edge.
980                         mVelocityTracker.clear();
981                     }
982                 }
983                 break;
984             }
985 
986             case MotionEvent.ACTION_CANCEL: {
987                 mTouchMode = TOUCH_MODE_IDLE;
988                 break;
989             }
990 
991             case MotionEvent.ACTION_UP: {
992                 mVelocityTracker.computeCurrentVelocity(1000, mMaximumVelocity);
993                 final float velocity = VelocityTrackerCompat.getYVelocity(mVelocityTracker,
994                         mActivePointerId);
995                 if (Math.abs(velocity) > mFlingVelocity) {
996                     mTouchMode = TOUCH_MODE_FLINGING;
997                     resetScroller();
998                     mScroller.fling(0, 0, 0, (int) velocity, 0, 0,
999                             Integer.MIN_VALUE, Integer.MAX_VALUE);
1000                     mLastTouchY = 0;
1001                     ViewCompat.postInvalidateOnAnimation(this);
1002                 } else {
1003                     mTouchMode = TOUCH_MODE_IDLE;
1004                 }
1005             }
1006             break;
1007         }
1008 
1009         return true;
1010     }
1011 
resetScroller()1012     private void resetScroller() {
1013         mTouchMode = TOUCH_MODE_IDLE;
1014         mTopEdge.finish();
1015         mBottomEdge.finish();
1016         mScroller.abortAnimation();
1017     }
1018 
1019     @Override
dispatchDragEvent(DragEvent event)1020     public boolean dispatchDragEvent(DragEvent event) {
1021         if (!isDragReorderingSupported()) {
1022             // If the consumer of this StaggeredGridView has not registered a ReorderListener,
1023             // don't bother handling drag events.
1024             return super.dispatchDragEvent(event);
1025         }
1026 
1027         switch(event.getAction()) {
1028             case DragEvent.ACTION_DRAG_STARTED:
1029                 // Per bug 7071594, we won't be able to catch this event in onDragEvent,
1030                 // so we'll handle the event as it is being dispatched on the way down.
1031                 if (mReorderHelper.hasReorderListener() && mIsDragReorderingEnabled) {
1032                     final View child = getChildAtCoordinate(
1033                             mTouchDownForDragStartX, mTouchDownForDragStartY);
1034                     if (child != null) {
1035                         // Child can be null if the touch point is not on a child view, but is
1036                         // still within the bounds of this StaggeredGridView (i.e., margins
1037                         // between cells).
1038                         startDragging(child, mTouchDownForDragStartX, mTouchDownForDragStartY);
1039                         // We must return true in order to continue getting future
1040                         // {@link DragEvent}s.
1041                         return true;
1042                     }
1043                 }
1044                 // Be sure to return a value here instead of calling super.dispatchDragEvent()
1045                 // which will unnecessarily dispatch to all the children (since the
1046                 // {@link StaggeredGridView} handles all drag events for our purposes)
1047                 return false;
1048 
1049             case DragEvent.ACTION_DROP:
1050             case DragEvent.ACTION_DRAG_ENDED:
1051                 if (mDragState == ReorderUtils.DRAG_STATE_DRAGGING) {
1052                     handleDrop((int)event.getX(), (int)event.getY());
1053                 }
1054 
1055                 // Return early here to avoid calling super.dispatchDragEvent() which dispatches to
1056                 // children (since this view already can handle all drag events). The super call
1057                 // can also cause a NPE if the view hierarchy changed in the middle of a drag
1058                 // and the {@link DragEvent} gets nulled out. This is a workaround for
1059                 // a framework bug: 8298439.
1060                 // Since the {@link StaggeredGridView} handles all drag events for our purposes,
1061                 // just manually fire the drag event to ourselves.
1062                 return onDragEvent(event);
1063         }
1064 
1065         // In all other cases, default to the superclass implementation. We need this so that
1066         // the drag/drop framework will fire off {@link #onDragEvent(DragEvent ev)} calls to us.
1067         return super.dispatchDragEvent(event);
1068     }
1069 
1070     @Override
onDragEvent(DragEvent ev)1071     public boolean onDragEvent(DragEvent ev) {
1072         if (!isDragReorderingSupported()) {
1073             // If the consumer of this StaggeredGridView has not registered a ReorderListener,
1074             // don't bother handling drag events.
1075             return false;
1076         }
1077 
1078         final int x = (int)ev.getX();
1079         final int y = (int)ev.getY();
1080 
1081         switch(ev.getAction()) {
1082             case DragEvent.ACTION_DRAG_LOCATION:
1083                 if (mDragState == ReorderUtils.DRAG_STATE_DRAGGING) {
1084                     handleDrag(x, y);
1085                     mLastTouchY = y;
1086                 }
1087 
1088                 // Kick off the scroll handler on the first drag location event,
1089                 // if it's not already running
1090                 if (!mIsDragScrollerRunning &&
1091                         // And if the distance traveled while dragging exceeds the touch slop
1092                         ((Math.abs(x - mTouchDownForDragStartX) >= 4 * mTouchSlop) ||
1093                         (Math.abs(y - mTouchDownForDragStartY) >= 4 * mTouchSlop))) {
1094                     // Set true because that the scroller is running now
1095                     mIsDragScrollerRunning = true;
1096 
1097                     if (mScrollHandler == null) {
1098                         mScrollHandler = getHandler();
1099                     }
1100                     mScrollHandler.postDelayed(mDragScroller, SCROLL_HANDLER_DELAY);
1101                 }
1102 
1103                 return true;
1104 
1105             case DragEvent.ACTION_DROP:
1106             case DragEvent.ACTION_DRAG_ENDED:
1107                 // We can either expect to receive:
1108                 // 1. Both {@link DragEvent#ACTION_DROP} and then
1109                 //    {@link DragEvent#ACTION_DRAG_ENDED} if the drop is over this view.
1110                 // 2. Only {@link DragEvent#ACTION_DRAG_ENDED} if the drop happened over a
1111                 //    different view.
1112                 // For this reason, we should always handle the drop. In case #1, if this code path
1113                 // gets executed again then nothing will happen because we will have already
1114                 // updated {@link #mDragState} to not be {@link ReorderUtils#DRAG_STATE_DRAGGING}.
1115                 if (mScrollHandler != null) {
1116                     mScrollHandler.removeCallbacks(mDragScroller);
1117                     // Scroller is no longer running
1118                     mIsDragScrollerRunning = false;
1119                 }
1120 
1121                 return true;
1122             }
1123 
1124         return false;
1125     }
1126 
1127     /**
1128      *
1129      * @param deltaY Pixels that content should move by
1130      * @return true if the movement completed, false if it was stopped prematurely.
1131      */
trackMotionScroll(int deltaY, boolean allowOverScroll)1132     private boolean trackMotionScroll(int deltaY, boolean allowOverScroll) {
1133         final boolean contentFits = contentFits();
1134         final int allowOverhang = Math.abs(deltaY);
1135         final int overScrolledBy;
1136         final int movedBy;
1137         if (!contentFits) {
1138             int overhang;
1139             final boolean up;
1140             mPopulating = true;
1141             if (deltaY > 0) {
1142                 overhang = fillUp(mFirstPosition - 1, allowOverhang);
1143                 up = true;
1144             } else {
1145                 overhang = fillDown(mFirstPosition + getChildCount(), allowOverhang);
1146 
1147                 if (overhang < 0) {
1148                     // Overhang when filling down indicates how many pixels past the bottom of the
1149                     // screen has been filled in.  If this value is negative, it should be set to
1150                     // 0 so that we don't allow over scrolling.
1151                     overhang = 0;
1152                 }
1153 
1154                 up = false;
1155             }
1156 
1157             movedBy = Math.min(overhang, allowOverhang);
1158             offsetChildren(up ? movedBy : -movedBy);
1159             recycleOffscreenViews();
1160             mPopulating = false;
1161             overScrolledBy = allowOverhang - overhang;
1162         } else {
1163             overScrolledBy = allowOverhang;
1164             movedBy = 0;
1165         }
1166 
1167         if (allowOverScroll) {
1168             final int overScrollMode = ViewCompat.getOverScrollMode(this);
1169 
1170             if (overScrollMode == ViewCompat.OVER_SCROLL_ALWAYS ||
1171                     (overScrollMode == ViewCompat.OVER_SCROLL_IF_CONTENT_SCROLLS && !contentFits)) {
1172 
1173                 if (overScrolledBy > 0) {
1174                     final EdgeEffectCompat edge = deltaY > 0 ? mTopEdge : mBottomEdge;
1175                     edge.onPull((float) Math.abs(deltaY) / getHeight());
1176                     ViewCompat.postInvalidateOnAnimation(this);
1177                 }
1178             }
1179         }
1180 
1181         awakenScrollBars(0 /* show immediately */, true /* invalidate */);
1182         return deltaY == 0 || movedBy != 0;
1183     }
1184 
contentFits()1185     public final boolean contentFits() {
1186         if (mFirstPosition != 0 || getChildCount() != mItemCount) {
1187             return false;
1188         }
1189 
1190         int topmost = Integer.MAX_VALUE;
1191         int bottommost = Integer.MIN_VALUE;
1192         for (int i = 0; i < mColCount; i++) {
1193             if (mItemTops[i] < topmost) {
1194                 topmost = mItemTops[i];
1195             }
1196             if (mItemBottoms[i] > bottommost) {
1197                 bottommost = mItemBottoms[i];
1198             }
1199         }
1200 
1201         return topmost >= getPaddingTop() && bottommost <= getHeight() - getPaddingBottom();
1202     }
1203 
1204     /**
1205      * Recycle views within the range starting from startIndex (inclusive) until the last
1206      * attached child view.
1207      */
recycleViewsInRange(int startIndex, int endIndex)1208     private void recycleViewsInRange(int startIndex, int endIndex) {
1209         for (int i = endIndex; i >= startIndex; i--) {
1210             final View child = getChildAt(i);
1211 
1212             if (mInLayout) {
1213                 removeViewsInLayout(i, 1);
1214             } else {
1215                 removeViewAt(i);
1216             }
1217 
1218             mRecycler.addScrap(child);
1219         }
1220     }
1221 
1222     // TODO: Have other overloaded recycle methods call into this one so we would just have one
1223     // code path.
recycleView(View view)1224     private void recycleView(View view) {
1225         if (view == null) {
1226             return;
1227         }
1228 
1229         if (mInLayout) {
1230             removeViewInLayout(view);
1231             invalidate();
1232         } else {
1233             removeView(view);
1234         }
1235 
1236         mRecycler.addScrap(view);
1237     }
1238 
1239     /**
1240      * Important: this method will leave offscreen views attached if they
1241      * are required to maintain the invariant that child view with index i
1242      * is always the view corresponding to position mFirstPosition + i.
1243      */
recycleOffscreenViews()1244     private void recycleOffscreenViews() {
1245         if (getChildCount() == 0) {
1246             return;
1247         }
1248 
1249         final int height = getHeight();
1250         final int clearAbove = -mItemMargin;
1251         final int clearBelow = height + mItemMargin;
1252         for (int i = getChildCount() - 1; i >= 0; i--) {
1253             final View child = getChildAt(i);
1254             if (child.getTop() <= clearBelow)  {
1255                 // There may be other offscreen views, but we need to maintain
1256                 // the invariant documented above.
1257                 break;
1258             }
1259 
1260             child.clearFocus();
1261             if (mInLayout) {
1262                 removeViewsInLayout(i, 1);
1263             } else {
1264                 removeViewAt(i);
1265             }
1266 
1267             mRecycler.addScrap(child);
1268         }
1269 
1270         while (getChildCount() > 0) {
1271             final View child = getChildAt(0);
1272             if (child.getBottom() >= clearAbove) {
1273                 // There may be other offscreen views, but we need to maintain
1274                 // the invariant documented above.
1275                 break;
1276             }
1277 
1278             child.clearFocus();
1279             if (mInLayout) {
1280                 removeViewsInLayout(0, 1);
1281             } else {
1282                 removeViewAt(0);
1283             }
1284 
1285             mRecycler.addScrap(child);
1286             mFirstPosition++;
1287         }
1288 
1289         final int childCount = getChildCount();
1290         if (childCount > 0) {
1291             // Repair the top and bottom column boundaries from the views we still have
1292             Arrays.fill(mItemTops, Integer.MAX_VALUE);
1293             Arrays.fill(mItemBottoms, Integer.MIN_VALUE);
1294             for (int i = 0; i < childCount; i++){
1295                 final View child = getChildAt(i);
1296                 final LayoutParams lp = (LayoutParams) child.getLayoutParams();
1297                 final int top = child.getTop() - mItemMargin;
1298                 final int bottom = child.getBottom();
1299                 LayoutRecord rec = mLayoutRecords.get(mFirstPosition + i);
1300 
1301                 // It's possible the layout record could be null for visible views because
1302                 // they are cleared between adapter data set changes, but the views are left
1303                 // attached for the purpose of animations. Hence, populate the layout record again.
1304                 if (rec == null) {
1305                     rec = recreateLayoutRecord(mFirstPosition + i, child, lp);
1306                 }
1307 
1308                 // In LTR layout, iterate across each column that this child is laid out in,
1309                 // starting from the child's first column (lp.column).  For each column, update
1310                 // mItemTops and mItemBottoms appropriately to take into account this child's
1311                 // dimension.  In RTL layout, iterate in reverse, where the child's starting
1312                 // column would start from the right-most.
1313                 final int span = Math.min(mColCount, lp.span);
1314                 for (int spanIndex = 0; spanIndex < span; spanIndex++) {
1315                     final int col = mIsRtlLayout ? lp.column - spanIndex :
1316                             lp.column + spanIndex;
1317                     final int colTop = top - rec.getMarginAbove(spanIndex);
1318                     final int colBottom = bottom + rec.getMarginBelow(spanIndex);
1319                     if (colTop < mItemTops[col]) {
1320                         mItemTops[col] = colTop;
1321                     }
1322                     if (colBottom > mItemBottoms[col]) {
1323                         mItemBottoms[col] = colBottom;
1324                     }
1325                 }
1326             }
1327 
1328             for (int col = 0; col < mColCount; col++) {
1329                 if (mItemTops[col] == Integer.MAX_VALUE) {
1330                     // If one was untouched, both were.
1331                     final int top = getPaddingTop();
1332                     mItemTops[col] = top;
1333                     mItemBottoms[col] = top;
1334                 }
1335             }
1336         }
1337 
1338         mCurrentScrollState = getScrollState();
1339     }
1340 
recreateLayoutRecord(int position, View child, LayoutParams lp)1341     private LayoutRecord recreateLayoutRecord(int position, View child, LayoutParams lp) {
1342         final LayoutRecord rec = new LayoutRecord();
1343         mLayoutRecords.put(position, rec);
1344         rec.column = lp.column;
1345         rec.height = child.getHeight();
1346         rec.id = lp.id;
1347         rec.span = Math.min(mColCount, lp.span);
1348         return rec;
1349     }
1350 
1351     @Override
computeScroll()1352     public void computeScroll() {
1353         if (mTouchMode == TOUCH_MODE_OVERFLING) {
1354             handleOverfling();
1355         } else if (mScroller.computeScrollOffset()) {
1356             final int overScrollMode = ViewCompat.getOverScrollMode(this);
1357             final boolean supportsOverscroll = overScrollMode != ViewCompat.OVER_SCROLL_NEVER;
1358             final int y = mScroller.getCurrY();
1359             final int dy = (int) (y - mLastTouchY);
1360             // TODO: Figure out why mLastTouchY is being updated here. Consider using a new class
1361             // variable since this value does not represent the last place on the screen where a
1362             // touch occurred.
1363             mLastTouchY = y;
1364             // Check if the top of the motion view is where it is
1365             // supposed to be
1366             final View motionView = supportsOverscroll &&
1367                     getChildCount() > 0 ? getChildAt(0) : null;
1368             final int motionViewPrevTop = motionView != null ? motionView.getTop() : 0;
1369             final boolean stopped = !trackMotionScroll(dy, false);
1370             if (!stopped && !mScroller.isFinished()) {
1371                 mTouchMode = TOUCH_MODE_IDLE;
1372                 ViewCompat.postInvalidateOnAnimation(this);
1373             } else if (stopped && dy != 0 && supportsOverscroll) {
1374                     // Check to see if we have bumped into the scroll limit
1375                     if (motionView != null) {
1376                         final int motionViewRealTop = motionView.getTop();
1377                         // Apply overscroll
1378                         final int overscroll = -dy - (motionViewRealTop - motionViewPrevTop);
1379                         overScrollBy(0, overscroll, 0, getScrollY(), 0, 0, 0, mOverscrollDistance,
1380                                 true);
1381                     }
1382                     final EdgeEffectCompat edge;
1383                     if (dy > 0) {
1384                         edge = mTopEdge;
1385                         mBottomEdge.finish();
1386                     } else {
1387                         edge = mBottomEdge;
1388                         mTopEdge.finish();
1389                     }
1390                     edge.onAbsorb(Math.abs((int) mScroller.getCurrVelocity()));
1391                     if (mScroller.computeScrollOffset()) {
1392                         mScroller.notifyVerticalEdgeReached(getScrollY(), 0, mOverscrollDistance);
1393                     }
1394                     mTouchMode = TOUCH_MODE_OVERFLING;
1395                     ViewCompat.postInvalidateOnAnimation(this);
1396             } else {
1397                 mTouchMode = TOUCH_MODE_IDLE;
1398             }
1399         }
1400     }
1401 
handleOverfling()1402     private void handleOverfling() {
1403         // If the animation is not finished yet, determine next steps.
1404         if (mScroller.computeScrollOffset()) {
1405             final int scrollY = getScrollY();
1406             final int currY = mScroller.getCurrY();
1407             final int deltaY = currY - scrollY;
1408             if (overScrollBy(0, deltaY, 0, scrollY, 0, 0, 0, mOverscrollDistance, false)) {
1409                 final boolean crossDown = scrollY <= 0 && currY > 0;
1410                 final boolean crossUp = scrollY >= 0 && currY < 0;
1411                 if (crossDown || crossUp) {
1412                     int velocity = (int) mScroller.getCurrVelocity();
1413                     if (crossUp) {
1414                         velocity = -velocity;
1415                     }
1416 
1417                     // Don't flywheel from this; we're just continuing
1418                     // things.
1419                     mTouchMode = TOUCH_MODE_IDLE;
1420                     mScroller.abortAnimation();
1421                 } else {
1422                     // Spring back! We are done overscrolling.
1423                     if (mScroller.springBack(0, scrollY, 0, 0, 0, 0)) {
1424                         mTouchMode = TOUCH_MODE_OVERFLING;
1425                         ViewCompat.postInvalidateOnAnimation(this);
1426                     } else {
1427                         // If already valid, we are done. Exit overfling mode.
1428                         mTouchMode = TOUCH_MODE_IDLE;
1429                     }
1430                 }
1431             } else {
1432                 // Still over-flinging; just post the next frame of the animation.
1433                 ViewCompat.postInvalidateOnAnimation(this);
1434             }
1435         } else {
1436             // Otherwise, exit overfling mode.
1437             mTouchMode = TOUCH_MODE_IDLE;
1438             mScroller.abortAnimation();
1439         }
1440     }
1441 
1442     @Override
1443     protected void onOverScrolled(int scrollX, int scrollY, boolean clampedX, boolean clampedY) {
1444         if (getScrollY() != scrollY) {
1445             scrollTo(0, scrollY);
1446         }
1447     }
1448 
1449     @Override
1450     public void draw(Canvas canvas) {
1451         super.draw(canvas);
1452 
1453         if (mTopEdge != null) {
1454             boolean needsInvalidate = false;
1455             if (!mTopEdge.isFinished()) {
1456                 final int restoreCount = canvas.save();
1457                 canvas.translate(0, 0);
1458                 mTopEdge.draw(canvas);
1459                 canvas.restoreToCount(restoreCount);
1460                 needsInvalidate = true;
1461             }
1462             if (!mBottomEdge.isFinished()) {
1463                 final int restoreCount = canvas.save();
1464                 final int width = getWidth();
1465                 canvas.translate(-width, getHeight());
1466                 canvas.rotate(180, width, 0);
1467                 mBottomEdge.draw(canvas);
1468                 canvas.restoreToCount(restoreCount);
1469                 needsInvalidate = true;
1470             }
1471 
1472             if (needsInvalidate) {
1473                 ViewCompat.postInvalidateOnAnimation(this);
1474             }
1475         }
1476     }
1477 
1478     public void beginFastChildLayout() {
1479         mFastChildLayout = true;
1480     }
1481 
1482     public void endFastChildLayout() {
1483         mFastChildLayout = false;
1484         populate();
1485     }
1486 
1487     @Override
1488     public void requestLayout() {
1489         if (!mPopulating && !mFastChildLayout) {
1490             super.requestLayout();
1491         }
1492     }
1493 
1494     /**
1495      * Sets the view to show if the adapter is empty
1496      */
1497     public void setEmptyView(View emptyView) {
1498         mEmptyView = emptyView;
1499 
1500         updateEmptyStatus();
1501     }
1502 
1503     public View getEmptyView() {
1504         return mEmptyView;
1505     }
1506 
1507     /**
1508      * Update the status of the list based on the whether the adapter is empty.  If is it empty and
1509      * we have an empty view, display it.  In all the other cases, make sure that the
1510      * StaggeredGridView is VISIBLE and that the empty view is GONE (if it's not null).
1511      */
1512     private void updateEmptyStatus() {
1513         if (mAdapter == null || mAdapter.isEmpty()) {
1514             if (mEmptyView != null) {
1515                 mEmptyView.setVisibility(View.VISIBLE);
1516                 setVisibility(View.GONE);
1517             } else {
1518                 setVisibility(View.VISIBLE);
1519             }
1520         } else {
1521             if (mEmptyView != null) {
1522                 mEmptyView.setVisibility(View.GONE);
1523             }
1524             setVisibility(View.VISIBLE);
1525         }
1526     }
1527 
1528     @Override
1529     protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
1530         int widthMode = MeasureSpec.getMode(widthMeasureSpec);
1531         int heightMode = MeasureSpec.getMode(heightMeasureSpec);
1532         final int widthSize = MeasureSpec.getSize(widthMeasureSpec);
1533         final int heightSize = MeasureSpec.getSize(heightMeasureSpec);
1534 
1535         if (widthMode != MeasureSpec.EXACTLY) {
1536             Log.d(TAG, "onMeasure: must have an exact width or match_parent! " +
1537                     "Using fallback spec of EXACTLY " + widthSize);
1538             widthMode = MeasureSpec.EXACTLY;
1539         }
1540         if (heightMode != MeasureSpec.EXACTLY) {
1541             Log.d(TAG, "onMeasure: must have an exact height or match_parent! " +
1542                     "Using fallback spec of EXACTLY " + heightSize);
1543             heightMode = MeasureSpec.EXACTLY;
1544         }
1545 
1546         setMeasuredDimension(widthSize, heightSize);
1547 
1548         if (mColCountSetting == COLUMN_COUNT_AUTO) {
1549             final int colCount = widthSize / mMinColWidth;
1550             if (colCount != mColCount) {
1551                 mColCount = colCount;
1552             }
1553         }
1554 
1555         if (mHorizontalReorderingAreaSize == 0) {
1556             if (mColCount > 1) {
1557                 final int totalMarginWidth = mItemMargin * (mColCount + 1);
1558                 final int singleViewWidth = (widthSize - totalMarginWidth) / mColCount;
1559                 mHorizontalReorderingAreaSize = singleViewWidth / CHILD_TO_REORDER_AREA_RATIO;
1560             } else {
1561                 mHorizontalReorderingAreaSize = SINGLE_COL_REORDERING_AREA_SIZE;
1562             }
1563         }
1564     }
1565 
1566     @Override
1567     protected void onLayout(boolean changed, int l, int t, int r, int b) {
1568         mIsRtlLayout = isLayoutRtl();
1569 
1570         mInLayout = true;
1571         populate();
1572         mInLayout = false;
1573         final int width = r - l;
1574         final int height = b - t;
1575         mTopEdge.setSize(width, height);
1576         mBottomEdge.setSize(width, height);
1577     }
1578 
1579     private void populate() {
1580         if (getWidth() == 0 || getHeight() == 0 || mAdapter == null) {
1581             return;
1582         }
1583 
1584         if (mColCount == COLUMN_COUNT_AUTO) {
1585             final int colCount = getWidth() / mMinColWidth;
1586             if (colCount != mColCount) {
1587                 mColCount = colCount;
1588             }
1589         }
1590 
1591         final int colCount = mColCount;
1592         if (mItemTops == null || mItemBottoms == null || mItemTops.length != colCount ||
1593                 mItemBottoms.length != colCount) {
1594             mItemTops = new int[colCount];
1595             mItemBottoms = new int[colCount];
1596 
1597             mLayoutRecords.clear();
1598             if (mInLayout) {
1599                 removeAllViewsInLayout();
1600             } else {
1601                 removeAllViews();
1602             }
1603         }
1604 
1605         // Before we do layout, if there are any pending animations and data has changed,
1606         // cancel the animation, as layout on new data will likely trigger another animation
1607         // set to be run.
1608         if (mDataChanged && mCurrentRunningAnimatorSet != null) {
1609             mCurrentRunningAnimatorSet.cancel();
1610             mCurrentRunningAnimatorSet = null;
1611         }
1612 
1613         if (isSelectionAtTop()) {
1614             mCurrentScrollState = null;
1615         }
1616 
1617         if (mCurrentScrollState != null) {
1618             restoreScrollPosition(mCurrentScrollState);
1619         } else {
1620             calculateLayoutStartOffsets(getPaddingTop() /* layout start offset */);
1621         }
1622 
1623         mPopulating = true;
1624 
1625         mFocusedChildIdToScrollIntoView = -1;
1626         final View focusedChild = getFocusedChild();
1627         if (focusedChild != null) {
1628             final LayoutParams lp = (LayoutParams) focusedChild.getLayoutParams();
1629             mFocusedChildIdToScrollIntoView = lp.id;
1630         }
1631 
1632         layoutChildren(mDataChanged);
1633         fillDown(mFirstPosition + getChildCount(), 0);
1634         fillUp(mFirstPosition - 1, 0);
1635 
1636         if (isDragReorderingSupported() &&
1637                 mDragState == ReorderUtils.DRAG_STATE_RELEASED_REORDER ||
1638                 mDragState == ReorderUtils.DRAG_STATE_RELEASED_HOVER) {
1639             // This child was dragged and dropped with the UI likely
1640             // still showing.  Call updateReorderStates, to update
1641             // all UI appropriately.
1642             mReorderHelper.clearDraggedChildId();
1643             updateReorderStates(ReorderUtils.DRAG_STATE_NONE);
1644         }
1645 
1646         if (mDataChanged) {
1647             // Animation should only play if data has changed since populate() can be called
1648             // multiple times with the same data set (e.g., screen size changed).
1649             handleLayoutAnimation();
1650         }
1651 
1652         recycleOffscreenViews();
1653 
1654         mPopulating = false;
1655         mDataChanged = false;
1656     }
1657 
1658     @Override
1659     public void scrollBy(int x, int y) {
1660         if (y != 0) {
1661             // TODO: Implement smooth scrolling for this so that scrolling does more than just
1662             // jumping by y pixels.
1663             trackMotionScroll(y, false /* over scroll */);
1664         }
1665     }
1666 
1667     private void offsetChildren(int offset) {
1668         final int childCount = getChildCount();
1669         for (int i = 0; i < childCount; i++) {
1670             final View child = getChildAt(i);
1671 
1672             child.offsetTopAndBottom(offset);
1673 
1674             // As we're scrolling, we need to make sure the children that are coming into view
1675             // have their reordering area set.
1676             final LayoutParams lp = (LayoutParams) child.getLayoutParams();
1677             setReorderingArea(lp);
1678         }
1679 
1680         final int colCount = mColCount;
1681         for (int i = 0; i < colCount; i++) {
1682             mItemTops[i] += offset;
1683             mItemBottoms[i] += offset;
1684         }
1685 
1686         if (mScrollListener != null) {
1687             mScrollListener.onScrollChanged(offset, computeVerticalScrollOffset(),
1688                     computeVerticalScrollRange());
1689         }
1690     }
1691 
1692     /**
1693      * Performs layout animation of child views.
1694      * @throws IllegalStateException Exception is thrown of currently set animation mode is
1695      * not recognized.
1696      */
1697     private void handleLayoutAnimation() throws IllegalStateException {
1698         final List<Animator> animators = new ArrayList<Animator>();
1699 
1700         // b/8422632 - Without this dummy first animator, startDelays of subsequent animators won't
1701         // be honored correctly; all animators will block regardless of startDelay until the first
1702         // animator in the AnimatorSet truly starts playing.
1703         final ValueAnimator anim = ValueAnimator.ofFloat(0f, 1f);
1704         anim.setDuration(0);
1705         animators.add(anim);
1706 
1707         addOutAnimatorsForStaleViews(animators, mAnimationOutMode);
1708 
1709         // Play the In animators at a slight delay after all Out animators have started.
1710         final int animationInStartDelay = animators.size() > 0 ?
1711                 (SgvAnimationHelper.getDefaultAnimationDuration() / 2) : 0;
1712         addInAnimators(animators, mAnimationInMode, animationInStartDelay);
1713 
1714         if (animators != null && animators.size() > 0) {
1715             final AnimatorSet animatorSet = new AnimatorSet();
1716             animatorSet.playTogether(animators);
1717             animatorSet.addListener(new AnimatorListenerAdapter() {
1718                 @Override
1719                 public void onAnimationStart(Animator animation) {
1720                     mIsCurrentAnimationCanceled = false;
1721                     mCurrentRunningAnimatorSet = animatorSet;
1722                 }
1723 
1724                 @Override
1725                 public void onAnimationCancel(Animator animation) {
1726                     mIsCurrentAnimationCanceled = true;
1727                 }
1728 
1729                 @Override
1730                 public void onAnimationEnd(Animator animation) {
1731                     if (!mIsCurrentAnimationCanceled) {
1732                         // If this animation ended naturally, not because it was canceled, then
1733                         // reset the animation mode back to ANIMATION_MODE_NONE.  However, if
1734                         // the animation was canceled by a data change, then keep the mode as is,
1735                         // so that on a re-layout, we can resume animation from the views' current
1736                         // positions.
1737                         resetAnimationMode();
1738                     }
1739                     mCurrentRunningAnimatorSet = null;
1740                 }
1741             });
1742 
1743             Log.v(TAG, "starting");
1744             animatorSet.start();
1745         } else {
1746             resetAnimationMode();
1747         }
1748 
1749         mViewsToAnimateOut.clear();
1750         mChildRectsForAnimation.clear();
1751     }
1752 
1753     /**
1754      * Reset the current animation mode.
1755      */
1756     private void resetAnimationMode() {
1757         mAnimationInMode = AnimationIn.NONE;
1758         mAnimationOutMode = AnimationOut.NONE;
1759     }
1760 
1761     /**
1762      * Add animators for animating in new views as well as updating positions of views that
1763      * should remain on screen.
1764      */
1765     private void addInAnimators(List<Animator> animators, AnimationIn animationInMode,
1766             int startDelay) {
1767         if (animationInMode == AnimationIn.NONE) {
1768             return;
1769         }
1770 
1771         switch (animationInMode) {
1772             case FLY_UP_ALL_VIEWS:
1773                 addFlyInAllViewsAnimators(animators);
1774                 break;
1775 
1776             case EXPAND_NEW_VIEWS:
1777                 addUpdateViewPositionsAnimators(animators, true /* cascade animation */,
1778                         AnimationIn.EXPAND_NEW_VIEWS, startDelay);
1779                 break;
1780 
1781             case EXPAND_NEW_VIEWS_NO_CASCADE:
1782                 addUpdateViewPositionsAnimators(animators, false /* cascade animation */,
1783                         AnimationIn.EXPAND_NEW_VIEWS_NO_CASCADE, startDelay);
1784                 break;
1785 
1786             case SLIDE_IN_NEW_VIEWS:
1787                 addUpdateViewPositionsAnimators(animators, true /* cascade animation */,
1788                         AnimationIn.SLIDE_IN_NEW_VIEWS, startDelay);
1789                 break;
1790 
1791             case FLY_IN_NEW_VIEWS:
1792                 addUpdateViewPositionsAnimators(animators, true /* cascade animation */,
1793                         AnimationIn.FLY_IN_NEW_VIEWS, startDelay);
1794                 break;
1795 
1796             case FADE:
1797                 addUpdateViewPositionsAnimators(animators, true /* cascade animation */,
1798                         AnimationIn.FADE, startDelay);
1799                 break;
1800 
1801             default:
1802                 throw new IllegalStateException("Unknown animationInMode: " + mAnimationInMode);
1803         }
1804     }
1805 
1806     /**
1807      * Add animators for animating out stale views
1808      * @param animationOutMode The animation mode to play for stale views
1809      */
1810     private void addOutAnimatorsForStaleViews(List<Animator> animators,
1811             AnimationOut animationOutMode) {
1812         if (animationOutMode == AnimationOut.NONE) {
1813             return;
1814         }
1815 
1816         for (final View v : mViewsToAnimateOut) {
1817             // For each stale view to animate out, retrieve the animators for the view, then attach
1818             // the StaleViewAnimationEndListener which checks to see if the view should be recycled
1819             // at the end of the animation.
1820             final List<Animator> viewAnimators = new ArrayList<Animator>();
1821 
1822             switch (animationOutMode) {
1823                 case SLIDE:
1824                     final LayoutParams lp = (LayoutParams) v.getLayoutParams();
1825                     // Bias towards sliding right, but depending on the column that this view
1826                     // is laid out in, slide towards the nearest side edge.
1827                     int endTranslation = (int)(v.getWidth() * 1.5);
1828                     if (lp.column < (mColCount / 2)) {
1829                         endTranslation = -endTranslation;
1830                     }
1831                     SgvAnimationHelper.addSlideOutAnimators(viewAnimators, v,
1832                             (int) v.getTranslationX(), endTranslation);
1833                     break;
1834 
1835                 case COLLAPSE:
1836                     SgvAnimationHelper.addCollapseOutAnimators(viewAnimators, v);
1837                     break;
1838 
1839                 case FLY_DOWN:
1840                     SgvAnimationHelper.addFlyOutAnimators(viewAnimators, v,
1841                             (int) v.getTranslationY(), getHeight());
1842                     break;
1843 
1844                 case FADE:
1845                     SgvAnimationHelper.addFadeAnimators(viewAnimators, v, v.getAlpha(),
1846                             0 /* end alpha */);
1847                     break;
1848 
1849                 default:
1850                     throw new IllegalStateException("Unknown animationOutMode: " +
1851                             animationOutMode);
1852             }
1853 
1854             if (viewAnimators.size() > 0) {
1855                 addStaleViewAnimationEndListener(v, viewAnimators);
1856                 animators.addAll(viewAnimators);
1857             }
1858         }
1859     }
1860 
1861     /**
1862      * Handle setting up the animators of child views when the animation is invoked by a change
1863      * in the adapter.  This method has a side effect of translating view positions in preparation
1864      * for the animations.
1865      */
1866     private List<Animator> addFlyInAllViewsAnimators(List<Animator> animators) {
1867         final int childCount = getChildCount();
1868         if (childCount == 0) {
1869             return null;
1870         }
1871 
1872         if (animators == null) {
1873             animators = new ArrayList<Animator>();
1874         }
1875 
1876         for (int i = 0; i < childCount; i++) {
1877             final int animationDelay = i * ANIMATION_DELAY_IN_MS;
1878             final View childToAnimate = getChildAt(i);
1879 
1880             // Start all views from below the bottom of this grid and animate them upwards. This
1881             // is done simply by translating the current view's vertical position by the height
1882             // of the entire grid.
1883             float yTranslation = getHeight();
1884             float rotation = SgvAnimationHelper.ANIMATION_ROTATION_DEGREES;
1885             if (mIsCurrentAnimationCanceled) {
1886                 // If mIsAnimationCanceled is true, then this is not the first time that this
1887                 // animation is running.  For this particular case, we should resume from where
1888                 // the previous animation left off, rather than resetting translation and rotation.
1889                 yTranslation = childToAnimate.getTranslationY();
1890                 rotation = childToAnimate.getRotation();
1891             }
1892 
1893             SgvAnimationHelper.addTranslationRotationAnimators(animators, childToAnimate,
1894                     0 /* xTranslation */, (int) yTranslation, rotation, animationDelay);
1895         }
1896 
1897         return animators;
1898     }
1899 
1900     /**
1901      * Animations to update the views on screen to their new positions.  For new views that aren't
1902      * currently on screen, animate them in using the specified animationInMode.
1903      */
1904     private List<Animator> addUpdateViewPositionsAnimators(List<Animator> animators,
1905             boolean cascadeAnimation, AnimationIn animationInMode, int startDelay) {
1906         final int childCount = getChildCount();
1907         if (childCount == 0) {
1908             return null;
1909         }
1910 
1911         if (animators == null) {
1912             animators = new ArrayList<Animator>();
1913         }
1914 
1915         int viewsAnimated = 0;
1916         for (int i = 0; i < childCount; i++) {
1917             final View childToAnimate = getChildAt(i);
1918 
1919             if (mViewsToAnimateOut.contains(childToAnimate)) {
1920                 // If the stale views are still animating, then they are still laid out, so
1921                 // getChildCount() would've accounted for them.  Since they have their own set
1922                 // of animations to play, we'll skip over them in this loop.
1923                 continue;
1924             }
1925 
1926             // Use progressive animation delay to create the staggered effect of animating
1927             // views.  This is done by having each view delay their animation by
1928             // ANIMATION_DELAY_IN_MS after the animation of the previous view.
1929             int animationDelay = startDelay +
1930                     (cascadeAnimation ? viewsAnimated * ANIMATION_DELAY_IN_MS : 0);
1931 
1932             // Figure out whether a view with this item ID existed before
1933             final LayoutParams lp = (LayoutParams) childToAnimate.getLayoutParams();
1934 
1935             final ViewRectPair viewRectPair = mChildRectsForAnimation.get(lp.id);
1936 
1937             final int xTranslation;
1938             final int yTranslation;
1939 
1940             // If there is a valid {@link Rect} for the view with this newId, then
1941             // setup an animation.
1942             if (viewRectPair != null && viewRectPair.rect != null) {
1943                 // In the special case where the items are explicitly fading, we don't want to do
1944                 // any of the translations.
1945                 if (animationInMode == AnimationIn.FADE) {
1946                     SgvAnimationHelper.addFadeAnimators(animators, childToAnimate,
1947                             0 /* start alpha */, 1.0f /* end alpha */, animationDelay);
1948                     continue;
1949                 }
1950 
1951                 final Rect oldRect = viewRectPair.rect;
1952                 // Since the view already exists, translate it to its new position.
1953                 // Reset the child back to its previous position given by oldRect if the child
1954                 // has not already been translated.  If the child has been translated, use the
1955                 // current translated values, as this child may be in the middle of a previous
1956                 // animation, so we don't want to simply force it to new location.
1957 
1958                 xTranslation = oldRect.left - childToAnimate.getLeft();
1959                 yTranslation = oldRect.top - childToAnimate.getTop();
1960                 final float rotation = childToAnimate.getRotation();
1961 
1962                 // First set the translation X and Y. The current translation might be out of date.
1963                 childToAnimate.setTranslationX(xTranslation);
1964                 childToAnimate.setTranslationY(yTranslation);
1965 
1966                 if (xTranslation == 0 && yTranslation == 0 && rotation == 0) {
1967                     // Bail early if this view doesn't need to be translated.
1968                     continue;
1969                 }
1970 
1971                 SgvAnimationHelper.addTranslationRotationAnimators(animators, childToAnimate,
1972                         xTranslation, yTranslation, rotation, animationDelay);
1973             } else {
1974                 // If this view was not present before the data updated, rather than just flashing
1975                 // the view into its designated position, fly it up from the bottom.
1976                 xTranslation = 0;
1977                 yTranslation = (animationInMode == AnimationIn.FLY_IN_NEW_VIEWS) ? getHeight() : 0;
1978 
1979                 // Since this is a new view coming in, add additional delays so that these IN
1980                 // animations start after all the OUT animations have been played.
1981                 animationDelay += SgvAnimationHelper.getDefaultAnimationDuration();
1982 
1983                 childToAnimate.setTranslationX(xTranslation);
1984                 childToAnimate.setTranslationY(yTranslation);
1985 
1986                 switch (animationInMode) {
1987                     case FLY_IN_NEW_VIEWS:
1988                         SgvAnimationHelper.addTranslationRotationAnimators(animators,
1989                                 childToAnimate, xTranslation, yTranslation,
1990                                 SgvAnimationHelper.ANIMATION_ROTATION_DEGREES, animationDelay);
1991                         break;
1992 
1993                     case SLIDE_IN_NEW_VIEWS:
1994                         // Bias towards sliding right, but depending on the column that this view
1995                         // is laid out in, slide towards the nearest side edge.
1996                         int startTranslation = (int)(childToAnimate.getWidth() * 1.5);
1997                         if (lp.column < (mColCount / 2)) {
1998                             startTranslation = -startTranslation;
1999                         }
2000 
2001                         SgvAnimationHelper.addSlideInFromRightAnimators(animators,
2002                                 childToAnimate, startTranslation,
2003                                 animationDelay);
2004                         break;
2005 
2006                     case EXPAND_NEW_VIEWS:
2007                     case EXPAND_NEW_VIEWS_NO_CASCADE:
2008                         if (i == 0) {
2009                             // Initially set the alpha of this view to be invisible, then fade in.
2010                             childToAnimate.setAlpha(0);
2011 
2012                             // Create animators that translate the view back to translation = 0
2013                             // which would be its new layout position
2014                             final int offset = -1 * childToAnimate.getHeight();
2015                             SgvAnimationHelper.addXYTranslationAnimators(animators,
2016                                     childToAnimate, 0 /* xTranslation */, offset, animationDelay);
2017 
2018                             SgvAnimationHelper.addFadeAnimators(animators, childToAnimate,
2019                                     0 /* start alpha */, 1.0f /* end alpha */, animationDelay);
2020                         } else {
2021                             SgvAnimationHelper.addExpandInAnimators(animators,
2022                                     childToAnimate, animationDelay);
2023                         }
2024                         break;
2025                     case FADE:
2026                         SgvAnimationHelper.addFadeAnimators(animators, childToAnimate,
2027                                 0 /* start alpha */, 1.0f /* end alpha */, animationDelay);
2028                         break;
2029 
2030                     default:
2031                         continue;
2032                 }
2033             }
2034 
2035             viewsAnimated++;
2036         }
2037 
2038         return animators;
2039     }
2040 
2041     private void addStaleViewAnimationEndListener(final View view, List<Animator> viewAnimators) {
2042         if (viewAnimators == null) {
2043             return;
2044         }
2045 
2046         for (final Animator animator : viewAnimators) {
2047             animator.addListener(new AnimatorListenerAdapter() {
2048                 @Override
2049                 public void onAnimationEnd(Animator animation) {
2050                     // In the event that onChanged is called before this animation finishes,
2051                     // we would have mistakenly cached a view that would be recycled.  So
2052                     // check if it's there, and remove it so that obtainView() doesn't
2053                     // accidentally use the cached view later when it's already been
2054                     // moved to the recycler.
2055                     final LayoutParams lp = (LayoutParams) view.getLayoutParams();
2056                     if (mChildRectsForAnimation.containsKey(lp.id)) {
2057                         mChildRectsForAnimation.remove(lp.id);
2058                     }
2059 
2060                     recycleView(view);
2061                 }
2062             });
2063         }
2064     }
2065 
2066     /**
2067      * Calculate and cache the {@link LayoutRecord}s for all positions up to mFirstPosition.
2068      * mFirstPosition is the position that layout will start from, but we need to know where all
2069      * views preceding it will be laid out so that mFirstPosition will be laid out at the correct
2070      * position.  If this is not done, mFirstPosition will be laid out at the first empty space
2071      * possible (i.e., top left), and this may not be the correct position in the overall layout.
2072      *
2073      * This can be optimized if we don't need to guard against jagged edges in the grid or if
2074      * mFirstChangedPosition is set to a non-zero value (so we can skip calculating some views).
2075      */
2076     private void calculateLayoutStartOffsets(int offset) {
2077         // Bail early if we don't guard against jagged edges or if nothing has changed before
2078         // mFirstPosition.
2079         // Also check that we're not at the top of the list because sometimes grid padding isn't set
2080         // until after mItemTops and mItemBottoms arrays have been initialized, so we should
2081         // go through and compute the right layout start offset for mFirstPosition = 0.
2082         if (mFirstPosition != 0 &&
2083                 (!mGuardAgainstJaggedEdges || mFirstPosition < mFirstChangedPosition)) {
2084             // At this time, we know that mItemTops should be the same, because
2085             // nothing has changed before view at mFirstPosition. The only thing
2086             // we need to do is to reset mItemBottoms. The result should be the
2087             // same, if we don't bail early and execute the following code
2088             // again. Notice that mItemBottoms always equal to mItemTops after
2089             // this method.
2090             System.arraycopy(mItemTops, 0, mItemBottoms, 0, mColCount);
2091             return;
2092         }
2093 
2094         final int colWidth = (getWidth() - getPaddingLeft() - getPaddingRight() -
2095                 mItemMargin * (mColCount - 1)) / mColCount;
2096 
2097         Arrays.fill(mItemTops, getPaddingTop());
2098         Arrays.fill(mItemBottoms, getPaddingTop());
2099 
2100         // Since we will be doing a pass to calculate all views up to mFirstPosition, it is likely
2101         // that all existing {@link LayoutRecord}s will be stale, so clear it out to avoid
2102         // accidentally the re-use of stale values.
2103         //
2104         // Note: We cannot just invalidate all layout records after mFirstPosition because it is
2105         // possible that this layout pass is caused by a down sync from the server that may affect
2106         // the layout of views from position 0 to mFirstPosition - 1.
2107         if (mDataChanged) {
2108             mLayoutRecords.clear();
2109         }
2110 
2111         for (int i = 0; i < mFirstPosition; i++) {
2112             LayoutRecord rec = mLayoutRecords.get(i);
2113 
2114             if (mDataChanged || rec == null) {
2115                 final View view = obtainView(i, null);
2116                 final LayoutParams lp = (LayoutParams) view.getLayoutParams();
2117 
2118                 final int heightSpec;
2119                 if (lp.height == LayoutParams.WRAP_CONTENT) {
2120                     heightSpec = MeasureSpec.makeMeasureSpec(0, MeasureSpec.UNSPECIFIED);
2121                 } else {
2122                     heightSpec = MeasureSpec.makeMeasureSpec(lp.height, MeasureSpec.EXACTLY);
2123                 }
2124 
2125                 final int span = Math.min(mColCount, lp.span);
2126                 final int widthSize = colWidth * span + mItemMargin * (span - 1);
2127                 final int widthSpec = MeasureSpec.makeMeasureSpec(widthSize, MeasureSpec.EXACTLY);
2128 
2129                 view.measure(widthSpec, heightSpec);
2130                 final int height = view.getMeasuredHeight();
2131 
2132                 if (rec == null) {
2133                     rec = new LayoutRecord();
2134                     mLayoutRecords.put(i, rec);
2135                 }
2136 
2137                 rec.height = height;
2138                 rec.id = lp.id;
2139                 rec.span = span;
2140 
2141                 // We're not actually using this view, so add this back to the recycler.
2142                 mRecycler.addScrap(view);
2143             }
2144 
2145             int nextColumn = getNextColumnDown();
2146 
2147             // Given the span, check if there's enough space to put this view at this column.
2148             // IMPORTANT Use the same logic in {@link #layoutChildren}.
2149             if (rec.span > 1) {
2150                 if (mIsRtlLayout) {
2151                     if (nextColumn + 1 < rec.span) {
2152                         nextColumn = mColCount - 1;
2153                     }
2154                 } else {
2155                     if (mColCount - nextColumn < rec.span) {
2156                         nextColumn = 0;
2157                     }
2158                 }
2159             }
2160             rec.column = nextColumn;
2161 
2162             // Place the top of this child beneath the last by finding the lowest coordinate across
2163             // the columns that this child will span.  For LTR layout, we scan across from left to
2164             // right, and for RTL layout, we scan from right to left.
2165             // TODO: Consolidate this logic with getNextRecordDown() in the future, as that method
2166             // already calculates the margins for us.  This will keep the implementation consistent
2167             // with layoutChildren(), fillUp() and fillDown().
2168             int lowest = mItemBottoms[nextColumn] + mItemMargin;
2169             if (rec.span > 1) {
2170                 for (int spanIndex = 0; spanIndex < rec.span; spanIndex++) {
2171                     final int index = mIsRtlLayout ? nextColumn - spanIndex :
2172                             nextColumn + spanIndex;
2173                     final int bottom = mItemBottoms[index] + mItemMargin;
2174                     if (bottom > lowest) {
2175                         lowest = bottom;
2176                     }
2177                 }
2178             }
2179 
2180             for (int spanIndex = 0; spanIndex < rec.span; spanIndex++) {
2181                 final int col = mIsRtlLayout ? nextColumn - spanIndex : nextColumn + spanIndex;
2182                 mItemBottoms[col] = lowest + rec.height;
2183 
2184                 if (Log.isLoggable(TAG, Log.VERBOSE)) {
2185                     Log.v(TAG, " position: " + i + " bottoms: ");
2186                     for (int j = 0; j < mColCount; j++) {
2187                         Log.v(TAG, "    mItemBottoms["+j+"]: " + mItemBottoms[j]);
2188                     }
2189                 }
2190             }
2191         }
2192 
2193         // mItemBottoms[] at this point contains the values of all views up to mFirstPosition.  To
2194         // figure out where view at mFirstPosition will be laid out, we'll need to find the column
2195         // that is the highest (i.e., i where mItemBottoms[i] <= mItemBottoms[j] for all j
2196         // from 0 to mColCount.)
2197         int highestValue = Integer.MAX_VALUE;
2198         for (int k = 0; k < mColCount; k++) {
2199             if (mItemBottoms[k] < highestValue) {
2200                 highestValue = mItemBottoms[k];
2201             }
2202         }
2203 
2204         // Adjust the offsets in each column so that values in mItemTops[] and mItemBottoms[]
2205         // reflect coordinates on screen.  These offsets will be the actual values where layout
2206         // will start from, otherwise, we'd naively start at (leftPadding, topPadding) for
2207         // mFirstPosition.
2208         for (int k = 0; k < mColCount; k++) {
2209             mItemBottoms[k] = mItemBottoms[k] - highestValue + offset;
2210             mItemTops[k] = mItemBottoms[k];
2211 
2212             // Log.v(TAG, "Adjusting to offset = mItemBottoms[" + k + "]: " + mItemBottoms[k]);
2213         }
2214     }
2215 
2216     /**
2217      * Measure and layout all currently visible children.
2218      *
2219      * @param queryAdapter true to requery the adapter for view data
2220      */
2221     final void layoutChildren(boolean queryAdapter) {
2222         final int paddingLeft = getPaddingLeft();
2223         final int paddingRight = getPaddingRight();
2224         final int itemMargin = mItemMargin;
2225         final int availableWidth = (getWidth() - paddingLeft - paddingRight - itemMargin
2226                 * (mColCount - 1));
2227         final int colWidth = availableWidth / mColCount;
2228         // The availableWidth may not be divisible by mColCount. Keep the
2229         // remainder. It will be added to the width of the last view in the row.
2230         final int remainder = availableWidth % mColCount;
2231 
2232         boolean viewsRemovedInLayout = false;
2233 
2234         // If we're animating out stale views, then we want to defer recycling of views.
2235         final boolean deferRecyclingForAnimation = mAnimationOutMode != AnimationOut.NONE;
2236 
2237         if (!deferRecyclingForAnimation) {
2238             final int childCount = getChildCount();
2239             // If the latest data set has fewer data items than mFirstPosition, don't keep any
2240             // views on screen, and just let the layout logic below retrieve appropriate views
2241             // from the recycler.
2242             final int viewsToKeepOnScreen = (mItemCount <= mFirstPosition) ? 0 :
2243                 mItemCount - mFirstPosition;
2244 
2245             if (childCount > viewsToKeepOnScreen) {
2246                 // If there are more views laid out than the number of data items remaining to be
2247                 // laid out, recycle the extraneous views.
2248                 recycleViewsInRange(viewsToKeepOnScreen, childCount - 1);
2249                 viewsRemovedInLayout = true;
2250             }
2251         } else {
2252             mViewsToAnimateOut.clear();
2253         }
2254 
2255         for (int i = 0; i < getChildCount(); i++) {
2256             final int position = mFirstPosition + i;
2257             View child = getChildAt(i);
2258 
2259             final int highestAvailableLayoutPosition = mItemBottoms[getNextColumnDown()];
2260             if (deferRecyclingForAnimation &&
2261                     (position >= mItemCount || highestAvailableLayoutPosition >= getHeight())) {
2262                 // For the remainder of views on screen, they should not be on screen, so we can
2263                 // skip layout.  Add them to the list of views to animate out.
2264                 // We should only get in this position if deferRecyclingForAnimation = true,
2265                 // otherwise, we should've recycled all views before getting into this layout loop.
2266                 mViewsToAnimateOut.add(child);
2267                 continue;
2268             }
2269 
2270             LayoutParams lp = null;
2271             int col = -1;
2272 
2273             if (child != null) {
2274                 lp = (LayoutParams) child.getLayoutParams();
2275                 col = lp.column;
2276             }
2277 
2278             final boolean needsLayout = queryAdapter || child == null || child.isLayoutRequested();
2279             if (queryAdapter) {
2280                 View newView = null;
2281                 if (deferRecyclingForAnimation) {
2282                     // If we are deferring recycling for animation, then we don't want to pass the
2283                     // current child in to obtainView for re-use.  obtainView() in this case should
2284                     // try to find the view belonging to this item on screen, or populate a fresh
2285                     // one from the recycler.
2286                     newView = obtainView(position);
2287                 } else {
2288                     newView = obtainView(position, child);
2289                 }
2290 
2291                 // Update layout params since they may have changed
2292                 lp = (LayoutParams) newView.getLayoutParams();
2293 
2294                 if (newView != child) {
2295                     if (child != null && !deferRecyclingForAnimation) {
2296                         mRecycler.addScrap(child);
2297                         removeViewInLayout(child);
2298                         viewsRemovedInLayout = true;
2299                     }
2300 
2301                     // If this view is already in the layout hierarchy, we can just detach it
2302                     // from the parent and re-attach it at the correct index.  If the view has
2303                     // already been removed from the layout hierarchy, getParent() == null.
2304                     if (newView.getParent() == this) {
2305                         detachViewFromParent(newView);
2306                         attachViewToParent(newView, i, lp);
2307                     } else {
2308                         addViewInLayout(newView, i, lp);
2309                     }
2310                 }
2311 
2312                 child = newView;
2313 
2314                 // Since the data has changed, we need to make sure the next child is in the
2315                 // right column. We choose the next column down (vs. next column up) because we
2316                 // are filling from the top of the screen downwards as we iterate through
2317                 // visible children. (We take span into account below.)
2318                 lp.column = getNextColumnDown();
2319                 col = lp.column;
2320             }
2321 
2322             setReorderingArea(lp);
2323 
2324             final int span = Math.min(mColCount, lp.span);
2325 
2326             // Given the span, check if there's enough space to put this view at this column.
2327             // IMPORTANT Propagate the same logic to {@link #calculateLayoutStartOffsets}.
2328             if (span > 1) {
2329                 if (mIsRtlLayout) {
2330                     // For RTL layout, if the current column index is less than the span of the
2331                     // child, then we know that there is not enough room remaining to lay this
2332                     // child out (e.g., if col == 0, but span == 2, then laying this child down
2333                     // at column = col would put us out of bound into a negative column index.).
2334                     // For this scenario, reset the index back to the right-most column, and lay
2335                     // out the child at this position where we can ensure that we can display as
2336                     // much of the child as possible.
2337                     if (col + 1 < span) {
2338                         col = mColCount - 1;
2339                     }
2340                 } else {
2341                     if (mColCount - col < span) {
2342                         // If not, reset the col to 0.
2343                         col = 0;
2344                     }
2345                 }
2346 
2347                 lp.column = col;
2348             }
2349 
2350             int widthSize = (colWidth * span + itemMargin * (span - 1));
2351             // If it is rtl, we layout the view from col to col - span +
2352             // 1. If it reaches the most left column, i.e. we added the
2353             // additional width. So the check it span == col +1
2354             if ((mIsRtlLayout && span == col + 1)
2355                     || (!mIsRtlLayout && span + col == mColCount)) {
2356                 widthSize += remainder;
2357             }
2358             if (needsLayout) {
2359                 final int widthSpec = MeasureSpec.makeMeasureSpec(widthSize, MeasureSpec.EXACTLY);
2360 
2361                 final int heightSpec;
2362                 if (lp.height == LayoutParams.WRAP_CONTENT) {
2363                     heightSpec = MeasureSpec.makeMeasureSpec(0, MeasureSpec.UNSPECIFIED);
2364                 } else {
2365                     heightSpec = MeasureSpec.makeMeasureSpec(lp.height, MeasureSpec.EXACTLY);
2366                 }
2367 
2368                 child.measure(widthSpec, heightSpec);
2369             }
2370 
2371             // Place the top of this child beneath the last by finding the lowest coordinate across
2372             // the columns that this child will span.  For LTR layout, we scan across from left to
2373             // right, and for RTL layout, we scan from right to left.
2374             // TODO:  Consolidate this logic with getNextRecordDown() in the future, as that method
2375             // already calculates the margins for us.  This will keep the implementation consistent
2376             // with fillUp() and fillDown().
2377             int childTop = mItemBottoms[col] + mItemMargin;
2378             if (span > 1) {
2379                 int lowest = childTop;
2380                 for (int spanIndex = 0; spanIndex < span; spanIndex++) {
2381                     final int index = mIsRtlLayout ? col - spanIndex : col + spanIndex;
2382                     final int bottom = mItemBottoms[index] + mItemMargin;
2383                     if (bottom > lowest) {
2384                         lowest = bottom;
2385                     }
2386                 }
2387 
2388                 childTop = lowest;
2389             }
2390 
2391             final int childHeight = child.getMeasuredHeight();
2392             final int childBottom = childTop + childHeight;
2393             int childLeft = 0;
2394             int childRight = 0;
2395             if (mIsRtlLayout) {
2396                 childRight = (getWidth() - paddingRight) -
2397                         (mColCount - col - 1) * (colWidth + itemMargin);
2398                 childLeft = childRight - child.getMeasuredWidth();
2399             } else {
2400                 childLeft = paddingLeft + col * (colWidth + itemMargin);
2401                 childRight = childLeft + child.getMeasuredWidth();
2402             }
2403 
2404         /*    Log.v(TAG, "[layoutChildren] height: " + childHeight
2405                     + " top: " + childTop + " bottom: " + childBottom
2406                     + " left: " + childLeft
2407                     + " column: " + col
2408                     + " position: " + position
2409                     + " id: " + lp.id);
2410 */
2411             child.layout(childLeft, childTop, childRight, childBottom);
2412             if (lp.id == mFocusedChildIdToScrollIntoView) {
2413                 child.requestFocus();
2414             }
2415 
2416             for (int spanIndex = 0; spanIndex < span; spanIndex++) {
2417                 final int index = mIsRtlLayout ? col - spanIndex : col + spanIndex;
2418                 mItemBottoms[index] = childBottom;
2419             }
2420 
2421             // Whether or not LayoutRecords may have already existed for the view at this position
2422             // on screen, we'll update it after we lay out to ensure that the LayoutRecord
2423             // has the most updated information about the view at this position.  We can be assured
2424             // that all views before those on screen (views with adapter position < mFirstPosition)
2425             // have the correct LayoutRecords because calculateLayoutStartOffsets() would have
2426             // set them appropriately.
2427             LayoutRecord rec = mLayoutRecords.get(position);
2428             if (rec == null) {
2429                 rec = new LayoutRecord();
2430                 mLayoutRecords.put(position, rec);
2431             }
2432 
2433             rec.column = lp.column;
2434             rec.height = childHeight;
2435             rec.id = lp.id;
2436             rec.span = span;
2437         }
2438 
2439         // It appears that removeViewInLayout() does not invalidate.  So if we make use of this
2440         // method during layout, we should invalidate explicitly.
2441         if (viewsRemovedInLayout || deferRecyclingForAnimation) {
2442             invalidate();
2443         }
2444     }
2445 
2446     /**
2447      * Set the reordering area for the child layout specified
2448      */
2449     private void setReorderingArea(LayoutParams childLayoutParams) {
2450         final boolean isLastColumn = childLayoutParams.column == (mColCount - 1);
2451         childLayoutParams.reorderingArea =
2452                 mAdapter.getReorderingArea(childLayoutParams.position, isLastColumn);
2453     }
2454 
2455     final void invalidateLayoutRecordsBeforePosition(int position) {
2456         int endAt = 0;
2457         while (endAt < mLayoutRecords.size() && mLayoutRecords.keyAt(endAt) < position) {
2458             endAt++;
2459         }
2460         mLayoutRecords.removeAtRange(0, endAt);
2461     }
2462 
2463     final void invalidateLayoutRecordsAfterPosition(int position) {
2464         int beginAt = mLayoutRecords.size() - 1;
2465         while (beginAt >= 0 && mLayoutRecords.keyAt(beginAt) > position) {
2466             beginAt--;
2467         }
2468         beginAt++;
2469         mLayoutRecords.removeAtRange(beginAt + 1, mLayoutRecords.size() - beginAt);
2470     }
2471 
2472     /**
2473      * Before doing an animation, map the item IDs for the currently visible children to the
2474      * {@link Rect} that defines their position on the screen so a translation animation
2475      * can be applied to their new layout positions.
2476      */
2477     private void cacheChildRects() {
2478         final int childCount = getChildCount();
2479         mChildRectsForAnimation.clear();
2480 
2481         long originalDraggedChildId = -1;
2482         if (isDragReorderingSupported()) {
2483             originalDraggedChildId = mReorderHelper.getDraggedChildId();
2484             if (mCachedDragViewRect != null && originalDraggedChildId != -1) {
2485                 // This child was dragged in a reordering operation.  Use the cached position
2486                 // of where the drag event was released as the cached location.
2487                 mChildRectsForAnimation.put(originalDraggedChildId,
2488                         new ViewRectPair(mDragView, mCachedDragViewRect));
2489                 mCachedDragViewRect = null;
2490             }
2491         }
2492 
2493         for (int i = 0; i < childCount; i++) {
2494             final View child = getChildAt(i);
2495             final LayoutParams lp = (LayoutParams) child.getLayoutParams();
2496 
2497             Rect rect;
2498             if (lp.id != originalDraggedChildId) {
2499                 final int childTop = (int) child.getY();
2500                 final int childBottom = childTop + child.getHeight();
2501                 final int childLeft = (int) child.getX();
2502                 final int childRight = childLeft + child.getWidth();
2503                 rect = new Rect(childLeft, childTop, childRight, childBottom);
2504                 mChildRectsForAnimation.put(lp.id /* item id */, new ViewRectPair(child, rect));
2505             }
2506         }
2507     }
2508 
2509     /**
2510      * Should be called with mPopulating set to true
2511      *
2512      * @param fromPosition Position to start filling from
2513      * @param overhang the number of extra pixels to fill beyond the current top edge
2514      * @return the max overhang beyond the beginning of the view of any added items at the top
2515      */
2516     final int fillUp(int fromPosition, int overhang) {
2517         final int paddingLeft = getPaddingLeft();
2518         final int paddingRight = getPaddingRight();
2519         final int itemMargin = mItemMargin;
2520         final int availableWidth = (getWidth() - paddingLeft - paddingRight - itemMargin
2521                 * (mColCount - 1));
2522         final int colWidth = availableWidth / mColCount;
2523         // The availableWidth may not be divisible by mColCount. Keep the
2524         // remainder. It will be added to the width of the last view in the row.
2525         final int remainder = availableWidth % mColCount;
2526         final int gridTop = getPaddingTop();
2527         final int fillTo = -overhang;
2528         int nextCol = getNextColumnUp();
2529         int position = fromPosition;
2530 
2531         while (nextCol >= 0 && mItemTops[nextCol] > fillTo && position >= 0) {
2532             final View child = obtainView(position, null);
2533             final LayoutParams lp = (LayoutParams) child.getLayoutParams();
2534 
2535             if (child.getParent() != this) {
2536                 if (mInLayout) {
2537                     addViewInLayout(child, 0, lp);
2538                 } else {
2539                     addView(child, 0);
2540                 }
2541             }
2542 
2543             final int span = Math.min(mColCount, lp.span);
2544 
2545             LayoutRecord rec;
2546             if (span > 1) {
2547                 rec = getNextRecordUp(position, span);
2548                 nextCol = rec.column;
2549             } else {
2550                 rec = mLayoutRecords.get(position);
2551             }
2552 
2553             boolean invalidateBefore = false;
2554             if (rec == null) {
2555                 rec = new LayoutRecord();
2556                 mLayoutRecords.put(position, rec);
2557                 rec.column = nextCol;
2558                 rec.span = span;
2559             } else if (span != rec.span) {
2560                 rec.span = span;
2561                 rec.column = nextCol;
2562                 invalidateBefore = true;
2563             } else {
2564                 nextCol = rec.column;
2565             }
2566 
2567             if (mHasStableIds) {
2568                 rec.id = lp.id;
2569             }
2570 
2571             lp.column = nextCol;
2572             setReorderingArea(lp);
2573 
2574             int widthSize = colWidth * span + itemMargin * (span - 1);
2575             // If it is rtl, we layout the view from nextCol to nextCol - span +
2576             // 1. If it reaches the most left column, i.e. we added the
2577             // additional width. So the check it span == nextCol + 1
2578             if ((mIsRtlLayout && span == nextCol + 1)
2579                     || (!mIsRtlLayout && span + nextCol == mColCount)) {
2580                 widthSize += remainder;
2581             }
2582             final int widthSpec = MeasureSpec.makeMeasureSpec(widthSize, MeasureSpec.EXACTLY);
2583             final int heightSpec;
2584             if (lp.height == LayoutParams.WRAP_CONTENT) {
2585                 heightSpec = MeasureSpec.makeMeasureSpec(0, MeasureSpec.UNSPECIFIED);
2586             } else {
2587                 heightSpec = MeasureSpec.makeMeasureSpec(lp.height, MeasureSpec.EXACTLY);
2588             }
2589             child.measure(widthSpec, heightSpec);
2590 
2591             final int childHeight = child.getMeasuredHeight();
2592             if (invalidateBefore || (childHeight != rec.height && rec.height > 0)) {
2593                 invalidateLayoutRecordsBeforePosition(position);
2594             }
2595             rec.height = childHeight;
2596 
2597             // Iterate across each column that this child spans and add the margin calculated
2598             // for that column to mItemTops.  getMarginBelow() is expected to give us the correct
2599             // margin values at each column such that mItemTops ends up with a smooth edge across
2600             // the column spans.  We need to do this before actually laying down the child,
2601             // otherwise we risk overlapping one child over another.  mItemTops stores the top
2602             // index for where the next child should be laid out.  For RTL, we do the update
2603             // in reverse order.
2604             for (int i = 0; i < span; i++) {
2605                 final int index = mIsRtlLayout ? nextCol - i : nextCol + i;
2606                 mItemTops[index] += rec.getMarginBelow(i);
2607             }
2608 
2609             final int startFrom = mItemTops[nextCol];
2610             final int childBottom = startFrom;
2611             final int childTop = childBottom - childHeight;
2612 
2613             int childLeft = 0;
2614             int childRight = 0;
2615             // For LTR layout, the child's left is calculated as the
2616             // (column index from left) * (columnWidth plus item margins).
2617             // For RTL layout, the child's left is relative to its right, and its right coordinate
2618             // is calculated as the difference between the width of this grid and
2619             // (column index from right) * (columnWidth plus item margins).
2620             if (mIsRtlLayout) {
2621                 childRight = (getWidth() - paddingRight) -
2622                         (mColCount - nextCol - 1) * (colWidth + itemMargin);
2623                 childLeft = childRight - child.getMeasuredWidth();
2624             } else {
2625                 childLeft = paddingLeft + nextCol * (colWidth + itemMargin);
2626                 childRight = childLeft + child.getMeasuredWidth();
2627             }
2628             child.layout(childLeft, childTop, childRight, childBottom);
2629 
2630             Log.v(TAG, "[fillUp] position: " + position + " id: " + lp.id
2631                     + " childLeft: " + childLeft + " childTop: " + childTop
2632                     + " column: " + rec.column + " childHeight:" + childHeight);
2633 
2634             // Since we're filling up, once the child is laid out, update mItemTops again
2635             // to reflect the next available top value at this column.  This is simply the child's
2636             // top coordinates, minus any available margins set.  For LTR, we start at the column
2637             // that this child is laid out from (nextCol) and move right for span amount.  For RTL
2638             // layout, we start at the column that this child is laid out from and move left.
2639             for (int i = 0; i < span; i++) {
2640                 final int index = mIsRtlLayout ? nextCol - i : nextCol + i;
2641                 mItemTops[index] = childTop - rec.getMarginAbove(i) - itemMargin;
2642             }
2643 
2644             if (lp.id == mFocusedChildIdToScrollIntoView) {
2645                 child.requestFocus();
2646             }
2647 
2648             nextCol = getNextColumnUp();
2649             mFirstPosition = position--;
2650         }
2651 
2652         int highestView = getHeight();
2653         for (int i = 0; i < mColCount; i++) {
2654             if (mItemTops[i] < highestView) {
2655                 highestView = mItemTops[i];
2656             }
2657         }
2658         return gridTop - highestView;
2659     }
2660 
2661     /**
2662      * Should be called with mPopulating set to true
2663      *
2664      * @param fromPosition Position to start filling from
2665      * @param overhang the number of extra pixels to fill beyond the current bottom edge
2666      * @return the max overhang beyond the end of the view of any added items at the bottom
2667      */
2668     final int fillDown(int fromPosition, int overhang) {
2669         final int paddingLeft = getPaddingLeft();
2670         final int paddingRight = getPaddingRight();
2671         final int itemMargin = mItemMargin;
2672         final int availableWidth = (getWidth() - paddingLeft - paddingRight - itemMargin
2673                 * (mColCount - 1));
2674         final int colWidth = availableWidth / mColCount;
2675         // The availableWidth may not be divisible by mColCount. Keep the
2676         // remainder. It will be added to the width of the last view in the row.
2677         final int remainder = availableWidth % mColCount;
2678         final int gridBottom = getHeight() - getPaddingBottom();
2679         final int fillTo = gridBottom + overhang;
2680         int nextCol = getNextColumnDown();
2681         int position = fromPosition;
2682 
2683         while (nextCol >= 0 && mItemBottoms[nextCol] < fillTo && position < mItemCount) {
2684             final View child = obtainView(position, null);
2685             final LayoutParams lp = (LayoutParams) child.getLayoutParams();
2686             if (child.getParent() != this) {
2687                 if (mInLayout) {
2688                     addViewInLayout(child, -1, lp);
2689                 } else {
2690                     addView(child);
2691                 }
2692             }
2693 
2694             final int span = Math.min(mColCount, lp.span);
2695 
2696             LayoutRecord rec;
2697             if (span > 1) {
2698                 rec = getNextRecordDown(position, span);
2699                 nextCol = rec.column;
2700             } else {
2701                 rec = mLayoutRecords.get(position);
2702             }
2703 
2704             boolean invalidateAfter = false;
2705             if (rec == null) {
2706                 rec = new LayoutRecord();
2707                 mLayoutRecords.put(position, rec);
2708                 rec.column = nextCol;
2709                 rec.span = span;
2710             } else if (span != rec.span) {
2711                 rec.span = span;
2712                 rec.column = nextCol;
2713                 invalidateAfter = true;
2714             } else {
2715                 nextCol = rec.column;
2716             }
2717 
2718             if (mHasStableIds) {
2719                 rec.id = lp.id;
2720             }
2721 
2722             lp.column = nextCol;
2723             setReorderingArea(lp);
2724 
2725 
2726             int widthSize = colWidth * span + itemMargin * (span - 1);
2727             // If it is rtl, we layout the view from nextCol to nextCol - span +
2728             // 1. If it reaches the most left column, i.e. we added the
2729             // additional width. So the check it span == nextCol +1
2730             if ((mIsRtlLayout && span == nextCol + 1)
2731                     || (!mIsRtlLayout && span + nextCol == mColCount)) {
2732                 widthSize += remainder;
2733             }
2734             final int widthSpec = MeasureSpec.makeMeasureSpec(widthSize, MeasureSpec.EXACTLY);
2735             final int heightSpec;
2736             if (lp.height == LayoutParams.WRAP_CONTENT) {
2737                 heightSpec = MeasureSpec.makeMeasureSpec(0, MeasureSpec.UNSPECIFIED);
2738             } else {
2739                 heightSpec = MeasureSpec.makeMeasureSpec(lp.height, MeasureSpec.EXACTLY);
2740             }
2741             child.measure(widthSpec, heightSpec);
2742 
2743             final int childHeight = child.getMeasuredHeight();
2744             if (invalidateAfter || (childHeight != rec.height && rec.height > 0)) {
2745                 invalidateLayoutRecordsAfterPosition(position);
2746             }
2747 
2748             rec.height = childHeight;
2749 
2750             // Before laying out the child, we need to make sure mItemBottoms is updated with the
2751             // correct values such that there is a smooth edge across the child's span.
2752             // getMarginAbove() is expected to give us these values.  For LTR layout, we start at
2753             // nextCol, and update forward for the number of columns this child spans.  For RTL
2754             // layout, we start at nextCol and update backwards for the same number of columns.
2755             for (int i = 0; i < span; i++) {
2756                 final int index = mIsRtlLayout ? nextCol - i : nextCol + i;
2757                 mItemBottoms[index] += rec.getMarginAbove(i);
2758             }
2759 
2760             final int startFrom = mItemBottoms[nextCol];
2761             final int childTop = startFrom + itemMargin;
2762             final int childBottom = childTop + childHeight;
2763             int childLeft = 0;
2764             int childRight = 0;
2765             if (mIsRtlLayout) {
2766                 childRight = (getWidth() - paddingRight) -
2767                         (mColCount - nextCol - 1) * (colWidth + itemMargin);
2768                 childLeft = childRight - child.getMeasuredWidth();
2769             } else {
2770                 childLeft = paddingLeft + nextCol * (colWidth + itemMargin);
2771                 childRight = childLeft + child.getMeasuredWidth();
2772             }
2773 
2774             Log.v(TAG, "[fillDown] position: " + position + " id: " + lp.id
2775                     + " childLeft: " + childLeft + " childTop: " + childTop
2776                     + " column: " + rec.column + " childHeight:" + childHeight);
2777 
2778             child.layout(childLeft, childTop, childRight, childBottom);
2779 
2780             // Once we've laid down the child, update mItemBottoms again to reflect the next
2781             // available set of bottom values for the next child.
2782             for (int i = 0; i < span; i++) {
2783                 final int index = mIsRtlLayout ? nextCol - i : nextCol + i;
2784                 mItemBottoms[index] = childBottom + rec.getMarginBelow(i);
2785             }
2786 
2787             if (lp.id == mFocusedChildIdToScrollIntoView) {
2788                 child.requestFocus();
2789             }
2790 
2791             nextCol = getNextColumnDown();
2792             position++;
2793         }
2794 
2795         int lowestView = 0;
2796         for (int i = 0; i < mColCount; i++) {
2797             final int index = mIsRtlLayout ? mColCount - (i + 1) : i;
2798             if (mItemBottoms[index] > lowestView) {
2799                 lowestView = mItemBottoms[index];
2800             }
2801         }
2802 
2803         return lowestView - gridBottom;
2804     }
2805 
2806     /**
2807      * @return column that the next view filling upwards should occupy. This is the bottom-most
2808      *         position available for a single-column item.
2809      */
2810     final int getNextColumnUp() {
2811         int result = -1;
2812         int bottomMost = Integer.MIN_VALUE;
2813 
2814         final int colCount = mColCount;
2815         for (int i = colCount - 1; i >= 0; i--) {
2816             final int index = mIsRtlLayout ? colCount - (i + 1) : i;
2817             final int top = mItemTops[index];
2818             if (top > bottomMost) {
2819                 bottomMost = top;
2820                 result = index;
2821             }
2822         }
2823 
2824         return result;
2825     }
2826 
2827     /**
2828      * Return a LayoutRecord for the given position
2829      * @param position
2830      * @param span
2831      * @return
2832      */
2833     final LayoutRecord getNextRecordUp(int position, int span) {
2834         LayoutRecord rec = mLayoutRecords.get(position);
2835         if (rec == null || rec.span != span) {
2836             if (span > mColCount) {
2837                 throw new IllegalStateException("Span larger than column count! Span:" + span
2838                         + " ColumnCount:" + mColCount);
2839             }
2840             rec = new LayoutRecord();
2841             rec.span = span;
2842             mLayoutRecords.put(position, rec);
2843         }
2844         int targetCol = -1;
2845         int bottomMost = Integer.MIN_VALUE;
2846 
2847         // For LTR layout, we start from the bottom-right corner upwards when we need to find the
2848         // NextRecordUp.  For RTL, we will start from bottom-left.
2849         final int colCount = mColCount;
2850         if (mIsRtlLayout) {
2851             for (int i = span - 1; i < colCount; i++) {
2852                 int top = Integer.MAX_VALUE;
2853                 for (int j = i; j > i - span; j--) {
2854                     final int singleTop = mItemTops[j];
2855                     if (singleTop < top) {
2856                         top = singleTop;
2857                     }
2858                 }
2859                 if (top > bottomMost) {
2860                     bottomMost = top;
2861                     targetCol = i;
2862                 }
2863             }
2864         } else {
2865             for (int i = colCount - span; i >= 0; i--) {
2866                 int top = Integer.MAX_VALUE;
2867                 for (int j = i; j < i + span; j++) {
2868                     final int singleTop = mItemTops[j];
2869                     if (singleTop < top) {
2870                         top = singleTop;
2871                     }
2872                 }
2873                 if (top > bottomMost) {
2874                     bottomMost = top;
2875                     targetCol = i;
2876                 }
2877             }
2878         }
2879 
2880         rec.column = targetCol;
2881 
2882         // Once we've found the target column for the view at this position, we update mItemTops
2883         // for all columns that this view will occupy.  We set the margin such that mItemTops is
2884         // equal for all columns in the view's span.  For LTR layout, we start at targetCol and
2885         // move right, and for RTL, we start at targetCol and move left.
2886         for (int i = 0; i < span; i++) {
2887             final int nextCol = mIsRtlLayout ? targetCol - i : targetCol + i;
2888             rec.setMarginBelow(i, mItemTops[nextCol] - bottomMost);
2889         }
2890 
2891         return rec;
2892     }
2893 
2894     /**
2895      * @return column that the next view filling downwards should occupy. This is the top-most
2896      *         position available.
2897      */
2898     final int getNextColumnDown() {
2899         int topMost = Integer.MAX_VALUE;
2900         int result = 0;
2901         final int colCount = mColCount;
2902 
2903         for (int i = 0; i < colCount; i++) {
2904             final int index = mIsRtlLayout ? colCount - (i + 1) : i;
2905             final int bottom = mItemBottoms[index];
2906             if (bottom < topMost) {
2907                 topMost = bottom;
2908                 result = index;
2909             }
2910         }
2911 
2912         return result;
2913     }
2914 
2915     final LayoutRecord getNextRecordDown(int position, int span) {
2916         LayoutRecord rec = mLayoutRecords.get(position);
2917         if (rec == null || rec.span != span) {
2918             if (span > mColCount) {
2919                 throw new IllegalStateException("Span larger than column count! Span:" + span
2920                         + " ColumnCount:" + mColCount);
2921             }
2922 
2923             rec = new LayoutRecord();
2924             rec.span = span;
2925             mLayoutRecords.put(position, rec);
2926         }
2927 
2928         int targetCol = -1;
2929         int topMost = Integer.MAX_VALUE;
2930 
2931         final int colCount = mColCount;
2932 
2933         // For LTR layout, we start from the top-left corner and move right-downwards, when we
2934         // need to find the NextRecordDown.  For RTL we will start from Top-Right corner, and move
2935         // left-downwards.
2936         if (mIsRtlLayout) {
2937             for (int i = colCount - 1; i >= span - 1; i--) {
2938                 int bottom = Integer.MIN_VALUE;
2939                 for (int j = i; j > i - span; j--) {
2940                     final int singleBottom = mItemBottoms[j];
2941                     if (singleBottom > bottom) {
2942                         bottom = singleBottom;
2943                     }
2944                 }
2945                 if (bottom < topMost) {
2946                     topMost = bottom;
2947                     targetCol = i;
2948                 }
2949             }
2950         } else {
2951             for (int i = 0; i <= colCount - span; i++) {
2952                 int bottom = Integer.MIN_VALUE;
2953                 for (int j = i; j < i + span; j++) {
2954                     final int singleBottom = mItemBottoms[j];
2955                     if (singleBottom > bottom) {
2956                         bottom = singleBottom;
2957                     }
2958                 }
2959                 if (bottom < topMost) {
2960                     topMost = bottom;
2961                     targetCol = i;
2962                 }
2963             }
2964         }
2965 
2966         rec.column = targetCol;
2967 
2968         // Once we've found the target column for the view at this position, we update mItemBottoms
2969         // for all columns that this view will occupy.  We set the margins such that mItemBottoms
2970         // is equal for all columns in the view's span.  For LTR layout, we start at targetCol and
2971         // move right, and for RTL, we start at targetCol and move left.
2972         for (int i = 0; i < span; i++) {
2973             final int nextCol = mIsRtlLayout ? targetCol - i : targetCol + i;
2974             rec.setMarginAbove(i, topMost - mItemBottoms[nextCol]);
2975         }
2976 
2977         return rec;
2978     }
2979 
2980     private int getItemWidth(int itemColumnSpan) {
2981         final int colWidth = (getWidth() - getPaddingLeft() - getPaddingRight() -
2982                 mItemMargin * (mColCount - 1)) / mColCount;
2983         return colWidth * itemColumnSpan + mItemMargin * (itemColumnSpan - 1);
2984     }
2985 
2986     /**
2987      * Obtain a populated view from the adapter.  This method checks to see if the view to populate
2988      * is already laid out on screen somewhere by comparing the item ids.
2989      *
2990      * If the view is already laid out, and the view type has not changed, populate the contents
2991      * and return.
2992      *
2993      * If the view is not laid out on screen somewhere, grab a view from the recycler and populate.
2994      *
2995      * NOTE: This method should be called during layout.
2996      *
2997      * TODO: This can probably be consolidated with the overloaded {@link #obtainView(int, View)}.
2998      *
2999      * @param position Position to get the view for.
3000      */
3001     final View obtainView(int position) {
3002         // TODO: This method currently does not support transient state views.
3003 
3004         final Object item = mAdapter.getItem(position);
3005 
3006         View scrap = null;
3007         final int positionViewType = mAdapter.getItemViewType(item, position);
3008 
3009         final long id = mAdapter.getItemId(item, position);
3010         final ViewRectPair viewRectPair = mChildRectsForAnimation.get(id);
3011         if (viewRectPair != null) {
3012             scrap = viewRectPair.view;
3013 
3014             // TODO: Make use of stable ids by retrieving the cached views using stable ids.  In
3015             // theory, we should maintain a list of active views, and then fetch the views
3016             // from that list.  If that fails, then we should go to the recycler.
3017             // For the collection holding stable ids, we must ensure that those views don't get
3018             // repurposed for other items at different positions.
3019         }
3020 
3021         final int scrapViewType = scrap != null &&
3022                 (scrap.getLayoutParams() instanceof LayoutParams) ?
3023                 ((LayoutParams) scrap.getLayoutParams()).viewType : -1;
3024 
3025         if (scrap == null || scrapViewType != positionViewType) {
3026             // If there is no cached view or the cached view's type no longer match the type
3027             // of the item at the specified position, retrieve a new view from the recycler and
3028             // recycle the cached view.
3029             if (scrap != null) {
3030                 // The cached view we had is not valid, so add it to the recycler and
3031                 // remove it from the current layout.
3032                 recycleView(scrap);
3033             }
3034 
3035             scrap = mRecycler.getScrapView(positionViewType);
3036         }
3037 
3038         final int itemColumnSpan = mAdapter.getItemColumnSpan(item, position);
3039         final int itemWidth = getItemWidth(itemColumnSpan);
3040         final View view = mAdapter.getView(item, position, scrap, this, itemWidth);
3041 
3042         ViewGroup.LayoutParams lp = view.getLayoutParams();
3043         if (view.getParent() != this) {
3044             if (lp == null) {
3045                 lp = generateDefaultLayoutParams();
3046             } else if (!checkLayoutParams(lp)) {
3047                 lp = generateLayoutParams(lp);
3048             }
3049 
3050             view.setLayoutParams(lp);
3051         }
3052 
3053         final LayoutParams sglp = (LayoutParams) view.getLayoutParams();
3054         sglp.position = position;
3055         sglp.viewType = positionViewType;
3056         sglp.id = id;
3057         sglp.span = itemColumnSpan;
3058 
3059         // When the view at the positions we are tracking update, make sure to
3060         // update our views as well. That way, we have the correct
3061         // rectangle for comparing when the drag target enters/ leaves the
3062         // placeholder view.
3063         if (isDragReorderingSupported() && mReorderHelper.getDraggedChildId() == id) {
3064             mReorderHelper.updateDraggedChildView(view);
3065             mReorderHelper.updateDraggedOverChildView(view);
3066         }
3067         return view;
3068     }
3069 
3070     /**
3071      * Obtain a populated view from the adapter. If optScrap is non-null and is not
3072      * reused it will be placed in the recycle bin.
3073      *
3074      * @param position position to get view for
3075      * @param optScrap Optional scrap view; will be reused if possible
3076      * @return A new view, a recycled view from mRecycler, or optScrap
3077      */
3078     final View obtainView(int position, View optScrap) {
3079         View view = mRecycler.getTransientStateView(position);
3080         final Object item = mAdapter.getItem(position);
3081         final int positionViewType = mAdapter.getItemViewType(item, position);
3082 
3083         if (view == null) {
3084             // Reuse optScrap if it's of the right type (and not null)
3085             final int optType = optScrap != null ?
3086                     ((LayoutParams) optScrap.getLayoutParams()).viewType : -1;
3087 
3088             final View scrap = optType == positionViewType ?
3089                     optScrap : mRecycler.getScrapView(positionViewType);
3090 
3091             final int itemColumnSpan = mAdapter.getItemColumnSpan(item, position);
3092             final int itemWidth = getItemWidth(itemColumnSpan);
3093             view = mAdapter.getView(item, position, scrap, this, itemWidth);
3094 
3095             if (view != scrap && scrap != null) {
3096                 // The adapter didn't use it; put it back.
3097                 mRecycler.addScrap(scrap);
3098             }
3099 
3100             ViewGroup.LayoutParams lp = view.getLayoutParams();
3101 
3102             if (view.getParent() != this) {
3103                 if (lp == null) {
3104                     lp = generateDefaultLayoutParams();
3105                 } else if (!checkLayoutParams(lp)) {
3106                     lp = generateLayoutParams(lp);
3107                 }
3108 
3109                 view.setLayoutParams(lp);
3110             }
3111         }
3112 
3113         final LayoutParams sglp = (LayoutParams) view.getLayoutParams();
3114         sglp.position = position;
3115         sglp.viewType = positionViewType;
3116         final long id = mAdapter.getItemIdFromView(view, position);
3117         sglp.id = id;
3118         sglp.span = mAdapter.getItemColumnSpan(item, position);
3119 
3120         // When the view at the positions we are tracking update, make sure to
3121         // update our views as well. That way, we have the correct
3122         // rectangle for comparing when the drag target enters/ leaves the
3123         // placeholder view.
3124         if (isDragReorderingSupported() && mReorderHelper.getDraggedChildId() == id) {
3125             mReorderHelper.updateDraggedChildView(view);
3126             mReorderHelper.updateDraggedOverChildView(view);
3127         }
3128 
3129         return view;
3130     }
3131 
3132     /**
3133      * Animation mode to play for new data coming in as well as the stale data that should be
3134      * animated out.
3135      * @param animationIn The animation to play to introduce new or updated data into view
3136      * @param animationOut The animation to play to transition stale data out of view.
3137      */
3138     public void setAnimationMode(AnimationIn animationIn, AnimationOut animationOut) {
3139         mAnimationInMode = animationIn;
3140         mAnimationOutMode = animationOut;
3141     }
3142 
3143     public AnimationIn getAnimationInMode() {
3144         return mAnimationInMode;
3145     }
3146 
3147     public AnimationOut getAnimationOutMode() {
3148         return mAnimationOutMode;
3149     }
3150 
3151     public GridAdapter getAdapter() {
3152         return mAdapter;
3153     }
3154 
3155     public void setAdapter(GridAdapter adapter) {
3156         if (mAdapter != null) {
3157             mAdapter.unregisterDataSetObserver(mObserver);
3158         }
3159 
3160         clearAllState();
3161 
3162         mAdapter = adapter;
3163         mDataChanged = true;
3164         mItemCount = adapter != null ? adapter.getCount() : 0;
3165 
3166         if (adapter != null) {
3167             adapter.registerDataSetObserver(mObserver);
3168             mRecycler.setViewTypeCount(adapter.getViewTypeCount());
3169             mHasStableIds = adapter.hasStableIds();
3170         } else {
3171             mHasStableIds = false;
3172         }
3173 
3174         if (isDragReorderingSupported()) {
3175             updateReorderStates(ReorderUtils.DRAG_STATE_NONE);
3176         }
3177 
3178         updateEmptyStatus();
3179     }
3180 
3181     public void setAdapter(GridAdapter adapter, ScrollState scrollState) {
3182         setAdapter(adapter);
3183         mCurrentScrollState = scrollState;
3184     }
3185 
3186     /**
3187      * Clear all state because the grid will be used for a completely different set of data.
3188      */
3189     private void clearAllState() {
3190         // Clear all layout records and views
3191         mLayoutRecords.clear();
3192         removeAllViews();
3193 
3194         mItemTops = null;
3195         mItemBottoms = null;
3196 
3197         setSelectionToTop();
3198 
3199         // Clear recycler because there could be different view types now
3200         mRecycler.clear();
3201 
3202         // Reset the last touch y coordinate so that any animation/events won't use stale values.
3203         mLastTouchY = 0;
3204 
3205         // Reset the first changed position to 0. At least we will update all views.
3206         mFirstChangedPosition = 0;
3207     }
3208 
3209     /**
3210      * Scroll the list so the first visible position in the grid is the first item in the adapter.
3211      */
3212     public void setSelectionToTop() {
3213         mCurrentScrollState = null;
3214         setFirstPositionAndOffsets(0 /* position */, getPaddingTop() /* offset */);
3215     }
3216 
3217     /**
3218      * Get {@link #mFirstPosition}, which is the adapter position of the View
3219      * returned by getChildAt(0).
3220      */
3221     public int getCurrentFirstPosition() {
3222         return mFirstPosition;
3223     }
3224 
3225     /**
3226      * Indicate whether the scrolling state is currently at the topmost of this grid
3227      * @return boolean Indicates whether the current view is the top most of this grid.
3228      */
3229     private boolean isSelectionAtTop() {
3230         if (mCurrentScrollState != null && mCurrentScrollState.getAdapterPosition() == 0) {
3231             // ScrollState is how far the top of the first child is from the top of the screen, and
3232             // does not include top padding when the adapter position is the first child. If the
3233             // vertical offset of the scroll state is exactly equal to {@link #mItemMargin}, then
3234             // the first item, and therefore the view of the grid, is at the top.
3235             return mCurrentScrollState.getVerticalOffset() == mItemMargin;
3236         }
3237 
3238         return false;
3239     }
3240 
3241     /**
3242      * Set the first position and offset so that on layout, we would start laying out starting
3243      * with the specified position at the top of the view.
3244      * @param position The child position to place at the top of this view.
3245      * @param offset The vertical layout offset of the view at the specified position.
3246      */
3247     public void setFirstPositionAndOffsets(int position, int offset) {
3248         // Reset the first visible position in the grid to be item 0
3249         mFirstPosition = position;
3250         if (mItemTops == null || mItemBottoms == null) {
3251             mItemTops = new int[mColCount];
3252             mItemBottoms = new int[mColCount];
3253         }
3254 
3255         calculateLayoutStartOffsets(offset);
3256     }
3257 
3258     /**
3259      * Restore the view to the states specified by the {@link ScrollState}.
3260      * @param scrollState {@link ScrollState} containing the scroll states to restore to.
3261      */
3262     private void restoreScrollPosition(ScrollState scrollState) {
3263         if (mAdapter == null || scrollState == null || mAdapter.getCount() == 0) {
3264             return;
3265         }
3266 
3267         Log.v(TAG, "[restoreScrollPosition] " + scrollState);
3268 
3269         int targetPosition = 0;
3270         long itemId = -1;
3271 
3272         final int originalPosition = scrollState.getAdapterPosition();
3273         final int adapterCount = mAdapter.getCount();
3274         // ScrollState is defined as the vertical offset of the first item that is laid out
3275         // on screen.  To restore scroll state, we check within a window to see if we can
3276         // find that original first item in this new data set.  If we can, restore that item
3277         // to the first position on screen, offset by its previous vertical offset.  If we
3278         // cannot find that item, then we'll simply layout out everything from the beginning
3279         // again.
3280 
3281         // TODO:  Perhaps it is more efficient if we check the cursor in one direction first
3282         // before going backwards, rather than jumping back and forth as we are doing now.
3283         for (int i = 0; i < SCROLL_RESTORE_WINDOW_SIZE; i++) {
3284             if (originalPosition + i < adapterCount) {
3285                 itemId = mAdapter.getItemId(originalPosition + i);
3286                 if (itemId != -1 && itemId == scrollState.getItemId()) {
3287                     targetPosition = originalPosition + i;
3288                     break;
3289                 }
3290             }
3291 
3292             if (originalPosition - i >= 0 && originalPosition - i < adapterCount) {
3293                 itemId = mAdapter.getItemId(originalPosition - i);
3294                 if (itemId != -1 && itemId == scrollState.getItemId()) {
3295                     targetPosition = originalPosition - i;
3296                     break;
3297                 }
3298             }
3299         }
3300 
3301         // layoutChildren(), fillDown() and fillUp() always apply mItemMargin when laying out
3302         // views.  Since restoring scroll position is effectively laying out a particular child
3303         // as the first child, we need to ensure we strip mItemMargin from the offset, as it
3304         // will be re-applied when the view is laid out.
3305         //
3306         // Since top padding varies with screen orientation and is not stored in the scroll
3307         // state when the scroll adapter position is the first child, we add it here.
3308         int offset = scrollState.getVerticalOffset() - mItemMargin;
3309         if (targetPosition == 0) {
3310             offset += getPaddingTop();
3311         }
3312 
3313         setFirstPositionAndOffsets(targetPosition, offset);
3314         mCurrentScrollState = null;
3315     }
3316 
3317     /**
3318      * Return the current scroll state of this view.
3319      * @return {@link ScrollState} The current scroll state
3320      */
3321     public ScrollState getScrollState() {
3322         final View v = getChildAt(0);
3323         if (v == null) {
3324             return null;
3325         }
3326 
3327         final LayoutParams lp = (LayoutParams) v.getLayoutParams();
3328         // Since top padding varies with screen orientation, it is not stored in the scroll state
3329         // when the scroll adapter position is the first child.
3330         final int offset = (lp.position == 0 ? v.getTop() - getPaddingTop() : v.getTop());
3331         return new ScrollState(lp.id, lp.position, offset);
3332     }
3333 
3334     /**
3335      * NOTE This method is borrowed from {@link ScrollView}.
3336      */
3337     @Override
3338     public boolean requestChildRectangleOnScreen(View child, Rect rectangle,
3339             boolean immediate) {
3340         // offset into coordinate space of this scroll view
3341         rectangle.offset(child.getLeft() - child.getScrollX(),
3342                 child.getTop() - child.getScrollY());
3343 
3344         return scrollToChildRect(rectangle, immediate);
3345     }
3346 
3347     /**
3348      * If rect is off screen, scroll just enough to get it (or at least the
3349      * first screen size chunk of it) on screen.
3350      * NOTE This method is borrowed from {@link ScrollView}.
3351      *
3352      * @param rect      The rectangle.
3353      * @param immediate True to scroll immediately without animation. Not used here.
3354      * @return true if scrolling was performed
3355      */
3356     private boolean scrollToChildRect(Rect rect, boolean immediate) {
3357         final int delta = computeScrollDeltaToGetChildRectOnScreen(rect);
3358         final boolean scroll = delta != 0;
3359         if (scroll) {
3360             // TODO smoothScrollBy if immediate is false.
3361             scrollBy(0, delta);
3362         }
3363         return scroll;
3364     }
3365 
3366     @Override
3367     protected void onSizeChanged(int w, int h, int oldw, int oldh) {
3368         super.onSizeChanged(w, h, oldw, oldh);
3369 
3370         if (mOnSizeChangedListener != null) {
3371             mOnSizeChangedListener.onSizeChanged(w, h, oldw, oldh);
3372         }
3373 
3374         // NOTE Below is borrowed from {@link ScrollView}.
3375         final View currentFocused = findFocus();
3376         if (null == currentFocused || this == currentFocused) {
3377             return;
3378         }
3379 
3380         // If the currently-focused view was visible on the screen when the
3381         // screen was at the old height, then scroll the screen to make that
3382         // view visible with the new screen height.
3383         if (isWithinDeltaOfScreen(currentFocused, 0, oldh)) {
3384             currentFocused.getDrawingRect(mTempRect);
3385             offsetDescendantRectToMyCoords(currentFocused, mTempRect);
3386             scrollBy(0, computeScrollDeltaToGetChildRectOnScreen(mTempRect));
3387         }
3388     }
3389 
3390     /**
3391      *
3392      * NOTE This method is borrowed from {@link ScrollView}.
3393      *
3394      * @return whether the descendant of this scroll view is within delta
3395      *  pixels of being on the screen.
3396      */
3397     private boolean isWithinDeltaOfScreen(View descendant, int delta, int height) {
3398         descendant.getDrawingRect(mTempRect);
3399         offsetDescendantRectToMyCoords(descendant, mTempRect);
3400 
3401         return (mTempRect.bottom + delta) >= getScrollY()
3402                 && (mTempRect.top - delta) <= (getScrollY() + height);
3403     }
3404 
3405     /**
3406      * NOTE: borrowed from {@link GridView}
3407      * Comments from {@link View}
3408      *
3409      * Compute the vertical extent of the vertical scrollbar's thumb within the vertical range.
3410      * This value is used to compute the length of the thumb within the scrollbar's track.
3411      * The range is expressed in arbitrary units that must be the same as the units used by
3412      * {@link #computeVerticalScrollRange} and {@link #computeVerticalScrollOffset}.
3413      *
3414      * The default extent is the drawing height of this view.
3415      *
3416      * @return the vertical extent of the scrollbar's thumb
3417      */
3418     @Override
3419     protected int computeVerticalScrollExtent() {
3420 
3421         final int count = getChildCount();
3422         if (count > 0) {
3423             if (mSmoothScrollbarEnabled) {
3424                 final int rowCount = (count + mColCount - 1) / mColCount;
3425                 int extent = rowCount * SCROLLING_ESTIMATED_ITEM_HEIGHT;
3426 
3427                 View view = getChildAt(0);
3428                 final int top = view.getTop();
3429                 int height = view.getHeight();
3430                 if (height > 0) {
3431                     extent += (top * SCROLLING_ESTIMATED_ITEM_HEIGHT) / height;
3432                 }
3433 
3434                 view = getChildAt(count - 1);
3435                 final int bottom = view.getBottom();
3436                 height = view.getHeight();
3437                 if (height > 0) {
3438                     extent -= ((bottom - getHeight()) * SCROLLING_ESTIMATED_ITEM_HEIGHT) / height;
3439                 }
3440 
3441                 return extent;
3442             } else {
3443                 return 1;
3444             }
3445         }
3446         return 0;
3447     }
3448 
3449     /**
3450      * NOTE: borrowed from {@link GridView} and altered as appropriate to accommodate for
3451      * {@link StaggeredGridView}
3452      *
3453      * Comments from {@link View}
3454      *
3455      * Compute the vertical offset of the vertical scrollbar's thumb within the horizontal range.
3456      * This value is used to compute the position of the thumb within the scrollbar's track.
3457      * The range is expressed in arbitrary units that must be the same as the units used by
3458      * {@link #computeVerticalScrollRange()} and {@link #computeVerticalScrollExtent()}.
3459      *
3460      * The default offset is the scroll offset of this view.
3461      *
3462      * @return the vertical offset of the scrollbar's thumb
3463      */
3464     @Override
3465     protected int computeVerticalScrollOffset() {
3466         final int firstPosition = mFirstPosition;
3467         final int childCount = getChildCount();
3468         final int paddingTop = getPaddingTop();
3469 
3470         if (firstPosition >= 0 && childCount > 0) {
3471             if (mSmoothScrollbarEnabled) {
3472                 final View view = getChildAt(0);
3473                 final int top = view.getTop();
3474                 final int currentTopViewHeight = view.getHeight();
3475                 if (currentTopViewHeight > 0) {
3476                     // In an ideal world, all items would have a fixed height that we would know
3477                     // a priori, calculating the scroll offset would simply be:
3478                     //     [A] (mFirstPosition * fixedHeight) - childView[0].top
3479                     //         where childView[0] is the first view on screen.
3480                     //
3481                     // However, given that we do not know the height ahead of time, and that each
3482                     // item in this grid can have varying heights, we'd need to assign an arbitrary
3483                     // item height (SCROLLING_ESTIMATED_ITEM_HEIGHT) in order to estimate the scroll
3484                     // offset.  The previous equation thus transforms to:
3485                     //     [B] (mFirstPosition * SCROLLING_ESTIMATED_ITEM_HEIGHT) -
3486                     //         ((childView[0].top * SCROLLING_ESTIMATED_ITEM_HEIGHT) /
3487                     //          childView[0].height)
3488                     //
3489                     // Equation [B] gives a pretty good calculation of the offset if this were a
3490                     // single column grid view, for a multi-column grid, one slight modification is
3491                     // needed:
3492                     //     [C] ((mFirstPosition * SCROLLING_ESTIMATED_ITEM_HEIGHT) / mColCount) -
3493                     //         ((childView[0].top * SCROLLING_ESTIMATED_ITEM_HEIGHT) /
3494                     //          childView[0].height)
3495                     final int estimatedScrollOffset =
3496                             ((firstPosition * SCROLLING_ESTIMATED_ITEM_HEIGHT) / mColCount) -
3497                             ((top * SCROLLING_ESTIMATED_ITEM_HEIGHT) / currentTopViewHeight);
3498 
3499                     final int rowCount = (mItemCount + mColCount - 1) / mColCount;
3500                     final int overScrollCompensation = (int) ((float) getScrollY() / getHeight() *
3501                             rowCount * SCROLLING_ESTIMATED_ITEM_HEIGHT);
3502 
3503                     int val = Math.max(estimatedScrollOffset + overScrollCompensation, 0);
3504                     // If mFirstPosition is currently the very first item in the adapter, check to
3505                     // see if we need to take into account any top padding.  This is so that we
3506                     // don't return 0 when in fact the user may still be scrolling through some
3507                     // top padding.
3508                     if (firstPosition == 0 && paddingTop > 0) {
3509                         val += paddingTop - top + mItemMargin;
3510                     }
3511                     return val;
3512                 }
3513             } else {
3514                 int index;
3515                 final int count = mItemCount;
3516                 if (firstPosition == 0) {
3517                     index = 0;
3518                 } else if (firstPosition + childCount == count) {
3519                     index = count;
3520                 } else {
3521                     index = firstPosition + childCount / 2;
3522                 }
3523                 return (int) (firstPosition + childCount * (index / (float) count));
3524             }
3525         }
3526 
3527         return paddingTop;
3528     }
3529 
3530     /**
3531      * NOTE: borrowed from {@link GridView} and altered as appropriate to accommodate for
3532      * {@link StaggeredGridView}
3533      *
3534      * Comments from {@link View}
3535      *
3536      * Compute the vertical range that the vertical scrollbar represents.
3537      * The range is expressed in arbitrary units that must be the same as the units used by
3538      * {@link #computeVerticalScrollExtent} and {@link #computeVerticalScrollOffset}.
3539      *
3540      * The default range is the drawing height of this view.
3541      *
3542      * @return the total vertical range represented by the vertical scrollbar
3543      */
3544     @Override
3545     protected int computeVerticalScrollRange() {
3546         final int rowCount = (mItemCount + mColCount - 1) / mColCount;
3547         int result = Math.max(rowCount * SCROLLING_ESTIMATED_ITEM_HEIGHT, 0);
3548 
3549         if (mSmoothScrollbarEnabled) {
3550             if (getScrollY() != 0) {
3551                 // Compensate for overscroll
3552                 result += Math.abs((int) ((float) getScrollY() / getHeight() * rowCount
3553                         * SCROLLING_ESTIMATED_ITEM_HEIGHT));
3554             }
3555         } else {
3556             result = mItemCount;
3557         }
3558 
3559         return result;
3560     }
3561 
3562     /**
3563      * Compute the amount to scroll in the Y direction in order to get
3564      * a rectangle completely on the screen (or, if taller than the screen,
3565      * at least the first screen size chunk of it).
3566      *
3567      * NOTE This method is borrowed from {@link ScrollView}.
3568      *
3569      * @param rect The rect.
3570      * @return The scroll delta.
3571      */
3572     protected int computeScrollDeltaToGetChildRectOnScreen(Rect rect) {
3573         if (getChildCount() == 0) {
3574             return 0;
3575         }
3576 
3577         final int height = getHeight();
3578         final int fadingEdge = getVerticalFadingEdgeLength();
3579 
3580         int screenTop = getScrollY();
3581         int screenBottom = screenTop + height;
3582 
3583         // leave room for top fading edge as long as rect isn't at very top
3584         if (rect.top > 0) {
3585             screenTop += fadingEdge;
3586         }
3587 
3588         // leave room for bottom fading edge as long as rect isn't at very bottom
3589         if (rect.bottom < getHeight()) {
3590             screenBottom -= fadingEdge;
3591         }
3592 
3593         int scrollYDelta = 0;
3594 
3595         if (rect.bottom > screenBottom && rect.top > screenTop) {
3596             // need to move down to get it in view: move down just enough so
3597             // that the entire rectangle is in view (or at least the first
3598             // screen size chunk).
3599 
3600             if (rect.height() > height) {
3601                 // just enough to get screen size chunk on
3602                 scrollYDelta = screenTop - rect.top;
3603             } else {
3604                 // get entire rect at bottom of screen
3605                 scrollYDelta = screenBottom - rect.bottom;
3606             }
3607         } else if (rect.top < screenTop && rect.bottom < screenBottom) {
3608             // need to move up to get it in view: move up just enough so that
3609             // entire rectangle is in view (or at least the first screen
3610             // size chunk of it).
3611 
3612             if (rect.height() > height) {
3613                 // screen size chunk
3614                 scrollYDelta = screenBottom - rect.bottom;
3615             } else {
3616                 // entire rect at top
3617                 scrollYDelta = screenTop - rect.top;
3618             }
3619         }
3620         return scrollYDelta;
3621     }
3622 
3623     @Override
3624     protected LayoutParams generateDefaultLayoutParams() {
3625         return new LayoutParams(LayoutParams.WRAP_CONTENT);
3626     }
3627 
3628     @Override
3629     protected LayoutParams generateLayoutParams(ViewGroup.LayoutParams lp) {
3630         return new LayoutParams(lp);
3631     }
3632 
3633     @Override
3634     protected boolean checkLayoutParams(ViewGroup.LayoutParams lp) {
3635         return lp instanceof LayoutParams;
3636     }
3637 
3638     @Override
3639     public ViewGroup.LayoutParams generateLayoutParams(AttributeSet attrs) {
3640         return new LayoutParams(getContext(), attrs);
3641     }
3642 
3643     @Override
3644     public Parcelable onSaveInstanceState() {
3645         final Parcelable superState = super.onSaveInstanceState();
3646         final SavedState ss = new SavedState(superState);
3647         final int position = mFirstPosition;
3648         ss.position = position;
3649         if (position >= 0 && mAdapter != null && position < mAdapter.getCount()) {
3650             ss.firstId = mAdapter.getItemId(position);
3651         }
3652         if (getChildCount() > 0) {
3653             // Since top padding varies with screen orientation, it is not stored in the scroll
3654             // state when the scroll adapter position is the first child.
3655             ss.topOffset = position == 0 ?
3656                     getChildAt(0).getTop() - getPaddingTop() : getChildAt(0).getTop();
3657         }
3658         return ss;
3659     }
3660 
3661     @Override
3662     public void onRestoreInstanceState(Parcelable state) {
3663         final SavedState ss = (SavedState) state;
3664         super.onRestoreInstanceState(ss.getSuperState());
3665         mDataChanged = true;
3666         mFirstPosition = ss.position;
3667         mCurrentScrollState = new ScrollState(ss.firstId, ss.position, ss.topOffset);
3668         requestLayout();
3669     }
3670 
3671     public static class LayoutParams extends ViewGroup.LayoutParams {
3672         private static final int[] LAYOUT_ATTRS = new int[] {
3673             android.R.attr.layout_span
3674         };
3675 
3676         private static final int SPAN_INDEX = 0;
3677 
3678         /**
3679          * The number of columns this item should span
3680          */
3681         public int span = 1;
3682 
3683         /**
3684          * Item position this view represents
3685          */
3686         public int position = -1;
3687 
3688         /**
3689          * Type of this view as reported by the adapter
3690          */
3691         int viewType;
3692 
3693         /**
3694          * The column this view is occupying
3695          */
3696         int column;
3697 
3698         /**
3699          * The stable ID of the item this view displays
3700          */
3701         long id = -1;
3702 
3703         /**
3704          * The position where reordering can happen for this view
3705          */
3706         public int reorderingArea = ReorderUtils.REORDER_AREA_NONE;
3707 
3708         public LayoutParams(int height) {
3709             super(MATCH_PARENT, height);
3710 
3711             if (this.height == MATCH_PARENT) {
3712                 Log.w(TAG, "Constructing LayoutParams with height FILL_PARENT - " +
3713                         "impossible! Falling back to WRAP_CONTENT");
3714                 this.height = WRAP_CONTENT;
3715             }
3716         }
3717 
3718         public LayoutParams(Context c, AttributeSet attrs) {
3719             super(c, attrs);
3720 
3721             if (this.width != MATCH_PARENT) {
3722                 Log.w(TAG, "Inflation setting LayoutParams width to " + this.width +
3723                         " - must be MATCH_PARENT");
3724                 this.width = MATCH_PARENT;
3725             }
3726             if (this.height == MATCH_PARENT) {
3727                 Log.w(TAG, "Inflation setting LayoutParams height to MATCH_PARENT - " +
3728                         "impossible! Falling back to WRAP_CONTENT");
3729                 this.height = WRAP_CONTENT;
3730             }
3731 
3732             final TypedArray a = c.obtainStyledAttributes(attrs, LAYOUT_ATTRS);
3733             span = a.getInteger(SPAN_INDEX, 1);
3734             a.recycle();
3735         }
3736 
3737         public LayoutParams(ViewGroup.LayoutParams other) {
3738             super(other);
3739 
3740             if (this.width != MATCH_PARENT) {
3741                 Log.w(TAG, "Constructing LayoutParams with width " + this.width +
3742                         " - must be MATCH_PARENT");
3743                 this.width = MATCH_PARENT;
3744             }
3745             if (this.height == MATCH_PARENT) {
3746                 Log.w(TAG, "Constructing LayoutParams with height MATCH_PARENT - " +
3747                         "impossible! Falling back to WRAP_CONTENT");
3748                 this.height = WRAP_CONTENT;
3749             }
3750         }
3751     }
3752 
3753     private class RecycleBin {
3754         private ArrayList<View>[] mScrapViews;
3755         private int mViewTypeCount;
3756         private int mMaxScrap;
3757 
3758         private SparseArray<View> mTransientStateViews;
3759 
3760         public void setViewTypeCount(int viewTypeCount) {
3761             if (viewTypeCount < 1) {
3762                 throw new IllegalArgumentException("Must have at least one view type (" +
3763                         viewTypeCount + " types reported)");
3764             }
3765             if (viewTypeCount == mViewTypeCount) {
3766                 return;
3767             }
3768 
3769             final ArrayList<View>[] scrapViews = new ArrayList[viewTypeCount];
3770             for (int i = 0; i < viewTypeCount; i++) {
3771                 scrapViews[i] = new ArrayList<View>();
3772             }
3773             mViewTypeCount = viewTypeCount;
3774             mScrapViews = scrapViews;
3775         }
3776 
3777         public void clear() {
3778             final int typeCount = mViewTypeCount;
3779             for (int i = 0; i < typeCount; i++) {
3780                 mScrapViews[i].clear();
3781             }
3782             if (mTransientStateViews != null) {
3783                 mTransientStateViews.clear();
3784             }
3785         }
3786 
3787         public void clearTransientViews() {
3788             if (mTransientStateViews != null) {
3789                 mTransientStateViews.clear();
3790             }
3791         }
3792 
3793         public void addScrap(View v) {
3794             if (!(v.getLayoutParams() instanceof LayoutParams)) {
3795                 return;
3796             }
3797 
3798             final LayoutParams lp = (LayoutParams) v.getLayoutParams();
3799             if (ViewCompat.hasTransientState(v)) {
3800                 if (mTransientStateViews == null) {
3801                     mTransientStateViews = new SparseArray<View>();
3802                 }
3803                 mTransientStateViews.put(lp.position, v);
3804                 return;
3805             }
3806 
3807             final int childCount = getChildCount();
3808             if (childCount > mMaxScrap) {
3809                 mMaxScrap = childCount;
3810             }
3811 
3812             // Clear possible modified states applied to the view when adding to the recycler.
3813             // This view may have been part of a cancelled animation, so clear that state so that
3814             // future consumer of this view won't have to deal with states from its past life.
3815             v.setTranslationX(0);
3816             v.setTranslationY(0);
3817             v.setRotation(0);
3818             v.setAlpha(1.0f);
3819             v.setScaleY(1.0f);
3820 
3821             final ArrayList<View> scrap = mScrapViews[lp.viewType];
3822             if (scrap.size() < mMaxScrap) {
3823                 // The number of scraps have not yet exceeded our limit, check to see that this
3824                 // view does not already exist in the recycler.  This can happen if a caller
3825                 // mistakenly calls addScrap(view) multiple times for the same view.
3826                 if (!scrap.contains(v)) {
3827                     scrap.add(v);
3828                 }
3829             }
3830         }
3831 
3832         public View getTransientStateView(int position) {
3833             if (mTransientStateViews == null) {
3834                 return null;
3835             }
3836 
3837             final View result = mTransientStateViews.get(position);
3838             if (result != null) {
3839                 mTransientStateViews.remove(position);
3840             }
3841             return result;
3842         }
3843 
3844         public View getScrapView(int type) {
3845             final ArrayList<View> scrap = mScrapViews[type];
3846             if (scrap.isEmpty()) {
3847                 return null;
3848             }
3849 
3850             final int index = scrap.size() - 1;
3851             final View result = scrap.remove(index);
3852 
3853             return result;
3854         }
3855 
3856         // TODO: Implement support to maintain a list of active views so that we can make use of
3857         // stable ids to retrieve the same view that is currently laid out for a particular item.
3858         // Currently, all views "recycled" are shoved into the same collection, this may not be
3859         // the most effective way.  Refer to the RecycleBin as implemented for AbsListView.
3860         public View getView(int type, long stableId) {
3861             final ArrayList<View> scrap = mScrapViews[type];
3862             if (scrap.isEmpty()) {
3863                 return null;
3864             }
3865 
3866             for (int i = 0; i < scrap.size(); i++) {
3867                 final View v = scrap.get(i);
3868                 final LayoutParams lp = (LayoutParams) v.getLayoutParams();
3869                 if (lp.id == stableId) {
3870                     scrap.remove(i);
3871                     return v;
3872                 }
3873             }
3874 
3875             return null;
3876         }
3877     }
3878 
3879     private class AdapterDataSetObserver extends DataSetObserver {
3880         @Override
3881         public void onChanged() {
3882             mDataChanged = true;
3883 
3884             mItemCount = mAdapter.getCount();
3885             mFirstChangedPosition = mAdapter.getFirstChangedPosition();
3886             if (mFirstPosition >= mItemCount) {
3887                 // If the latest data set has fewer data items than mFirstPosition, we will not be
3888                 // able to accurately restore scroll state, so just reset to the top.
3889                 mFirstPosition = 0;
3890                 mCurrentScrollState = null;
3891             }
3892 
3893             // TODO: Consider matching these back up if we have stable IDs.
3894             mRecycler.clearTransientViews();
3895 
3896             if (mHasStableIds) {
3897                 // If we will animate the transition to the new layout, cache the current positions
3898                 // of the visible children. This is before any views get removed below.
3899                 cacheChildRects();
3900             } else {
3901                 // Clear all layout records
3902                 mLayoutRecords.clear();
3903 
3904                 // Reset item bottoms to be equal to item tops
3905                 final int colCount = mColCount;
3906                 for (int i = 0; i < colCount; i++) {
3907                     mItemBottoms[i] = mItemTops[i];
3908                 }
3909             }
3910 
3911             updateEmptyStatus();
3912 
3913             // TODO: consider repopulating in a deferred runnable instead
3914             // (so that successive changes may still be batched)
3915             requestLayout();
3916         }
3917 
3918         @Override
3919         public void onInvalidated() {
3920         }
3921     }
3922 
3923     static class SavedState extends BaseSavedState {
3924         long firstId = -1;
3925         int position;
3926 
3927         // topOffset is the vertical value that the view specified by position should
3928         // start rendering from.  If it is 0, the view would be at the top of the grid.
3929         int topOffset;
3930 
3931         SavedState(Parcelable superState) {
3932             super(superState);
3933         }
3934 
3935         private SavedState(Parcel in) {
3936             super(in);
3937             firstId = in.readLong();
3938             position = in.readInt();
3939             topOffset = in.readInt();
3940         }
3941 
3942         @Override
3943         public void writeToParcel(Parcel out, int flags) {
3944             super.writeToParcel(out, flags);
3945             out.writeLong(firstId);
3946             out.writeInt(position);
3947             out.writeInt(topOffset);
3948         }
3949 
3950         @Override
3951         public String toString() {
3952             return "StaggereGridView.SavedState{"
3953                         + Integer.toHexString(System.identityHashCode(this))
3954                         + " firstId=" + firstId
3955                         + " position=" + position + "}";
3956         }
3957 
3958         public static final Parcelable.Creator<SavedState> CREATOR
3959                 = new Parcelable.Creator<SavedState>() {
3960             @Override
3961             public SavedState createFromParcel(Parcel in) {
3962                 return new SavedState(in);
3963             }
3964 
3965             @Override
3966             public SavedState[] newArray(int size) {
3967                 return new SavedState[size];
3968             }
3969         };
3970     }
3971 
3972     public void setDropListener(ReorderListener listener) {
3973         mReorderHelper = new ReorderHelper(listener, this);
3974     }
3975 
3976     public void setScrollListener(ScrollListener listener) {
3977         mScrollListener = listener;
3978     }
3979 
3980     public void setOnSizeChangedListener(OnSizeChangedListener listener) {
3981         mOnSizeChangedListener = listener;
3982     }
3983 
3984     /**
3985      * Helper class to store a {@link View} with its corresponding layout positions
3986      * as a {@link Rect}.
3987      */
3988     private static class ViewRectPair {
3989         public final View view;
3990         public final Rect rect;
3991 
3992         public ViewRectPair(View v, Rect r) {
3993             view = v;
3994             rect = r;
3995         }
3996     }
3997 
3998     public static class ScrollState implements Parcelable {
3999         private final long mItemId;
4000         private final int mAdapterPosition;
4001 
4002         // The offset that the view specified by mAdapterPosition should start rendering from.  If
4003         // this value is 0, then the view would be rendered from the very top of this grid.
4004         private int mVerticalOffset;
4005 
4006         public ScrollState(long itemId, int adapterPosition, int offset) {
4007             mItemId = itemId;
4008             mAdapterPosition = adapterPosition;
4009             mVerticalOffset = offset;
4010         }
4011 
4012         private ScrollState(Parcel in) {
4013             mItemId = in.readLong();
4014             mAdapterPosition = in.readInt();
4015             mVerticalOffset = in.readInt();
4016         }
4017 
4018         public long getItemId() {
4019             return mItemId;
4020         }
4021 
4022         public int getAdapterPosition() {
4023             return mAdapterPosition;
4024         }
4025 
4026         public void setVerticalOffset(int offset) {
4027             mVerticalOffset = offset;
4028         }
4029 
4030         public int getVerticalOffset() {
4031             return mVerticalOffset;
4032         }
4033 
4034         @Override
4035         public int describeContents() {
4036             return 0;
4037         }
4038 
4039         @Override
4040         public void writeToParcel(Parcel dest, int flags) {
4041             dest.writeLong(mItemId);
4042             dest.writeInt(mAdapterPosition);
4043             dest.writeInt(mVerticalOffset);
4044         }
4045 
4046         public static final Parcelable.Creator<ScrollState> CREATOR =
4047                 new Parcelable.Creator<ScrollState>() {
4048             @Override
4049             public ScrollState createFromParcel(Parcel source) {
4050                 return new ScrollState(source);
4051             }
4052 
4053             @Override
4054             public ScrollState[] newArray(int size) {
4055                 return new ScrollState[size];
4056             }
4057         };
4058 
4059         @Override
4060         public String toString() {
4061             return "ScrollState {mItemId=" + mItemId +
4062                     " mAdapterPosition=" + mAdapterPosition +
4063                     " mVerticalOffset=" + mVerticalOffset + "}";
4064         }
4065     }
4066 
4067     /**
4068      * Listener of {@Link StaggeredGridView} for grid size change.
4069      */
4070     public interface OnSizeChangedListener {
4071         void onSizeChanged(int width, int height, int oldWidth, int oldHeight);
4072     }
4073 
4074     /**
4075      * Listener of {@Link StaggeredGridView} for scroll change.
4076      */
4077     public interface ScrollListener {
4078 
4079         /**
4080          * Called when scroll happens on this view.
4081          *
4082          * @param offset The scroll offset amount.
4083          * @param currentScrollY The current y position of this view.
4084          * @param maxScrollY The maximum amount of scroll possible in this view.
4085          */
4086         void onScrollChanged(int offset, int currentScrollY, int maxScrollY);
4087     }
4088 
4089     /**
4090      * Listener of {@link StaggeredGridView} for animations.  This listener is responsible
4091      * for playing all animations created by this {@link StaggeredGridView}
4092      */
4093     public interface AnimationListener {
4094         /**
4095          * Called when animations are ready to be played
4096          * @param animationMode The current animation mode based on the state of the data.  Valid
4097          * animation modes are {@link ANIMATION_MODE_NONE}, {@link ANIMATION_MODE_NEW_DATA}, and
4098          * {@link ANIMATION_MODE_UPDATE_DATA}.
4099          * @param animators The list of animators to be played
4100          */
4101         void onAnimationReady(int animationMode, List<Animator> animators);
4102     }
4103 
4104     /**
4105      * Listener of {@link StaggeredGridView} for drag and drop reordering of child views.
4106      */
4107     public interface ReorderListener {
4108 
4109         /**
4110          * onPickedUp is called to notify listeners that an item has been picked up for reordering.
4111          * @param draggedChild the original child view that picked up.
4112          */
4113         void onPickedUp(View draggedChild);
4114 
4115         /**
4116          * onDrop is called to notify listeners that an intent to drop the
4117          * item at position "from" over the position "target"
4118          * @param draggedView the original child view that was dropped
4119          * @param sourcePosition the original position where the item was dragged from
4120          * @param targetPosition the target position where the item is dropped at
4121          */
4122         void onDrop(View draggedView, int sourcePosition, int targetPosition);
4123 
4124         /**
4125          * onCancelDrag is called to notify listeners that the drag event has been cancelled.
4126          * @param draggediew the original child view that was dragged.
4127          */
4128         void onCancelDrag(View draggediew);
4129 
4130         /**
4131          * onReorder is called to notify listeners that an intent to move the
4132          * item at position "from" to position "to"
4133          * @param draggedView the original child view that was dragged
4134          * @param id id of the original item that was picked up
4135          * @param from
4136          * @param to the target position where the item is dropped at
4137          */
4138         boolean onReorder(View draggedView, long id, int from, int to);
4139 
4140         /**
4141          * Event handler for a drag entering the {@link StaggeredGridView} element's
4142          * reordering area.
4143          * @param view The child view that just received an enter event on the reordering area.
4144          * @param position The adapter position of the view that just received an enter event.
4145          */
4146         void onEnterReorderArea(View view, int position);
4147     }
4148 }
4149