Home | History | Annotate | Download | only in sgv
      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 
    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 
    295         public final int getMarginAbove(int col) {
    296             if (mMargins == null) {
    297                 return 0;
    298             }
    299             return mMargins[col * 2];
    300         }
    301 
    302         public final int getMarginBelow(int col) {
    303             if (mMargins == null) {
    304                 return 0;
    305             }
    306             return mMargins[col * 2 + 1];
    307         }
    308 
    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 
    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
    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 
    387     public StaggeredGridView(Context context) {
    388         this(context, null);
    389     }
    390 
    391     public StaggeredGridView(Context context, AttributeSet attrs) {
    392         this(context, attrs, 0);
    393     }
    394 
    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")
    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      */
    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 
    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      */
    480     public void setGuardAgainstJaggedEdges(boolean guardAgainstJaggedEdges) {
    481         mGuardAgainstJaggedEdges = guardAgainstJaggedEdges;
    482     }
    483 
    484     /**
    485      * Set a minimum column width for
    486      * @param minColWidth
    487      */
    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      */
    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 
    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      */
    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      */
    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      */
    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      */
    576     public float getLastTouchY() {
    577         return mLastTouchY;
    578     }
    579 
    580     /**
    581      * Enable drag reordering of child items.
    582      */
    583     public void enableDragReordering() {
    584         mIsDragReorderingEnabled = true;
    585     }
    586 
    587     /**
    588      * Disable drag reordering of child items.
    589      */
    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      */
    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      */
    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      */
    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 
    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      */
    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      */
    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      */
    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      */
    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      */
    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
    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
    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 
   1012     private void resetScroller() {
   1013         mTouchMode = TOUCH_MODE_IDLE;
   1014         mTopEdge.finish();
   1015         mBottomEdge.finish();
   1016         mScroller.abortAnimation();
   1017     }
   1018 
   1019     @Override
   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
   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      */
   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 
   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      */
   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.
   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      */
   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 
   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
   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 
   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