com.deepak.myclock.widget.sgv.StaggeredGridView.java Source code

Java tutorial

Introduction

Here is the source code for com.deepak.myclock.widget.sgv.StaggeredGridView.java

Source

/*
 * Copyright (C) 2012 The Android Open Source Project
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *      http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

package com.deepak.myclock.widget.sgv;

import android.animation.Animator;
import android.animation.AnimatorListenerAdapter;
import android.animation.AnimatorSet;
import android.animation.ValueAnimator;
import android.annotation.SuppressLint;
import android.content.Context;
import android.content.res.TypedArray;
import android.database.DataSetObserver;
import android.graphics.Bitmap;
import android.graphics.Canvas;
import android.graphics.PixelFormat;
import android.graphics.Point;
import android.graphics.Rect;
import android.os.Handler;
import android.os.Parcel;
import android.os.Parcelable;
import android.support.v4.util.SparseArrayCompat;
import android.support.v4.view.MotionEventCompat;
import android.support.v4.view.VelocityTrackerCompat;
import android.support.v4.view.ViewCompat;
import android.support.v4.widget.EdgeEffectCompat;
import android.util.AttributeSet;
import android.util.Log;
import android.util.SparseArray;
import android.view.DragEvent;
import android.view.Gravity;
import android.view.MotionEvent;
import android.view.VelocityTracker;
import android.view.View;
import android.view.ViewConfiguration;
import android.view.ViewGroup;
import android.view.WindowManager;
import android.widget.GridView;
import android.widget.ImageView;
import android.widget.ScrollView;

import java.util.ArrayList;
import java.util.Arrays;
import java.util.HashMap;
import java.util.List;
import java.util.Map;

/**
 * Temporarily copied from support v4 library so that StaggeredGridView can access
 * animation APIs on the current SDK version.
 */
/**
 * ListView and GridView just not complex enough? Try StaggeredGridView!
 *
 * <p>StaggeredGridView presents a multi-column grid with consistent column sizes
 * but varying row sizes between the columns. Each successive item from a
 * {@link android.widget.ListAdapter ListAdapter} will be arranged from top to bottom,
 * left to right. The largest vertical gap is always filled first.</p>
 *
 * <p>Item views may span multiple columns as specified by their {@link LayoutParams}.
 * The attribute <code>android:layout_span</code> may be used when inflating
 * item views from xml.</p>
 */
public class StaggeredGridView extends ViewGroup {

    private static final String TAG = "Clock-" + StaggeredGridView.class.getSimpleName();

    /*
     * There are a few things you should know if you're going to make modifications
     * to StaggeredGridView.
     *
     * Like ListView, SGV populates from an adapter and recycles views that fall out
     * of the visible boundaries of the grid. A few invariants always hold:
     *
     * - mFirstPosition is the adapter position of the View returned by getChildAt(0).
     * - Any child index can be translated to an adapter position by adding mFirstPosition.
     * - Any adapter position can be translated to a child index by subtracting mFirstPosition.
     * - Views for items in the range [mFirstPosition, mFirstPosition + getChildCount()) are
     *   currently attached to the grid as children. All other adapter positions do not have
     *   active views.
     *
     * This means a few things thanks to the staggered grid's nature. Some views may stay attached
     * long after they have scrolled offscreen if removing and recycling them would result in
     * breaking one of the invariants above.
     *
     * LayoutRecords are used to track data about a particular item's layout after the associated
     * view has been removed. These let positioning and the choice of column for an item
     * remain consistent even though the rules for filling content up vs. filling down vary.
     *
     * Whenever layout parameters for a known LayoutRecord change, other LayoutRecords before
     * or after it may need to be invalidated. e.g. if the item's height or the number
     * of columns it spans changes, all bets for other items in the same direction are off
     * since the cached information no longer applies.
     */

    private GridAdapter mAdapter;

    public static final int COLUMN_COUNT_AUTO = -1;

    /**
     * The window size to search for a specific item when restoring scroll position.
     */
    private final int SCROLL_RESTORE_WINDOW_SIZE = 10;

    private static final int CHILD_TO_REORDER_AREA_RATIO = 4;

    private static final int SINGLE_COL_REORDERING_AREA_SIZE = 30;

    // Time delay in milliseconds between posting each scroll runnables.
    private static final int SCROLL_HANDLER_DELAY = 5;

    // The default rate of pixels to scroll by when a child view is dragged towards the
    // upper and lower bound of this view.
    private static final int DRAG_SCROLL_RATE = 10;

    public static final int ANIMATION_DELAY_IN_MS = 50;

    private SgvAnimationHelper.AnimationIn mAnimationInMode = SgvAnimationHelper.AnimationIn.NONE;
    private SgvAnimationHelper.AnimationOut mAnimationOutMode = SgvAnimationHelper.AnimationOut.NONE;

    private AnimatorSet mCurrentRunningAnimatorSet = null;

    /**
     * Flag to indicate whether the current running animator set was canceled before it reaching
     * the end of the animations.  This flag is used to help indicate whether the next set of
     * animators should resume from where the last animator set left off.
     */
    boolean mIsCurrentAnimationCanceled = false;

    private int mColCountSetting = 2;
    private int mColCount = 2;
    private int mMinColWidth = 0;
    private int mItemMargin = 0;

    private int[] mItemTops;
    private int[] mItemBottoms;

    private final Rect mTempRect = new Rect();

    private boolean mFastChildLayout;
    private boolean mPopulating;
    private boolean mInLayout;

    private boolean mIsRtlLayout;

    private final RecycleBin mRecycler = new RecycleBin();

    private final AdapterDataSetObserver mObserver = new AdapterDataSetObserver();

    private boolean mDataChanged;
    private int mItemCount;

    /**
     * After data set change, we ask adapter the first view that changed.
     * Any view from 0 to mFirstChangedPosition - 1 is not changed.
     */
    private int mFirstChangedPosition;

    /**
     * If set to true, then we guard against jagged edges in the grid by doing expensive
     * computation. Otherwise if this is false, we skip the computation.
     */
    private boolean mGuardAgainstJaggedEdges;

    private boolean mHasStableIds;

    /**
     * List of all views to animate out.  This is used when we need to animate out stale views.
     */
    private final List<View> mViewsToAnimateOut = new ArrayList<View>();

    private int mFirstPosition;

    private long mFocusedChildIdToScrollIntoView;
    private ScrollState mCurrentScrollState;

    private final int mTouchSlop;
    private final int mMaximumVelocity;
    private final int mFlingVelocity;
    private float mLastTouchY = 0;
    private float mTouchRemainderY;
    private int mActivePointerId;

    private static final int TOUCH_MODE_IDLE = 0;
    private static final int TOUCH_MODE_DRAGGING = 1;
    private static final int TOUCH_MODE_FLINGING = 2;
    private static final int TOUCH_MODE_OVERFLING = 3;

    // Value used to estimate the range of scroll and scroll position
    final static int SCROLLING_ESTIMATED_ITEM_HEIGHT = 100;

    private int mTouchMode;
    private final VelocityTracker mVelocityTracker = VelocityTracker.obtain();
    private final OverScrollerSGV mScroller;

    private final EdgeEffectCompat mTopEdge;
    private final EdgeEffectCompat mBottomEdge;

    private boolean mIsDragReorderingEnabled;

    private ScrollListener mScrollListener;
    private OnSizeChangedListener mOnSizeChangedListener;

    // The view to show when the adapter is empty.
    private View mEmptyView;

    // The size of the region at location relative to the child's edges where reordering
    // can happen if another child view is dragged and dropped over it.
    private int mHorizontalReorderingAreaSize;

    // TODO: Put these states into a ReorderingParam object for maintainability.
    private ImageView mDragView;

    // X and Y positions of the touch down event that started the drag
    private int mTouchDownForDragStartX;
    private int mTouchDownForDragStartY;

    // X and Y offsets inside the item from where the user grabbed to the
    // child's left coordinate.
    // This is used to aid in the drawing of the drag shadow.
    private int mTouchOffsetToChildLeft;
    private int mTouchOffsetToChildTop;

    // Difference between screen coordinates and coordinates in this view.
    private int mOffsetToAbsoluteX;
    private int mOffsetToAbsoluteY;

    // the cached positions of the drag view when released.
    private Rect mCachedDragViewRect;

    // the current drag state
    private int mDragState;

    // the height of this view
    private int mHeight;

    // The bounds of the screen that should initiate scrolling when a view
    // is dragged past these positions.
    private int mUpperScrollBound;
    private int mLowerScrollBound;

    // The Bitmap that contains the drag shadow.
    private Bitmap mDragBitmap;
    private final int mOverscrollDistance;

    private final WindowManager mWindowManager;
    private WindowManager.LayoutParams mWindowParams;
    private static final int mWindowManagerLayoutFlags = WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE
            | WindowManager.LayoutParams.FLAG_NOT_TOUCHABLE | WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON
            | WindowManager.LayoutParams.FLAG_LAYOUT_IN_SCREEN | WindowManager.LayoutParams.FLAG_LAYOUT_NO_LIMITS;

    private ReorderHelper mReorderHelper;

    /**
     * Indicates whether to use pixels-based or position-based scrollbar
     * properties.
     * This property is borrow from AbsListView
     */
    private boolean mSmoothScrollbarEnabled = false;

    private static final class LayoutRecord {
        public int column;
        public long id = -1;
        public int height;
        public int span;
        private int[] mMargins;

        private final void ensureMargins() {
            if (mMargins == null) {
                // Don't need to confirm length;
                // all layoutrecords are purged when column count changes.
                mMargins = new int[span * 2];
            }
        }

        public final int getMarginAbove(int col) {
            if (mMargins == null) {
                return 0;
            }
            return mMargins[col * 2];
        }

        public final int getMarginBelow(int col) {
            if (mMargins == null) {
                return 0;
            }
            return mMargins[col * 2 + 1];
        }

        public final void setMarginAbove(int col, int margin) {
            if (mMargins == null && margin == 0) {
                return;
            }
            ensureMargins();
            mMargins[col * 2] = margin;
        }

        public final void setMarginBelow(int col, int margin) {
            if (mMargins == null && margin == 0) {
                return;
            }
            ensureMargins();
            mMargins[col * 2 + 1] = margin;
        }

        @Override
        public String toString() {
            String result = "LayoutRecord{c=" + column + ", id=" + id + " h=" + height + " s=" + span;
            if (mMargins != null) {
                result += " margins[above, below](";
                for (int i = 0; i < mMargins.length; i += 2) {
                    result += "[" + mMargins[i] + ", " + mMargins[i + 1] + "]";
                }
                result += ")";
            }
            return result + "}";
        }
    }

    private final Map<Long, ViewRectPair> mChildRectsForAnimation = new HashMap<Long, ViewRectPair>();

    private final SparseArrayCompat<LayoutRecord> mLayoutRecords = new SparseArrayCompat<LayoutRecord>();

    // Handler for executing the scroll runnable
    private Handler mScrollHandler;

    // Boolean is true when the {@link #mDragScroller} scroll runanbled has been kicked off.
    // This is set back to false when it is removed from the handler.
    private boolean mIsDragScrollerRunning;

    /**
     * Scroller runnable to invoke scrolling when user is holding a dragged view over the upper
     * or lower bounds of the screen.
     */
    private final Runnable mDragScroller = new Runnable() {
        @Override
        public void run() {
            if (mDragState == ReorderUtils.DRAG_STATE_NONE) {
                return;
            }

            boolean enableUpdate = true;
            if (mLastTouchY >= mLowerScrollBound) {
                // scroll the list up a bit if we're past the lower bound, and the direction
                // of the movement is towards the bottom of the view.
                if (trackMotionScroll(-DRAG_SCROLL_RATE, false)) {
                    // Disable reordering if the view is scrolling
                    enableUpdate = false;
                }
            } else if (mLastTouchY <= mUpperScrollBound) {
                // scroll the list down a bit if we're past the upper bound, and the direction
                // of the movement is towards the top of the view.
                if (trackMotionScroll(DRAG_SCROLL_RATE, false)) {
                    // Disable reordering if the view is scrolling
                    enableUpdate = false;
                }
            }

            mReorderHelper.enableUpdatesOnDrag(enableUpdate);

            mScrollHandler.postDelayed(this, SCROLL_HANDLER_DELAY);
        }
    };

    public StaggeredGridView(Context context) {
        this(context, null);
    }

    public StaggeredGridView(Context context, AttributeSet attrs) {
        this(context, attrs, 0);
    }

    public StaggeredGridView(Context context, AttributeSet attrs, int defStyle) {
        super(context, attrs, defStyle);

        final ViewConfiguration vc = ViewConfiguration.get(context);
        mTouchSlop = vc.getScaledTouchSlop();
        mMaximumVelocity = vc.getScaledMaximumFlingVelocity();
        mFlingVelocity = vc.getScaledMinimumFlingVelocity();
        mScroller = new OverScrollerSGV(context);

        mTopEdge = new EdgeEffectCompat(context);
        mBottomEdge = new EdgeEffectCompat(context);
        setWillNotDraw(false);
        setClipToPadding(false);

        SgvAnimationHelper.initialize(context);

        mDragState = ReorderUtils.DRAG_STATE_NONE;
        mIsDragReorderingEnabled = true;
        mWindowManager = (WindowManager) context.getSystemService(Context.WINDOW_SERVICE);
        final ViewConfiguration configuration = ViewConfiguration.get(context);
        mOverscrollDistance = configuration.getScaledOverflingDistance();
        // Disable splitting event. Only one of the children can handle motion event.
        setMotionEventSplittingEnabled(false);
    }

    /**
     * Check to see if the current layout is Right-to-Left.  This check is only supported for
     * API 17+.  For earlier versions, this method will just return false.
     *
     * NOTE:  This is based on the private API method in {@link View} class.
     *
     * @return boolean Boolean indicating whether the currently locale is RTL.
     */
    @SuppressLint("NewApi")
    private boolean isLayoutRtl() {
        if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.JELLY_BEAN_MR1) {
            return View.LAYOUT_DIRECTION_RTL == getLayoutDirection();
        } else {
            return false;
        }
    }

    /**
     * Set a fixed number of columns for this grid. Space will be divided evenly
     * among all columns, respecting the item margin between columns.
     * The default is 2. (If it were 1, perhaps you should be using a
     * {@link android.widget.ListView ListView}.)
     *
     * @param colCount Number of columns to display.
     * @see #setMinColumnWidth(int)
     */
    public void setColumnCount(int colCount) {
        if (colCount < 1 && colCount != COLUMN_COUNT_AUTO) {
            throw new IllegalArgumentException("Column count must be at least 1 - received " + colCount);
        }
        final boolean needsPopulate = colCount != mColCount;
        mColCount = mColCountSetting = colCount;
        if (needsPopulate) {
            // When switching column count, for now, don't restore scroll position, and just
            // start layout fresh again.
            clearAllState();

            mHorizontalReorderingAreaSize = 0;
            populate();
        }
    }

    public int getColumnCount() {
        return mColCount;
    }

    /**
     * Set whether or not to explicitly guard against "jagged edges" in the grid
     * (meaning that the top edge of the children views in the first row of the grid can be
     * horizontally misaligned).
     *
     * If guardAgainstJaggedEdges is true, then we prevent jagged edges by computing the heights of
     * all views starting at the 0th position of the adapter to figure out the proper offset of the
     * views currently on screen. This is an expensive operation and should be avoided if possible.
     *
     * If guardAgainstJaggedEdges is false, then we can skip the expensive computation that
     * guards against jagged edges and just layout views on the screen starting from mFirstPosition
     * (ignoring what came before it).
     */
    public void setGuardAgainstJaggedEdges(boolean guardAgainstJaggedEdges) {
        mGuardAgainstJaggedEdges = guardAgainstJaggedEdges;
    }

    /**
     * Set a minimum column width for
     * @param minColWidth
     */
    public void setMinColumnWidth(int minColWidth) {
        mMinColWidth = minColWidth;
        setColumnCount(COLUMN_COUNT_AUTO);
    }

    /**
     * Set the margin between items in pixels. This margin is applied
     * both vertically and horizontally.
     *
     * @param marginPixels Spacing between items in pixels
     */
    public void setItemMargin(int marginPixels) {
        // We only need to {@link #populate()} if the margin has been changed.
        if (marginPixels != mItemMargin) {
            mItemMargin = marginPixels;
            populate();
        }
    }

    public int getItemMargin() {
        return mItemMargin;
    }

    /**
     * When smooth scrollbar is enabled, the position and size of the scrollbar thumb
     * is computed based on the number of visible pixels in the visible items. This
     * however assumes that all list items have the same height. If you use a list in
     * which items have different heights, the scrollbar will change appearance as the
     * user scrolls through the list. To avoid this issue, you need to disable this
     * property.
     *
     * When smooth scrollbar is disabled, the position and size of the scrollbar thumb
     * is based solely on the number of items in the adapter and the position of the
     * visible items inside the adapter. This provides a stable scrollbar as the user
     * navigates through a list of items with varying heights.
     *
     * @param enabled Whether or not to enable smooth scrollbar.
     *
     * @see #setSmoothScrollbarEnabled(boolean)
     * @attr ref android.R.styleable#AbsListView_smoothScrollbar
     */
    public void setSmoothScrollbarEnabled(boolean enabled) {
        mSmoothScrollbarEnabled = enabled;
    }

    /**
     * Returns the current state of the fast scroll feature.
     *
     * @return True if smooth scrollbar is enabled is enabled, false otherwise.
     *
     * @see #setSmoothScrollbarEnabled(boolean)
     */
    public boolean isSmoothScrollbarEnabled() {
        return mSmoothScrollbarEnabled;
    }

    /**
     * Return the child view specified by the coordinates if
     * there exists a child there.
     *
     * @return the child in this StaggeredGridView at the coordinates, null otherwise.
     */
    private View getChildAtCoordinate(int x, int y) {
        if (y < 0) {
            // TODO: If we've dragged off the screen, return null for now until we know what
            // we'd like the experience to be like.
            return null;
        }

        final Rect frame = new Rect();
        final int count = getChildCount();
        for (int i = 0; i < count; i++) {

            final View childView = getChildAt(i);
            childView.getHitRect(frame);
            if (frame.contains(x, y)) {
                return getChildAt(i);
            }
        }

        // No child view at this coordinate.
        return null;
    }

    /**
     * Get the last Y coordinate on this grid where the last touch was made
     */
    public float getLastTouchY() {
        return mLastTouchY;
    }

    /**
     * Enable drag reordering of child items.
     */
    public void enableDragReordering() {
        mIsDragReorderingEnabled = true;
    }

    /**
     * Disable drag reordering of child items.
     */
    public void disableDragReordering() {
        mIsDragReorderingEnabled = false;
    }

    /**
     * Check to see if drag reordering is supported.  The switch must be flipped to true, and there
     * must be a {@link ReorderListener} registered to listen for reordering events.
     *
     * @return boolean indicating whether drag reordering is currently supported.
     */
    private boolean isDragReorderingSupported() {
        return mIsDragReorderingEnabled && mReorderHelper != null && mReorderHelper.hasReorderListener();
    }

    /**
     * Calculate bounds to assist in scrolling during a drag
     * @param y The y coordinate of the current drag.
     */
    private void initializeDragScrollParameters(int y) {
        // Calculate the upper and lower bound of the screen to support drag scrolling
        mHeight = getHeight();
        mUpperScrollBound = Math.min(y - mTouchSlop, mHeight / 5);
        mLowerScrollBound = Math.max(y + mTouchSlop, mHeight * 4 / 5);
    }

    /**
     * Initiate the dragging process. Create a bitmap that is displayed as the dragging event
     * happens and is moved around across the screen.  This function is called once for each time
     * that a dragging event is initiated.
     *
     * The logic to this method was borrowed from the TouchInterceptor.java class from the
     * music app.
     *
     * @param draggedChild The child view being dragged
     * @param x The x coordinate of this view where dragging began
     * @param y The y coordinate of this view where dragging began
     */
    private void startDragging(final View draggedChild, final int x, final int y) {
        if (!isDragReorderingSupported()) {
            return;
        }

        mDragBitmap = createDraggedChildBitmap(draggedChild);
        if (mDragBitmap == null) {
            // It appears that creating bitmaps for large views fail. For now, don't allow
            // dragging in this scenario.  When using the framework's drag and drop implementation,
            // drag shadow also fails with a OutofResourceException when trying to draw the drag
            // shadow onto a Surface.
            mReorderHelper.handleDragCancelled(draggedChild);
            return;
        }
        mTouchOffsetToChildLeft = x - draggedChild.getLeft();
        mTouchOffsetToChildTop = y - draggedChild.getTop();
        updateReorderStates(ReorderUtils.DRAG_STATE_DRAGGING);

        initializeDragScrollParameters(y);

        final LayoutParams params = (LayoutParams) draggedChild.getLayoutParams();
        mReorderHelper.handleDragStart(draggedChild, params.position, params.id,
                new Point(mTouchDownForDragStartX, mTouchDownForDragStartY));

        // TODO: Reconsider using the framework's DragShadow support for dragging,
        // and only draw the bitmap in onDrop for animation.
        final Context context = getContext();
        mDragView = new ImageView(context);
        mDragView.setImageBitmap(mDragBitmap);
        mDragView.setAlpha(160);

        mWindowParams = new WindowManager.LayoutParams();
        mWindowParams.gravity = Gravity.TOP | Gravity.START;

        mWindowParams.height = WindowManager.LayoutParams.WRAP_CONTENT;
        mWindowParams.width = WindowManager.LayoutParams.WRAP_CONTENT;
        mWindowParams.flags = mWindowManagerLayoutFlags;
        mWindowParams.format = PixelFormat.TRANSLUCENT;
        // Use WindowManager to overlay a transparent image on drag
        mWindowManager.addView(mDragView, mWindowParams);
        updateDraggedBitmapLocation(x, y);
    }

    private Bitmap createDraggedChildBitmap(View view) {
        view.setDrawingCacheEnabled(true);
        final Bitmap cache = view.getDrawingCache();

        Bitmap bitmap = null;
        if (cache != null) {
            try {
                bitmap = cache.copy(Bitmap.Config.ARGB_8888, false);
            } catch (final OutOfMemoryError e) {
                Log.w(TAG, "Failed to copy bitmap from Drawing cache", e);
                bitmap = null;
            }
        }

        view.destroyDrawingCache();
        view.setDrawingCacheEnabled(false);

        return bitmap;
    }

    /**
     * Updates the current drag state and the UI appropriately.
     * @param state the new drag state to update to.
     */
    private void updateReorderStates(int state) throws IllegalStateException {
        boolean resetDraggedChildView = false;
        boolean resetDragProperties = false;

        mDragState = state;

        switch (state) {
        case ReorderUtils.DRAG_STATE_NONE:
        case ReorderUtils.DRAG_STATE_DRAGGING:
            // reset all states when a drag is complete or when we're starting a new drag.
            resetDraggedChildView = true;
            resetDragProperties = true;
            break;

        case ReorderUtils.DRAG_STATE_RELEASED_REORDER:
            // In a release over a valid reordering zone, don't reset any UI.  Let
            // LayoutChildren() take care of doing the appropriate animation
            // based on the result
            break;

        case ReorderUtils.DRAG_STATE_RELEASED_HOVER:
            // When a dragged child is released over another child, the dragged child will
            // remain hidden.  It is up to the ReorderListener to refresh the UI state
            // of the child if it does not handle the drop.
            resetDragProperties = true;
            break;

        default:
            throw new IllegalStateException("Illegal drag state: " + mDragState);
        }

        if (resetDraggedChildView && mReorderHelper.getDraggedChild() != null) {
            // DraggedChildId and mCachedDragViewRect need to stay around longer than
            // the other properties because on the next data change, as we lay out, we'll need
            // mCachedDragViewRect to position the view's animation start position, and
            // draggedChildId to check if the current was the dragged view.
            // For the other properties - DraggedOverChildView, DraggedChildView, etc.,
            // as soon as drag is released, we can reset them because they have no impact on the
            // next layout pass.
            mReorderHelper.clearDraggedChildId();
            mCachedDragViewRect = null;
        }

        if (resetDragProperties) {
            if (mDragView != null) {
                mDragView.setVisibility(INVISIBLE);
                mWindowManager.removeView(mDragView);
                mDragView.setImageDrawable(null);
                mDragView = null;

                if (mDragBitmap != null) {
                    mDragBitmap.recycle();
                    mDragBitmap = null;
                }
            }

            // We don't reset DraggedChildId here because it may still be in used.
            // Let LayoutChildren reset it when it's done with it.
            mReorderHelper.clearDraggedChild();
            mReorderHelper.clearDraggedOverChild();
        }
    }

    /**
     * Redraw the dragged child's bitmap based on the new coordinates.  If the reordering direction
     * is {@link ReorderUtils#REORDER_DIRECTION_VERTICAL}, then ignore the x coordinate, as
     * only vertical movement is allowed.  Similarly, if reordering direction is
     * {@link ReorderUtils#REORDER_DIRECTION_HORIZONTAL}.  Even though this class does not manage
     * drag shadow directly, we need to make sure we position the dragged bitmap at where the
     * drag shadow is so that when drag ends, we can swap the shadow and the bitmap to animate
     * the view into place.
     * @param x The updated x coordinate of the drag shadow.
     * @param y THe updated y coordinate of the drag shadow.
     */
    private void updateDraggedBitmapLocation(int x, int y) {
        final int direction = mAdapter.getReorderingDirection();
        if ((direction & ReorderUtils.REORDER_DIRECTION_HORIZONTAL) == ReorderUtils.REORDER_DIRECTION_HORIZONTAL) {
            if (mDragBitmap != null && mDragBitmap.getWidth() > getWidth()) {
                // If the bitmap is wider than the width of the screen, then some parts of the view
                // are off screen.  In this case, just set the drag shadow to start at x = 0
                // (adjusted to the absolute position on screen) so that at least the beginning of
                // the drag shadow is guaranteed to be within view.
                mWindowParams.x = mOffsetToAbsoluteX;
            } else {
                // WindowParams is RTL agnostic and operates on raw coordinates.  So in an RTL
                // layout, we would still want to find the view's left coordinate for the
                // drag shadow, rather than the view's start.
                mWindowParams.x = x - mTouchOffsetToChildLeft + mOffsetToAbsoluteX;
            }
        } else {
            mWindowParams.x = mOffsetToAbsoluteX;
        }

        if ((direction & ReorderUtils.REORDER_DIRECTION_VERTICAL) == ReorderUtils.REORDER_DIRECTION_VERTICAL) {
            mWindowParams.y = y - mTouchOffsetToChildTop + mOffsetToAbsoluteY;
        } else {
            mWindowParams.y = mOffsetToAbsoluteY;
        }

        mWindowManager.updateViewLayout(mDragView, mWindowParams);
    }

    /**
     * Update the visual state of the drag event based on the current drag location.  If the user
     * has attempted to re-order by dragging a child over another child's drop zone, call the
     * appropriate {@link ReorderListener} callback.
     *
     * @param x The current x coordinate of the drag event
     * @param y The current y coordinate of the drag event
     */
    private void handleDrag(int x, int y) {
        if (mDragState != ReorderUtils.DRAG_STATE_DRAGGING) {
            return;
        }

        // TODO: Consider moving drag shadow management logic into mReorderHelper as well, or
        // scrap the custom logic and use the framework's drag-and-drop support now that we're not
        // doing anything special to the drag shadow.
        updateDraggedBitmapLocation(x, y);

        if (mCurrentRunningAnimatorSet == null) {
            // If the current animator set is not null, then animation is running, in which case,
            // we shouldn't do any reordering processing, as views will be moving around, and
            // interfering with drag target calculations.
            mReorderHelper.handleDrag(new Point(x, y));
        }
    }

    /**
     * Check if a view is reorderable.
     * @param i the child index in view group
     */
    public boolean isChildReorderable(int i) {
        return mAdapter.isDraggable(mFirstPosition + i);
    }

    /**
     * Handle the the release of a dragged view.
     * @param x The current x coordinate where the drag was released.
     * @param y The current y coordinate where the drag was released.
     */
    private void handleDrop(int x, int y) {
        if (!mReorderHelper.hasReorderListener()) {
            updateReorderStates(ReorderUtils.DRAG_STATE_NONE);
            return;
        }

        if (mReorderHelper.isOverReorderingArea()) {
            // Store the location of the drag shadow at where dragging stopped
            // for animation if a reordering has just happened. Since the drag
            // shadow is drawn as a WindowManager view, its coordinates are
            // absolute. However, for views inside the grid, we need to operate
            // with coordinate values that's relative to this grid, so we need
            // to subtract the offset to absolute screen coordinates that have
            // been added to mWindowParams.
            final int left = mWindowParams.x - mOffsetToAbsoluteX;
            final int top = mWindowParams.y - mOffsetToAbsoluteY;

            mCachedDragViewRect = new Rect(left, top, left + mDragView.getWidth(), top + mDragView.getHeight());
            if (getChildCount() > 0) {
                final View view = getChildAt(0);
                final LayoutParams lp = (LayoutParams) view.getLayoutParams();
                if (lp.position > mReorderHelper.getDraggedChildPosition()) {
                    // If the adapter position of the first child in view is
                    // greater than the position of the original dragged child,
                    // this means that the user has scrolled the child out of
                    // view. Those off screen views would have been recycled. If
                    // mFirstPosition is currently x, after the reordering
                    // operation, the child[mFirstPosition] will be
                    // at mFirstPosition-1. We want to adjust mFirstPosition so
                    // that we render the view in the correct location after
                    // reordering completes.
                    //
                    // If the user has not scrolled the original dragged child
                    // out of view, then the view has not been recycled and is
                    // still in view.
                    // When onLayout() gets called, we'll automatically fill in
                    // the empty space that the child leaves behind from the
                    // reordering operation.
                    mFirstPosition--;
                }
            }

            // Get the current scroll position so that after reordering
            // completes, we can restore the scroll position of mFirstPosition.
            mCurrentScrollState = getScrollState();
        }

        final boolean reordered = mReorderHelper.handleDrop(new Point(x, y));
        if (reordered) {
            updateReorderStates(ReorderUtils.DRAG_STATE_RELEASED_REORDER);
        } else {
            updateReorderStates(ReorderUtils.DRAG_STATE_NONE);
        }
    }

    @Override
    public boolean onInterceptTouchEvent(MotionEvent ev) {
        mVelocityTracker.addMovement(ev);
        final int action = ev.getAction() & MotionEventCompat.ACTION_MASK;
        switch (action) {
        case MotionEvent.ACTION_DOWN: {
            mOffsetToAbsoluteX = (int) (ev.getRawX() - ev.getX());
            mOffsetToAbsoluteY = (int) (ev.getRawY() - ev.getY());

            // Per bug 7377413, event.getX() and getY() returns rawX and rawY when accessed in
            // dispatchDragEvent, so since an action down is required before a drag can be
            // initiated, initialize mTouchDownForDragStartX/Y here for the most accurate value.
            mTouchDownForDragStartX = (int) ev.getX();
            mTouchDownForDragStartY = (int) ev.getY();

            mVelocityTracker.clear();
            mScroller.abortAnimation();
            mLastTouchY = ev.getY();
            mActivePointerId = MotionEventCompat.getPointerId(ev, 0);
            mTouchRemainderY = 0;
            if (mTouchMode == TOUCH_MODE_FLINGING) {
                // Catch!
                mTouchMode = TOUCH_MODE_DRAGGING;
                return true;
            }
            break;
        }
        case MotionEvent.ACTION_MOVE: {
            final int index = MotionEventCompat.findPointerIndex(ev, mActivePointerId);
            if (index < 0) {
                Log.e(TAG, "onInterceptTouchEvent could not find pointer with id " + mActivePointerId
                        + " - did StaggeredGridView receive an inconsistent " + "event stream?");
                return false;
            }
            final float y = MotionEventCompat.getY(ev, index);
            final float dy = y - mLastTouchY + mTouchRemainderY;
            final int deltaY = (int) dy;
            mTouchRemainderY = dy - deltaY;

            if (Math.abs(dy) > mTouchSlop) {
                mTouchMode = TOUCH_MODE_DRAGGING;
                return true;
            }
        }
        }

        return false;
    }

    @Override
    public boolean onTouchEvent(MotionEvent ev) {
        mVelocityTracker.addMovement(ev);
        final int action = ev.getAction() & MotionEventCompat.ACTION_MASK;
        switch (action) {
        case MotionEvent.ACTION_DOWN:
            resetScroller();
            mVelocityTracker.clear();
            mScroller.abortAnimation();
            mLastTouchY = ev.getY();
            mActivePointerId = MotionEventCompat.getPointerId(ev, 0);
            mTouchRemainderY = 0;
            break;

        case MotionEvent.ACTION_MOVE: {
            final int index = MotionEventCompat.findPointerIndex(ev, mActivePointerId);
            if (index < 0) {
                Log.e(TAG, "onInterceptTouchEvent could not find pointer with id " + mActivePointerId
                        + " - did StaggeredGridView receive an inconsistent " + "event stream?");
                return false;
            }

            final float y = MotionEventCompat.getY(ev, index);
            final float dy = y - mLastTouchY + mTouchRemainderY;
            final int deltaY = (int) dy;
            mTouchRemainderY = dy - deltaY;

            if (Math.abs(dy) > mTouchSlop) {
                mTouchMode = TOUCH_MODE_DRAGGING;
            }

            if (mTouchMode == TOUCH_MODE_DRAGGING) {
                mLastTouchY = y;
                if (!trackMotionScroll(deltaY, true)) {
                    // Break fling velocity if we impacted an edge.
                    mVelocityTracker.clear();
                }
            }
            break;
        }

        case MotionEvent.ACTION_CANCEL: {
            mTouchMode = TOUCH_MODE_IDLE;
            break;
        }

        case MotionEvent.ACTION_UP: {
            mVelocityTracker.computeCurrentVelocity(1000, mMaximumVelocity);
            final float velocity = VelocityTrackerCompat.getYVelocity(mVelocityTracker, mActivePointerId);
            if (Math.abs(velocity) > mFlingVelocity) {
                mTouchMode = TOUCH_MODE_FLINGING;
                resetScroller();
                mScroller.fling(0, 0, 0, (int) velocity, 0, 0, Integer.MIN_VALUE, Integer.MAX_VALUE);
                mLastTouchY = 0;
                ViewCompat.postInvalidateOnAnimation(this);
            } else {
                mTouchMode = TOUCH_MODE_IDLE;
            }
        }
            break;
        }

        return true;
    }

    private void resetScroller() {
        mTouchMode = TOUCH_MODE_IDLE;
        mTopEdge.finish();
        mBottomEdge.finish();
        mScroller.abortAnimation();
    }

    @Override
    public boolean dispatchDragEvent(DragEvent event) {
        if (!isDragReorderingSupported()) {
            // If the consumer of this StaggeredGridView has not registered a ReorderListener,
            // don't bother handling drag events.
            return super.dispatchDragEvent(event);
        }

        switch (event.getAction()) {
        case DragEvent.ACTION_DRAG_STARTED:
            // Per bug 7071594, we won't be able to catch this event in onDragEvent,
            // so we'll handle the event as it is being dispatched on the way down.
            if (mReorderHelper.hasReorderListener() && mIsDragReorderingEnabled) {
                final View child = getChildAtCoordinate(mTouchDownForDragStartX, mTouchDownForDragStartY);
                if (child != null) {
                    // Child can be null if the touch point is not on a child view, but is
                    // still within the bounds of this StaggeredGridView (i.e., margins
                    // between cells).
                    startDragging(child, mTouchDownForDragStartX, mTouchDownForDragStartY);
                    // We must return true in order to continue getting future
                    // {@link DragEvent}s.
                    return true;
                }
            }
            // Be sure to return a value here instead of calling super.dispatchDragEvent()
            // which will unnecessarily dispatch to all the children (since the
            // {@link StaggeredGridView} handles all drag events for our purposes)
            return false;

        case DragEvent.ACTION_DROP:
        case DragEvent.ACTION_DRAG_ENDED:
            if (mDragState == ReorderUtils.DRAG_STATE_DRAGGING) {
                handleDrop((int) event.getX(), (int) event.getY());
            }

            // Return early here to avoid calling super.dispatchDragEvent() which dispatches to
            // children (since this view already can handle all drag events). The super call
            // can also cause a NPE if the view hierarchy changed in the middle of a drag
            // and the {@link DragEvent} gets nulled out. This is a workaround for
            // a framework bug: 8298439.
            // Since the {@link StaggeredGridView} handles all drag events for our purposes,
            // just manually fire the drag event to ourselves.
            return onDragEvent(event);
        }

        // In all other cases, default to the superclass implementation. We need this so that
        // the drag/drop framework will fire off {@link #onDragEvent(DragEvent ev)} calls to us.
        return super.dispatchDragEvent(event);
    }

    @Override
    public boolean onDragEvent(DragEvent ev) {
        if (!isDragReorderingSupported()) {
            // If the consumer of this StaggeredGridView has not registered a ReorderListener,
            // don't bother handling drag events.
            return false;
        }

        final int x = (int) ev.getX();
        final int y = (int) ev.getY();

        switch (ev.getAction()) {
        case DragEvent.ACTION_DRAG_LOCATION:
            if (mDragState == ReorderUtils.DRAG_STATE_DRAGGING) {
                handleDrag(x, y);
                mLastTouchY = y;
            }

            // Kick off the scroll handler on the first drag location event,
            // if it's not already running
            if (!mIsDragScrollerRunning &&
            // And if the distance traveled while dragging exceeds the touch slop
                    ((Math.abs(x - mTouchDownForDragStartX) >= 4 * mTouchSlop)
                            || (Math.abs(y - mTouchDownForDragStartY) >= 4 * mTouchSlop))) {
                // Set true because that the scroller is running now
                mIsDragScrollerRunning = true;

                if (mScrollHandler == null) {
                    mScrollHandler = getHandler();
                }
                mScrollHandler.postDelayed(mDragScroller, SCROLL_HANDLER_DELAY);
            }

            return true;

        case DragEvent.ACTION_DROP:
        case DragEvent.ACTION_DRAG_ENDED:
            // We can either expect to receive:
            // 1. Both {@link DragEvent#ACTION_DROP} and then
            //    {@link DragEvent#ACTION_DRAG_ENDED} if the drop is over this view.
            // 2. Only {@link DragEvent#ACTION_DRAG_ENDED} if the drop happened over a
            //    different view.
            // For this reason, we should always handle the drop. In case #1, if this code path
            // gets executed again then nothing will happen because we will have already
            // updated {@link #mDragState} to not be {@link ReorderUtils#DRAG_STATE_DRAGGING}.
            if (mScrollHandler != null) {
                mScrollHandler.removeCallbacks(mDragScroller);
                // Scroller is no longer running
                mIsDragScrollerRunning = false;
            }

            return true;
        }

        return false;
    }

    /**
     *
     * @param deltaY Pixels that content should move by
     * @return true if the movement completed, false if it was stopped prematurely.
     */
    private boolean trackMotionScroll(int deltaY, boolean allowOverScroll) {
        final boolean contentFits = contentFits();
        final int allowOverhang = Math.abs(deltaY);
        final int overScrolledBy;
        final int movedBy;
        if (!contentFits) {
            int overhang;
            final boolean up;
            mPopulating = true;
            if (deltaY > 0) {
                overhang = fillUp(mFirstPosition - 1, allowOverhang);
                up = true;
            } else {
                overhang = fillDown(mFirstPosition + getChildCount(), allowOverhang);

                if (overhang < 0) {
                    // Overhang when filling down indicates how many pixels past the bottom of the
                    // screen has been filled in.  If this value is negative, it should be set to
                    // 0 so that we don't allow over scrolling.
                    overhang = 0;
                }

                up = false;
            }

            movedBy = Math.min(overhang, allowOverhang);
            offsetChildren(up ? movedBy : -movedBy);
            recycleOffscreenViews();
            mPopulating = false;
            overScrolledBy = allowOverhang - overhang;
        } else {
            overScrolledBy = allowOverhang;
            movedBy = 0;
        }

        if (allowOverScroll) {
            final int overScrollMode = ViewCompat.getOverScrollMode(this);

            if (overScrollMode == ViewCompat.OVER_SCROLL_ALWAYS
                    || (overScrollMode == ViewCompat.OVER_SCROLL_IF_CONTENT_SCROLLS && !contentFits)) {

                if (overScrolledBy > 0) {
                    final EdgeEffectCompat edge = deltaY > 0 ? mTopEdge : mBottomEdge;
                    edge.onPull((float) Math.abs(deltaY) / getHeight());
                    ViewCompat.postInvalidateOnAnimation(this);
                }
            }
        }

        awakenScrollBars(0 /* show immediately */, true /* invalidate */);
        return deltaY == 0 || movedBy != 0;
    }

    public final boolean contentFits() {
        if (mFirstPosition != 0 || getChildCount() != mItemCount) {
            return false;
        }

        int topmost = Integer.MAX_VALUE;
        int bottommost = Integer.MIN_VALUE;
        for (int i = 0; i < mColCount; i++) {
            if (mItemTops[i] < topmost) {
                topmost = mItemTops[i];
            }
            if (mItemBottoms[i] > bottommost) {
                bottommost = mItemBottoms[i];
            }
        }

        return topmost >= getPaddingTop() && bottommost <= getHeight() - getPaddingBottom();
    }

    /**
     * Recycle views within the range starting from startIndex (inclusive) until the last
     * attached child view.
     */
    private void recycleViewsInRange(int startIndex, int endIndex) {
        for (int i = endIndex; i >= startIndex; i--) {
            final View child = getChildAt(i);

            if (mInLayout) {
                removeViewsInLayout(i, 1);
            } else {
                removeViewAt(i);
            }

            mRecycler.addScrap(child);
        }
    }

    // TODO: Have other overloaded recycle methods call into this one so we would just have one
    // code path.
    private void recycleView(View view) {
        if (view == null) {
            return;
        }

        if (mInLayout) {
            removeViewInLayout(view);
            invalidate();
        } else {
            removeView(view);
        }

        mRecycler.addScrap(view);
    }

    /**
     * Important: this method will leave offscreen views attached if they
     * are required to maintain the invariant that child view with index i
     * is always the view corresponding to position mFirstPosition + i.
     */
    private void recycleOffscreenViews() {
        if (getChildCount() == 0) {
            return;
        }

        final int height = getHeight();
        final int clearAbove = -mItemMargin;
        final int clearBelow = height + mItemMargin;
        for (int i = getChildCount() - 1; i >= 0; i--) {
            final View child = getChildAt(i);
            if (child.getTop() <= clearBelow) {
                // There may be other offscreen views, but we need to maintain
                // the invariant documented above.
                break;
            }

            child.clearFocus();
            if (mInLayout) {
                removeViewsInLayout(i, 1);
            } else {
                removeViewAt(i);
            }

            mRecycler.addScrap(child);
        }

        while (getChildCount() > 0) {
            final View child = getChildAt(0);
            if (child.getBottom() >= clearAbove) {
                // There may be other offscreen views, but we need to maintain
                // the invariant documented above.
                break;
            }

            child.clearFocus();
            if (mInLayout) {
                removeViewsInLayout(0, 1);
            } else {
                removeViewAt(0);
            }

            mRecycler.addScrap(child);
            mFirstPosition++;
        }

        final int childCount = getChildCount();
        if (childCount > 0) {
            // Repair the top and bottom column boundaries from the views we still have
            Arrays.fill(mItemTops, Integer.MAX_VALUE);
            Arrays.fill(mItemBottoms, Integer.MIN_VALUE);
            for (int i = 0; i < childCount; i++) {
                final View child = getChildAt(i);
                final LayoutParams lp = (LayoutParams) child.getLayoutParams();
                final int top = child.getTop() - mItemMargin;
                final int bottom = child.getBottom();
                LayoutRecord rec = mLayoutRecords.get(mFirstPosition + i);

                // It's possible the layout record could be null for visible views because
                // they are cleared between adapter data set changes, but the views are left
                // attached for the purpose of animations. Hence, populate the layout record again.
                if (rec == null) {
                    rec = recreateLayoutRecord(mFirstPosition + i, child, lp);
                }

                // In LTR layout, iterate across each column that this child is laid out in,
                // starting from the child's first column (lp.column).  For each column, update
                // mItemTops and mItemBottoms appropriately to take into account this child's
                // dimension.  In RTL layout, iterate in reverse, where the child's starting
                // column would start from the right-most.
                final int span = Math.min(mColCount, lp.span);
                for (int spanIndex = 0; spanIndex < span; spanIndex++) {
                    final int col = mIsRtlLayout ? lp.column - spanIndex : lp.column + spanIndex;
                    final int colTop = top - rec.getMarginAbove(spanIndex);
                    final int colBottom = bottom + rec.getMarginBelow(spanIndex);
                    if (colTop < mItemTops[col]) {
                        mItemTops[col] = colTop;
                    }
                    if (colBottom > mItemBottoms[col]) {
                        mItemBottoms[col] = colBottom;
                    }
                }
            }

            for (int col = 0; col < mColCount; col++) {
                if (mItemTops[col] == Integer.MAX_VALUE) {
                    // If one was untouched, both were.
                    final int top = getPaddingTop();
                    mItemTops[col] = top;
                    mItemBottoms[col] = top;
                }
            }
        }

        mCurrentScrollState = getScrollState();
    }

    private LayoutRecord recreateLayoutRecord(int position, View child, LayoutParams lp) {
        final LayoutRecord rec = new LayoutRecord();
        mLayoutRecords.put(position, rec);
        rec.column = lp.column;
        rec.height = child.getHeight();
        rec.id = lp.id;
        rec.span = Math.min(mColCount, lp.span);
        return rec;
    }

    @Override
    public void computeScroll() {
        if (mTouchMode == TOUCH_MODE_OVERFLING) {
            handleOverfling();
        } else if (mScroller.computeScrollOffset()) {
            final int overScrollMode = ViewCompat.getOverScrollMode(this);
            final boolean supportsOverscroll = overScrollMode != ViewCompat.OVER_SCROLL_NEVER;
            final int y = mScroller.getCurrY();
            final int dy = (int) (y - mLastTouchY);
            // TODO: Figure out why mLastTouchY is being updated here. Consider using a new class
            // variable since this value does not represent the last place on the screen where a
            // touch occurred.
            mLastTouchY = y;
            // Check if the top of the motion view is where it is
            // supposed to be
            final View motionView = supportsOverscroll && getChildCount() > 0 ? getChildAt(0) : null;
            final int motionViewPrevTop = motionView != null ? motionView.getTop() : 0;
            final boolean stopped = !trackMotionScroll(dy, false);
            if (!stopped && !mScroller.isFinished()) {
                mTouchMode = TOUCH_MODE_IDLE;
                ViewCompat.postInvalidateOnAnimation(this);
            } else if (stopped && dy != 0 && supportsOverscroll) {
                // Check to see if we have bumped into the scroll limit
                if (motionView != null) {
                    final int motionViewRealTop = motionView.getTop();
                    // Apply overscroll
                    final int overscroll = -dy - (motionViewRealTop - motionViewPrevTop);
                    overScrollBy(0, overscroll, 0, getScrollY(), 0, 0, 0, mOverscrollDistance, true);
                }
                final EdgeEffectCompat edge;
                if (dy > 0) {
                    edge = mTopEdge;
                    mBottomEdge.finish();
                } else {
                    edge = mBottomEdge;
                    mTopEdge.finish();
                }
                edge.onAbsorb(Math.abs((int) mScroller.getCurrVelocity()));
                if (mScroller.computeScrollOffset()) {
                    mScroller.notifyVerticalEdgeReached(getScrollY(), 0, mOverscrollDistance);
                }
                mTouchMode = TOUCH_MODE_OVERFLING;
                ViewCompat.postInvalidateOnAnimation(this);
            } else {
                mTouchMode = TOUCH_MODE_IDLE;
            }
        }
    }

    private void handleOverfling() {
        // If the animation is not finished yet, determine next steps.
        if (mScroller.computeScrollOffset()) {
            final int scrollY = getScrollY();
            final int currY = mScroller.getCurrY();
            final int deltaY = currY - scrollY;
            if (overScrollBy(0, deltaY, 0, scrollY, 0, 0, 0, mOverscrollDistance, false)) {
                final boolean crossDown = scrollY <= 0 && currY > 0;
                final boolean crossUp = scrollY >= 0 && currY < 0;
                if (crossDown || crossUp) {
                    int velocity = (int) mScroller.getCurrVelocity();
                    if (crossUp) {
                        velocity = -velocity;
                    }

                    // Don't flywheel from this; we're just continuing
                    // things.
                    mTouchMode = TOUCH_MODE_IDLE;
                    mScroller.abortAnimation();
                } else {
                    // Spring back! We are done overscrolling.
                    if (mScroller.springBack(0, scrollY, 0, 0, 0, 0)) {
                        mTouchMode = TOUCH_MODE_OVERFLING;
                        ViewCompat.postInvalidateOnAnimation(this);
                    } else {
                        // If already valid, we are done. Exit overfling mode.
                        mTouchMode = TOUCH_MODE_IDLE;
                    }
                }
            } else {
                // Still over-flinging; just post the next frame of the animation.
                ViewCompat.postInvalidateOnAnimation(this);
            }
        } else {
            // Otherwise, exit overfling mode.
            mTouchMode = TOUCH_MODE_IDLE;
            mScroller.abortAnimation();
        }
    }

    @Override
    protected void onOverScrolled(int scrollX, int scrollY, boolean clampedX, boolean clampedY) {
        if (getScrollY() != scrollY) {
            scrollTo(0, scrollY);
        }
    }

    @Override
    public void draw(Canvas canvas) {
        super.draw(canvas);

        if (mTopEdge != null) {
            boolean needsInvalidate = false;
            if (!mTopEdge.isFinished()) {
                final int restoreCount = canvas.save();
                canvas.translate(0, 0);
                mTopEdge.draw(canvas);
                canvas.restoreToCount(restoreCount);
                needsInvalidate = true;
            }
            if (!mBottomEdge.isFinished()) {
                final int restoreCount = canvas.save();
                final int width = getWidth();
                canvas.translate(-width, getHeight());
                canvas.rotate(180, width, 0);
                mBottomEdge.draw(canvas);
                canvas.restoreToCount(restoreCount);
                needsInvalidate = true;
            }

            if (needsInvalidate) {
                ViewCompat.postInvalidateOnAnimation(this);
            }
        }
    }

    public void beginFastChildLayout() {
        mFastChildLayout = true;
    }

    public void endFastChildLayout() {
        mFastChildLayout = false;
        populate();
    }

    @Override
    public void requestLayout() {
        if (!mPopulating && !mFastChildLayout) {
            super.requestLayout();
        }
    }

    /**
     * Sets the view to show if the adapter is empty
     */
    public void setEmptyView(View emptyView) {
        mEmptyView = emptyView;

        updateEmptyStatus();
    }

    public View getEmptyView() {
        return mEmptyView;
    }

    /**
     * Update the status of the list based on the whether the adapter is empty.  If is it empty and
     * we have an empty view, display it.  In all the other cases, make sure that the
     * StaggeredGridView is VISIBLE and that the empty view is GONE (if it's not null).
     */
    private void updateEmptyStatus() {
        if (mAdapter == null || mAdapter.isEmpty()) {
            if (mEmptyView != null) {
                mEmptyView.setVisibility(View.VISIBLE);
                setVisibility(View.GONE);
            } else {
                setVisibility(View.VISIBLE);
            }
        } else {
            if (mEmptyView != null) {
                mEmptyView.setVisibility(View.GONE);
            }
            setVisibility(View.VISIBLE);
        }
    }

    @Override
    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
        int widthMode = MeasureSpec.getMode(widthMeasureSpec);
        int heightMode = MeasureSpec.getMode(heightMeasureSpec);
        final int widthSize = MeasureSpec.getSize(widthMeasureSpec);
        final int heightSize = MeasureSpec.getSize(heightMeasureSpec);

        if (widthMode != MeasureSpec.EXACTLY) {
            Log.d(TAG, "onMeasure: must have an exact width or match_parent! " + "Using fallback spec of EXACTLY "
                    + widthSize);
            widthMode = MeasureSpec.EXACTLY;
        }
        if (heightMode != MeasureSpec.EXACTLY) {
            Log.d(TAG, "onMeasure: must have an exact height or match_parent! " + "Using fallback spec of EXACTLY "
                    + heightSize);
            heightMode = MeasureSpec.EXACTLY;
        }

        setMeasuredDimension(widthSize, heightSize);

        if (mColCountSetting == COLUMN_COUNT_AUTO) {
            final int colCount = widthSize / mMinColWidth;
            if (colCount != mColCount) {
                mColCount = colCount;
            }
        }

        if (mHorizontalReorderingAreaSize == 0) {
            if (mColCount > 1) {
                final int totalMarginWidth = mItemMargin * (mColCount + 1);
                final int singleViewWidth = (widthSize - totalMarginWidth) / mColCount;
                mHorizontalReorderingAreaSize = singleViewWidth / CHILD_TO_REORDER_AREA_RATIO;
            } else {
                mHorizontalReorderingAreaSize = SINGLE_COL_REORDERING_AREA_SIZE;
            }
        }
    }

    @Override
    protected void onLayout(boolean changed, int l, int t, int r, int b) {
        mIsRtlLayout = isLayoutRtl();

        mInLayout = true;
        populate();
        mInLayout = false;
        final int width = r - l;
        final int height = b - t;
        mTopEdge.setSize(width, height);
        mBottomEdge.setSize(width, height);
    }

    private void populate() {
        if (getWidth() == 0 || getHeight() == 0 || mAdapter == null) {
            return;
        }

        if (mColCount == COLUMN_COUNT_AUTO) {
            final int colCount = getWidth() / mMinColWidth;
            if (colCount != mColCount) {
                mColCount = colCount;
            }
        }

        final int colCount = mColCount;
        if (mItemTops == null || mItemBottoms == null || mItemTops.length != colCount
                || mItemBottoms.length != colCount) {
            mItemTops = new int[colCount];
            mItemBottoms = new int[colCount];

            mLayoutRecords.clear();
            if (mInLayout) {
                removeAllViewsInLayout();
            } else {
                removeAllViews();
            }
        }

        // Before we do layout, if there are any pending animations and data has changed,
        // cancel the animation, as layout on new data will likely trigger another animation
        // set to be run.
        if (mDataChanged && mCurrentRunningAnimatorSet != null) {
            mCurrentRunningAnimatorSet.cancel();
            mCurrentRunningAnimatorSet = null;
        }

        if (isSelectionAtTop()) {
            mCurrentScrollState = null;
        }

        if (mCurrentScrollState != null) {
            restoreScrollPosition(mCurrentScrollState);
        } else {
            calculateLayoutStartOffsets(getPaddingTop() /* layout start offset */);
        }

        mPopulating = true;

        mFocusedChildIdToScrollIntoView = -1;
        final View focusedChild = getFocusedChild();
        if (focusedChild != null) {
            final LayoutParams lp = (LayoutParams) focusedChild.getLayoutParams();
            mFocusedChildIdToScrollIntoView = lp.id;
        }

        layoutChildren(mDataChanged);
        fillDown(mFirstPosition + getChildCount(), 0);
        fillUp(mFirstPosition - 1, 0);

        if (isDragReorderingSupported() && mDragState == ReorderUtils.DRAG_STATE_RELEASED_REORDER
                || mDragState == ReorderUtils.DRAG_STATE_RELEASED_HOVER) {
            // This child was dragged and dropped with the UI likely
            // still showing.  Call updateReorderStates, to update
            // all UI appropriately.
            mReorderHelper.clearDraggedChildId();
            updateReorderStates(ReorderUtils.DRAG_STATE_NONE);
        }

        if (mDataChanged) {
            // Animation should only play if data has changed since populate() can be called
            // multiple times with the same data set (e.g., screen size changed).
            handleLayoutAnimation();
        }

        recycleOffscreenViews();

        mPopulating = false;
        mDataChanged = false;
    }

    @Override
    public void scrollBy(int x, int y) {
        if (y != 0) {
            // TODO: Implement smooth scrolling for this so that scrolling does more than just
            // jumping by y pixels.
            trackMotionScroll(y, false /* over scroll */);
        }
    }

    private void offsetChildren(int offset) {
        final int childCount = getChildCount();
        for (int i = 0; i < childCount; i++) {
            final View child = getChildAt(i);

            child.offsetTopAndBottom(offset);

            // As we're scrolling, we need to make sure the children that are coming into view
            // have their reordering area set.
            final LayoutParams lp = (LayoutParams) child.getLayoutParams();
            setReorderingArea(lp);
        }

        final int colCount = mColCount;
        for (int i = 0; i < colCount; i++) {
            mItemTops[i] += offset;
            mItemBottoms[i] += offset;
        }

        if (mScrollListener != null) {
            mScrollListener.onScrollChanged(offset, computeVerticalScrollOffset(), computeVerticalScrollRange());
        }
    }

    /**
     * Performs layout animation of child views.
     * @throws IllegalStateException Exception is thrown of currently set animation mode is
     * not recognized.
     */
    private void handleLayoutAnimation() throws IllegalStateException {
        final List<Animator> animators = new ArrayList<Animator>();

        // b/8422632 - Without this dummy first animator, startDelays of subsequent animators won't
        // be honored correctly; all animators will block regardless of startDelay until the first
        // animator in the AnimatorSet truly starts playing.
        final ValueAnimator anim = ValueAnimator.ofFloat(0f, 1f);
        anim.setDuration(0);
        animators.add(anim);

        addOutAnimatorsForStaleViews(animators, mAnimationOutMode);

        // Play the In animators at a slight delay after all Out animators have started.
        final int animationInStartDelay = animators.size() > 0
                ? (SgvAnimationHelper.getDefaultAnimationDuration() / 2)
                : 0;
        addInAnimators(animators, mAnimationInMode, animationInStartDelay);

        if (animators != null && animators.size() > 0) {
            final AnimatorSet animatorSet = new AnimatorSet();
            animatorSet.playTogether(animators);
            animatorSet.addListener(new AnimatorListenerAdapter() {
                @Override
                public void onAnimationStart(Animator animation) {
                    mIsCurrentAnimationCanceled = false;
                    mCurrentRunningAnimatorSet = animatorSet;
                }

                @Override
                public void onAnimationCancel(Animator animation) {
                    mIsCurrentAnimationCanceled = true;
                }

                @Override
                public void onAnimationEnd(Animator animation) {
                    if (!mIsCurrentAnimationCanceled) {
                        // If this animation ended naturally, not because it was canceled, then
                        // reset the animation mode back to ANIMATION_MODE_NONE.  However, if
                        // the animation was canceled by a data change, then keep the mode as is,
                        // so that on a re-layout, we can resume animation from the views' current
                        // positions.
                        resetAnimationMode();
                    }
                    mCurrentRunningAnimatorSet = null;
                }
            });

            Log.v(TAG, "starting");
            animatorSet.start();
        } else {
            resetAnimationMode();
        }

        mViewsToAnimateOut.clear();
        mChildRectsForAnimation.clear();
    }

    /**
     * Reset the current animation mode.
     */
    private void resetAnimationMode() {
        mAnimationInMode = SgvAnimationHelper.AnimationIn.NONE;
        mAnimationOutMode = SgvAnimationHelper.AnimationOut.NONE;
    }

    /**
     * Add animators for animating in new views as well as updating positions of views that
     * should remain on screen.
     */
    private void addInAnimators(List<Animator> animators, SgvAnimationHelper.AnimationIn animationInMode,
            int startDelay) {
        if (animationInMode == SgvAnimationHelper.AnimationIn.NONE) {
            return;
        }

        switch (animationInMode) {
        case FLY_UP_ALL_VIEWS:
            addFlyInAllViewsAnimators(animators);
            break;

        case EXPAND_NEW_VIEWS:
            addUpdateViewPositionsAnimators(animators, true /* cascade animation */,
                    SgvAnimationHelper.AnimationIn.EXPAND_NEW_VIEWS, startDelay);
            break;

        case EXPAND_NEW_VIEWS_NO_CASCADE:
            addUpdateViewPositionsAnimators(animators, false /* cascade animation */,
                    SgvAnimationHelper.AnimationIn.EXPAND_NEW_VIEWS_NO_CASCADE, startDelay);
            break;

        case SLIDE_IN_NEW_VIEWS:
            addUpdateViewPositionsAnimators(animators, true /* cascade animation */,
                    SgvAnimationHelper.AnimationIn.SLIDE_IN_NEW_VIEWS, startDelay);
            break;

        case FLY_IN_NEW_VIEWS:
            addUpdateViewPositionsAnimators(animators, true /* cascade animation */,
                    SgvAnimationHelper.AnimationIn.FLY_IN_NEW_VIEWS, startDelay);
            break;

        case FADE:
            addUpdateViewPositionsAnimators(animators, true /* cascade animation */,
                    SgvAnimationHelper.AnimationIn.FADE, startDelay);
            break;

        default:
            throw new IllegalStateException("Unknown animationInMode: " + mAnimationInMode);
        }
    }

    /**
     * Add animators for animating out stale views
     * @param animationOutMode The animation mode to play for stale views
     */
    private void addOutAnimatorsForStaleViews(List<Animator> animators,
            SgvAnimationHelper.AnimationOut animationOutMode) {
        if (animationOutMode == SgvAnimationHelper.AnimationOut.NONE) {
            return;
        }

        for (final View v : mViewsToAnimateOut) {
            // For each stale view to animate out, retrieve the animators for the view, then attach
            // the StaleViewAnimationEndListener which checks to see if the view should be recycled
            // at the end of the animation.
            final List<Animator> viewAnimators = new ArrayList<Animator>();

            switch (animationOutMode) {
            case SLIDE:
                final LayoutParams lp = (LayoutParams) v.getLayoutParams();
                // Bias towards sliding right, but depending on the column that this view
                // is laid out in, slide towards the nearest side edge.
                int endTranslation = (int) (v.getWidth() * 1.5);
                if (lp.column < (mColCount / 2)) {
                    endTranslation = -endTranslation;
                }
                SgvAnimationHelper.addSlideOutAnimators(viewAnimators, v, (int) v.getTranslationX(),
                        endTranslation);
                break;

            case COLLAPSE:
                SgvAnimationHelper.addCollapseOutAnimators(viewAnimators, v);
                break;

            case FLY_DOWN:
                SgvAnimationHelper.addFlyOutAnimators(viewAnimators, v, (int) v.getTranslationY(), getHeight());
                break;

            case FADE:
                SgvAnimationHelper.addFadeAnimators(viewAnimators, v, v.getAlpha(), 0 /* end alpha */);
                break;

            default:
                throw new IllegalStateException("Unknown animationOutMode: " + animationOutMode);
            }

            if (viewAnimators.size() > 0) {
                addStaleViewAnimationEndListener(v, viewAnimators);
                animators.addAll(viewAnimators);
            }
        }
    }

    /**
     * Handle setting up the animators of child views when the animation is invoked by a change
     * in the adapter.  This method has a side effect of translating view positions in preparation
     * for the animations.
     */
    private List<Animator> addFlyInAllViewsAnimators(List<Animator> animators) {
        final int childCount = getChildCount();
        if (childCount == 0) {
            return null;
        }

        if (animators == null) {
            animators = new ArrayList<Animator>();
        }

        for (int i = 0; i < childCount; i++) {
            final int animationDelay = i * ANIMATION_DELAY_IN_MS;
            final View childToAnimate = getChildAt(i);

            // Start all views from below the bottom of this grid and animate them upwards. This
            // is done simply by translating the current view's vertical position by the height
            // of the entire grid.
            float yTranslation = getHeight();
            float rotation = SgvAnimationHelper.ANIMATION_ROTATION_DEGREES;
            if (mIsCurrentAnimationCanceled) {
                // If mIsAnimationCanceled is true, then this is not the first time that this
                // animation is running.  For this particular case, we should resume from where
                // the previous animation left off, rather than resetting translation and rotation.
                yTranslation = childToAnimate.getTranslationY();
                rotation = childToAnimate.getRotation();
            }

            SgvAnimationHelper.addTranslationRotationAnimators(animators, childToAnimate, 0 /* xTranslation */,
                    (int) yTranslation, rotation, animationDelay);
        }

        return animators;
    }

    /**
     * Animations to update the views on screen to their new positions.  For new views that aren't
     * currently on screen, animate them in using the specified animationInMode.
     */
    private List<Animator> addUpdateViewPositionsAnimators(List<Animator> animators, boolean cascadeAnimation,
            SgvAnimationHelper.AnimationIn animationInMode, int startDelay) {
        final int childCount = getChildCount();
        if (childCount == 0) {
            return null;
        }

        if (animators == null) {
            animators = new ArrayList<Animator>();
        }

        int viewsAnimated = 0;
        for (int i = 0; i < childCount; i++) {
            final View childToAnimate = getChildAt(i);

            if (mViewsToAnimateOut.contains(childToAnimate)) {
                // If the stale views are still animating, then they are still laid out, so
                // getChildCount() would've accounted for them.  Since they have their own set
                // of animations to play, we'll skip over them in this loop.
                continue;
            }

            // Use progressive animation delay to create the staggered effect of animating
            // views.  This is done by having each view delay their animation by
            // ANIMATION_DELAY_IN_MS after the animation of the previous view.
            int animationDelay = startDelay + (cascadeAnimation ? viewsAnimated * ANIMATION_DELAY_IN_MS : 0);

            // Figure out whether a view with this item ID existed before
            final LayoutParams lp = (LayoutParams) childToAnimate.getLayoutParams();

            final ViewRectPair viewRectPair = mChildRectsForAnimation.get(lp.id);

            final int xTranslation;
            final int yTranslation;

            // If there is a valid {@link Rect} for the view with this newId, then
            // setup an animation.
            if (viewRectPair != null && viewRectPair.rect != null) {
                // In the special case where the items are explicitly fading, we don't want to do
                // any of the translations.
                if (animationInMode == SgvAnimationHelper.AnimationIn.FADE) {
                    SgvAnimationHelper.addFadeAnimators(animators, childToAnimate, 0 /* start alpha */,
                            1.0f /* end alpha */, animationDelay);
                    continue;
                }

                final Rect oldRect = viewRectPair.rect;
                // Since the view already exists, translate it to its new position.
                // Reset the child back to its previous position given by oldRect if the child
                // has not already been translated.  If the child has been translated, use the
                // current translated values, as this child may be in the middle of a previous
                // animation, so we don't want to simply force it to new location.

                xTranslation = oldRect.left - childToAnimate.getLeft();
                yTranslation = oldRect.top - childToAnimate.getTop();
                final float rotation = childToAnimate.getRotation();

                // First set the translation X and Y. The current translation might be out of date.
                childToAnimate.setTranslationX(xTranslation);
                childToAnimate.setTranslationY(yTranslation);

                if (xTranslation == 0 && yTranslation == 0 && rotation == 0) {
                    // Bail early if this view doesn't need to be translated.
                    continue;
                }

                SgvAnimationHelper.addTranslationRotationAnimators(animators, childToAnimate, xTranslation,
                        yTranslation, rotation, animationDelay);
            } else {
                // If this view was not present before the data updated, rather than just flashing
                // the view into its designated position, fly it up from the bottom.
                xTranslation = 0;
                yTranslation = (animationInMode == SgvAnimationHelper.AnimationIn.FLY_IN_NEW_VIEWS) ? getHeight()
                        : 0;

                // Since this is a new view coming in, add additional delays so that these IN
                // animations start after all the OUT animations have been played.
                animationDelay += SgvAnimationHelper.getDefaultAnimationDuration();

                childToAnimate.setTranslationX(xTranslation);
                childToAnimate.setTranslationY(yTranslation);

                switch (animationInMode) {
                case FLY_IN_NEW_VIEWS:
                    SgvAnimationHelper.addTranslationRotationAnimators(animators, childToAnimate, xTranslation,
                            yTranslation, SgvAnimationHelper.ANIMATION_ROTATION_DEGREES, animationDelay);
                    break;

                case SLIDE_IN_NEW_VIEWS:
                    // Bias towards sliding right, but depending on the column that this view
                    // is laid out in, slide towards the nearest side edge.
                    int startTranslation = (int) (childToAnimate.getWidth() * 1.5);
                    if (lp.column < (mColCount / 2)) {
                        startTranslation = -startTranslation;
                    }

                    SgvAnimationHelper.addSlideInFromRightAnimators(animators, childToAnimate, startTranslation,
                            animationDelay);
                    break;

                case EXPAND_NEW_VIEWS:
                case EXPAND_NEW_VIEWS_NO_CASCADE:
                    if (i == 0) {
                        // Initially set the alpha of this view to be invisible, then fade in.
                        childToAnimate.setAlpha(0);

                        // Create animators that translate the view back to translation = 0
                        // which would be its new layout position
                        final int offset = -1 * childToAnimate.getHeight();
                        SgvAnimationHelper.addXYTranslationAnimators(animators, childToAnimate,
                                0 /* xTranslation */, offset, animationDelay);

                        SgvAnimationHelper.addFadeAnimators(animators, childToAnimate, 0 /* start alpha */,
                                1.0f /* end alpha */, animationDelay);
                    } else {
                        SgvAnimationHelper.addExpandInAnimators(animators, childToAnimate, animationDelay);
                    }
                    break;
                case FADE:
                    SgvAnimationHelper.addFadeAnimators(animators, childToAnimate, 0 /* start alpha */,
                            1.0f /* end alpha */, animationDelay);
                    break;

                default:
                    continue;
                }
            }

            viewsAnimated++;
        }

        return animators;
    }

    private void addStaleViewAnimationEndListener(final View view, List<Animator> viewAnimators) {
        if (viewAnimators == null) {
            return;
        }

        for (final Animator animator : viewAnimators) {
            animator.addListener(new AnimatorListenerAdapter() {
                @Override
                public void onAnimationEnd(Animator animation) {
                    // In the event that onChanged is called before this animation finishes,
                    // we would have mistakenly cached a view that would be recycled.  So
                    // check if it's there, and remove it so that obtainView() doesn't
                    // accidentally use the cached view later when it's already been
                    // moved to the recycler.
                    final LayoutParams lp = (LayoutParams) view.getLayoutParams();
                    if (mChildRectsForAnimation.containsKey(lp.id)) {
                        mChildRectsForAnimation.remove(lp.id);
                    }

                    recycleView(view);
                }
            });
        }
    }

    /**
     * Calculate and cache the {@link LayoutRecord}s for all positions up to mFirstPosition.
     * mFirstPosition is the position that layout will start from, but we need to know where all
     * views preceding it will be laid out so that mFirstPosition will be laid out at the correct
     * position.  If this is not done, mFirstPosition will be laid out at the first empty space
     * possible (i.e., top left), and this may not be the correct position in the overall layout.
     *
     * This can be optimized if we don't need to guard against jagged edges in the grid or if
     * mFirstChangedPosition is set to a non-zero value (so we can skip calculating some views).
     */
    private void calculateLayoutStartOffsets(int offset) {
        // Bail early if we don't guard against jagged edges or if nothing has changed before
        // mFirstPosition.
        // Also check that we're not at the top of the list because sometimes grid padding isn't set
        // until after mItemTops and mItemBottoms arrays have been initialized, so we should
        // go through and compute the right layout start offset for mFirstPosition = 0.
        if (mFirstPosition != 0 && (!mGuardAgainstJaggedEdges || mFirstPosition < mFirstChangedPosition)) {
            // At this time, we know that mItemTops should be the same, because
            // nothing has changed before view at mFirstPosition. The only thing
            // we need to do is to reset mItemBottoms. The result should be the
            // same, if we don't bail early and execute the following code
            // again. Notice that mItemBottoms always equal to mItemTops after
            // this method.
            System.arraycopy(mItemTops, 0, mItemBottoms, 0, mColCount);
            return;
        }

        final int colWidth = (getWidth() - getPaddingLeft() - getPaddingRight() - mItemMargin * (mColCount - 1))
                / mColCount;

        Arrays.fill(mItemTops, getPaddingTop());
        Arrays.fill(mItemBottoms, getPaddingTop());

        // Since we will be doing a pass to calculate all views up to mFirstPosition, it is likely
        // that all existing {@link LayoutRecord}s will be stale, so clear it out to avoid
        // accidentally the re-use of stale values.
        //
        // Note: We cannot just invalidate all layout records after mFirstPosition because it is
        // possible that this layout pass is caused by a down sync from the server that may affect
        // the layout of views from position 0 to mFirstPosition - 1.
        if (mDataChanged) {
            mLayoutRecords.clear();
        }

        for (int i = 0; i < mFirstPosition; i++) {
            LayoutRecord rec = mLayoutRecords.get(i);

            if (mDataChanged || rec == null) {
                final View view = obtainView(i, null);
                final LayoutParams lp = (LayoutParams) view.getLayoutParams();

                final int heightSpec;
                if (lp.height == LayoutParams.WRAP_CONTENT) {
                    heightSpec = MeasureSpec.makeMeasureSpec(0, MeasureSpec.UNSPECIFIED);
                } else {
                    heightSpec = MeasureSpec.makeMeasureSpec(lp.height, MeasureSpec.EXACTLY);
                }

                final int span = Math.min(mColCount, lp.span);
                final int widthSize = colWidth * span + mItemMargin * (span - 1);
                final int widthSpec = MeasureSpec.makeMeasureSpec(widthSize, MeasureSpec.EXACTLY);

                view.measure(widthSpec, heightSpec);
                final int height = view.getMeasuredHeight();

                if (rec == null) {
                    rec = new LayoutRecord();
                    mLayoutRecords.put(i, rec);
                }

                rec.height = height;
                rec.id = lp.id;
                rec.span = span;

                // We're not actually using this view, so add this back to the recycler.
                mRecycler.addScrap(view);
            }

            int nextColumn = getNextColumnDown();

            // Given the span, check if there's enough space to put this view at this column.
            // IMPORTANT Use the same logic in {@link #layoutChildren}.
            if (rec.span > 1) {
                if (mIsRtlLayout) {
                    if (nextColumn + 1 < rec.span) {
                        nextColumn = mColCount - 1;
                    }
                } else {
                    if (mColCount - nextColumn < rec.span) {
                        nextColumn = 0;
                    }
                }
            }
            rec.column = nextColumn;

            // Place the top of this child beneath the last by finding the lowest coordinate across
            // the columns that this child will span.  For LTR layout, we scan across from left to
            // right, and for RTL layout, we scan from right to left.
            // TODO: Consolidate this logic with getNextRecordDown() in the future, as that method
            // already calculates the margins for us.  This will keep the implementation consistent
            // with layoutChildren(), fillUp() and fillDown().
            int lowest = mItemBottoms[nextColumn] + mItemMargin;
            if (rec.span > 1) {
                for (int spanIndex = 0; spanIndex < rec.span; spanIndex++) {
                    final int index = mIsRtlLayout ? nextColumn - spanIndex : nextColumn + spanIndex;
                    final int bottom = mItemBottoms[index] + mItemMargin;
                    if (bottom > lowest) {
                        lowest = bottom;
                    }
                }
            }

            for (int spanIndex = 0; spanIndex < rec.span; spanIndex++) {
                final int col = mIsRtlLayout ? nextColumn - spanIndex : nextColumn + spanIndex;
                mItemBottoms[col] = lowest + rec.height;

                if (Log.isLoggable(TAG, Log.VERBOSE)) {
                    Log.v(TAG, " position: " + i + " bottoms: ");
                    for (int j = 0; j < mColCount; j++) {
                        Log.v(TAG, "    mItemBottoms[" + j + "]: " + mItemBottoms[j]);
                    }
                }
            }
        }

        // mItemBottoms[] at this point contains the values of all views up to mFirstPosition.  To
        // figure out where view at mFirstPosition will be laid out, we'll need to find the column
        // that is the highest (i.e., i where mItemBottoms[i] <= mItemBottoms[j] for all j
        // from 0 to mColCount.)
        int highestValue = Integer.MAX_VALUE;
        for (int k = 0; k < mColCount; k++) {
            if (mItemBottoms[k] < highestValue) {
                highestValue = mItemBottoms[k];
            }
        }

        // Adjust the offsets in each column so that values in mItemTops[] and mItemBottoms[]
        // reflect coordinates on screen.  These offsets will be the actual values where layout
        // will start from, otherwise, we'd naively start at (leftPadding, topPadding) for
        // mFirstPosition.
        for (int k = 0; k < mColCount; k++) {
            mItemBottoms[k] = mItemBottoms[k] - highestValue + offset;
            mItemTops[k] = mItemBottoms[k];

            // Log.v(TAG, "Adjusting to offset = mItemBottoms[" + k + "]: " + mItemBottoms[k]);
        }
    }

    /**
     * Measure and layout all currently visible children.
     *
     * @param queryAdapter true to requery the adapter for view data
     */
    final void layoutChildren(boolean queryAdapter) {
        final int paddingLeft = getPaddingLeft();
        final int paddingRight = getPaddingRight();
        final int itemMargin = mItemMargin;
        final int availableWidth = (getWidth() - paddingLeft - paddingRight - itemMargin * (mColCount - 1));
        final int colWidth = availableWidth / mColCount;
        // The availableWidth may not be divisible by mColCount. Keep the
        // remainder. It will be added to the width of the last view in the row.
        final int remainder = availableWidth % mColCount;

        boolean viewsRemovedInLayout = false;

        // If we're animating out stale views, then we want to defer recycling of views.
        final boolean deferRecyclingForAnimation = mAnimationOutMode != SgvAnimationHelper.AnimationOut.NONE;

        if (!deferRecyclingForAnimation) {
            final int childCount = getChildCount();
            // If the latest data set has fewer data items than mFirstPosition, don't keep any
            // views on screen, and just let the layout logic below retrieve appropriate views
            // from the recycler.
            final int viewsToKeepOnScreen = (mItemCount <= mFirstPosition) ? 0 : mItemCount - mFirstPosition;

            if (childCount > viewsToKeepOnScreen) {
                // If there are more views laid out than the number of data items remaining to be
                // laid out, recycle the extraneous views.
                recycleViewsInRange(viewsToKeepOnScreen, childCount - 1);
                viewsRemovedInLayout = true;
            }
        } else {
            mViewsToAnimateOut.clear();
        }

        for (int i = 0; i < getChildCount(); i++) {
            final int position = mFirstPosition + i;
            View child = getChildAt(i);

            final int highestAvailableLayoutPosition = mItemBottoms[getNextColumnDown()];
            if (deferRecyclingForAnimation
                    && (position >= mItemCount || highestAvailableLayoutPosition >= getHeight())) {
                // For the remainder of views on screen, they should not be on screen, so we can
                // skip layout.  Add them to the list of views to animate out.
                // We should only get in this position if deferRecyclingForAnimation = true,
                // otherwise, we should've recycled all views before getting into this layout loop.
                mViewsToAnimateOut.add(child);
                continue;
            }

            LayoutParams lp = null;
            int col = -1;

            if (child != null) {
                lp = (LayoutParams) child.getLayoutParams();
                col = lp.column;
            }

            final boolean needsLayout = queryAdapter || child == null || child.isLayoutRequested();
            if (queryAdapter) {
                View newView = null;
                if (deferRecyclingForAnimation) {
                    // If we are deferring recycling for animation, then we don't want to pass the
                    // current child in to obtainView for re-use.  obtainView() in this case should
                    // try to find the view belonging to this item on screen, or populate a fresh
                    // one from the recycler.
                    newView = obtainView(position);
                } else {
                    newView = obtainView(position, child);
                }

                // Update layout params since they may have changed
                lp = (LayoutParams) newView.getLayoutParams();

                if (newView != child) {
                    if (child != null && !deferRecyclingForAnimation) {
                        mRecycler.addScrap(child);
                        removeViewInLayout(child);
                        viewsRemovedInLayout = true;
                    }

                    // If this view is already in the layout hierarchy, we can just detach it
                    // from the parent and re-attach it at the correct index.  If the view has
                    // already been removed from the layout hierarchy, getParent() == null.
                    if (newView.getParent() == this) {
                        detachViewFromParent(newView);
                        attachViewToParent(newView, i, lp);
                    } else {
                        addViewInLayout(newView, i, lp);
                    }
                }

                child = newView;

                // Since the data has changed, we need to make sure the next child is in the
                // right column. We choose the next column down (vs. next column up) because we
                // are filling from the top of the screen downwards as we iterate through
                // visible children. (We take span into account below.)
                lp.column = getNextColumnDown();
                col = lp.column;
            }

            setReorderingArea(lp);

            final int span = Math.min(mColCount, lp.span);

            // Given the span, check if there's enough space to put this view at this column.
            // IMPORTANT Propagate the same logic to {@link #calculateLayoutStartOffsets}.
            if (span > 1) {
                if (mIsRtlLayout) {
                    // For RTL layout, if the current column index is less than the span of the
                    // child, then we know that there is not enough room remaining to lay this
                    // child out (e.g., if col == 0, but span == 2, then laying this child down
                    // at column = col would put us out of bound into a negative column index.).
                    // For this scenario, reset the index back to the right-most column, and lay
                    // out the child at this position where we can ensure that we can display as
                    // much of the child as possible.
                    if (col + 1 < span) {
                        col = mColCount - 1;
                    }
                } else {
                    if (mColCount - col < span) {
                        // If not, reset the col to 0.
                        col = 0;
                    }
                }

                lp.column = col;
            }

            int widthSize = (colWidth * span + itemMargin * (span - 1));
            // If it is rtl, we layout the view from col to col - span +
            // 1. If it reaches the most left column, i.e. we added the
            // additional width. So the check it span == col +1
            if ((mIsRtlLayout && span == col + 1) || (!mIsRtlLayout && span + col == mColCount)) {
                widthSize += remainder;
            }
            if (needsLayout) {
                final int widthSpec = MeasureSpec.makeMeasureSpec(widthSize, MeasureSpec.EXACTLY);

                final int heightSpec;
                if (lp.height == LayoutParams.WRAP_CONTENT) {
                    heightSpec = MeasureSpec.makeMeasureSpec(0, MeasureSpec.UNSPECIFIED);
                } else {
                    heightSpec = MeasureSpec.makeMeasureSpec(lp.height, MeasureSpec.EXACTLY);
                }

                child.measure(widthSpec, heightSpec);
            }

            // Place the top of this child beneath the last by finding the lowest coordinate across
            // the columns that this child will span.  For LTR layout, we scan across from left to
            // right, and for RTL layout, we scan from right to left.
            // TODO:  Consolidate this logic with getNextRecordDown() in the future, as that method
            // already calculates the margins for us.  This will keep the implementation consistent
            // with fillUp() and fillDown().
            int childTop = mItemBottoms[col] + mItemMargin;
            if (span > 1) {
                int lowest = childTop;
                for (int spanIndex = 0; spanIndex < span; spanIndex++) {
                    final int index = mIsRtlLayout ? col - spanIndex : col + spanIndex;
                    final int bottom = mItemBottoms[index] + mItemMargin;
                    if (bottom > lowest) {
                        lowest = bottom;
                    }
                }

                childTop = lowest;
            }

            final int childHeight = child.getMeasuredHeight();
            final int childBottom = childTop + childHeight;
            int childLeft = 0;
            int childRight = 0;
            if (mIsRtlLayout) {
                childRight = (getWidth() - paddingRight) - (mColCount - col - 1) * (colWidth + itemMargin);
                childLeft = childRight - child.getMeasuredWidth();
            } else {
                childLeft = paddingLeft + col * (colWidth + itemMargin);
                childRight = childLeft + child.getMeasuredWidth();
            }

            /*    Log.v(TAG, "[layoutChildren] height: " + childHeight
                + " top: " + childTop + " bottom: " + childBottom
                + " left: " + childLeft
                + " column: " + col
                + " position: " + position
                + " id: " + lp.id);
            */
            child.layout(childLeft, childTop, childRight, childBottom);
            if (lp.id == mFocusedChildIdToScrollIntoView) {
                child.requestFocus();
            }

            for (int spanIndex = 0; spanIndex < span; spanIndex++) {
                final int index = mIsRtlLayout ? col - spanIndex : col + spanIndex;
                mItemBottoms[index] = childBottom;
            }

            // Whether or not LayoutRecords may have already existed for the view at this position
            // on screen, we'll update it after we lay out to ensure that the LayoutRecord
            // has the most updated information about the view at this position.  We can be assured
            // that all views before those on screen (views with adapter position < mFirstPosition)
            // have the correct LayoutRecords because calculateLayoutStartOffsets() would have
            // set them appropriately.
            LayoutRecord rec = mLayoutRecords.get(position);
            if (rec == null) {
                rec = new LayoutRecord();
                mLayoutRecords.put(position, rec);
            }

            rec.column = lp.column;
            rec.height = childHeight;
            rec.id = lp.id;
            rec.span = span;
        }

        // It appears that removeViewInLayout() does not invalidate.  So if we make use of this
        // method during layout, we should invalidate explicitly.
        if (viewsRemovedInLayout || deferRecyclingForAnimation) {
            invalidate();
        }
    }

    /**
     * Set the reordering area for the child layout specified
     */
    private void setReorderingArea(LayoutParams childLayoutParams) {
        final boolean isLastColumn = childLayoutParams.column == (mColCount - 1);
        childLayoutParams.reorderingArea = mAdapter.getReorderingArea(childLayoutParams.position, isLastColumn);
    }

    final void invalidateLayoutRecordsBeforePosition(int position) {
        int endAt = 0;
        while (endAt < mLayoutRecords.size() && mLayoutRecords.keyAt(endAt) < position) {
            endAt++;
        }
        mLayoutRecords.removeAtRange(0, endAt);
    }

    final void invalidateLayoutRecordsAfterPosition(int position) {
        int beginAt = mLayoutRecords.size() - 1;
        while (beginAt >= 0 && mLayoutRecords.keyAt(beginAt) > position) {
            beginAt--;
        }
        beginAt++;
        mLayoutRecords.removeAtRange(beginAt + 1, mLayoutRecords.size() - beginAt);
    }

    /**
     * Before doing an animation, map the item IDs for the currently visible children to the
     * {@link Rect} that defines their position on the screen so a translation animation
     * can be applied to their new layout positions.
     */
    private void cacheChildRects() {
        final int childCount = getChildCount();
        mChildRectsForAnimation.clear();

        long originalDraggedChildId = -1;
        if (isDragReorderingSupported()) {
            originalDraggedChildId = mReorderHelper.getDraggedChildId();
            if (mCachedDragViewRect != null && originalDraggedChildId != -1) {
                // This child was dragged in a reordering operation.  Use the cached position
                // of where the drag event was released as the cached location.
                mChildRectsForAnimation.put(originalDraggedChildId,
                        new ViewRectPair(mDragView, mCachedDragViewRect));
                mCachedDragViewRect = null;
            }
        }

        for (int i = 0; i < childCount; i++) {
            final View child = getChildAt(i);
            final LayoutParams lp = (LayoutParams) child.getLayoutParams();

            Rect rect;
            if (lp.id != originalDraggedChildId) {
                final int childTop = (int) child.getY();
                final int childBottom = childTop + child.getHeight();
                final int childLeft = (int) child.getX();
                final int childRight = childLeft + child.getWidth();
                rect = new Rect(childLeft, childTop, childRight, childBottom);
                mChildRectsForAnimation.put(lp.id /* item id */, new ViewRectPair(child, rect));
            }
        }
    }

    /**
     * Should be called with mPopulating set to true
     *
     * @param fromPosition Position to start filling from
     * @param overhang the number of extra pixels to fill beyond the current top edge
     * @return the max overhang beyond the beginning of the view of any added items at the top
     */
    final int fillUp(int fromPosition, int overhang) {
        final int paddingLeft = getPaddingLeft();
        final int paddingRight = getPaddingRight();
        final int itemMargin = mItemMargin;
        final int availableWidth = (getWidth() - paddingLeft - paddingRight - itemMargin * (mColCount - 1));
        final int colWidth = availableWidth / mColCount;
        // The availableWidth may not be divisible by mColCount. Keep the
        // remainder. It will be added to the width of the last view in the row.
        final int remainder = availableWidth % mColCount;
        final int gridTop = getPaddingTop();
        final int fillTo = -overhang;
        int nextCol = getNextColumnUp();
        int position = fromPosition;

        while (nextCol >= 0 && mItemTops[nextCol] > fillTo && position >= 0) {
            final View child = obtainView(position, null);
            final LayoutParams lp = (LayoutParams) child.getLayoutParams();

            if (child.getParent() != this) {
                if (mInLayout) {
                    addViewInLayout(child, 0, lp);
                } else {
                    addView(child, 0);
                }
            }

            final int span = Math.min(mColCount, lp.span);

            LayoutRecord rec;
            if (span > 1) {
                rec = getNextRecordUp(position, span);
                nextCol = rec.column;
            } else {
                rec = mLayoutRecords.get(position);
            }

            boolean invalidateBefore = false;
            if (rec == null) {
                rec = new LayoutRecord();
                mLayoutRecords.put(position, rec);
                rec.column = nextCol;
                rec.span = span;
            } else if (span != rec.span) {
                rec.span = span;
                rec.column = nextCol;
                invalidateBefore = true;
            } else {
                nextCol = rec.column;
            }

            if (mHasStableIds) {
                rec.id = lp.id;
            }

            lp.column = nextCol;
            setReorderingArea(lp);

            int widthSize = colWidth * span + itemMargin * (span - 1);
            // If it is rtl, we layout the view from nextCol to nextCol - span +
            // 1. If it reaches the most left column, i.e. we added the
            // additional width. So the check it span == nextCol + 1
            if ((mIsRtlLayout && span == nextCol + 1) || (!mIsRtlLayout && span + nextCol == mColCount)) {
                widthSize += remainder;
            }
            final int widthSpec = MeasureSpec.makeMeasureSpec(widthSize, MeasureSpec.EXACTLY);
            final int heightSpec;
            if (lp.height == LayoutParams.WRAP_CONTENT) {
                heightSpec = MeasureSpec.makeMeasureSpec(0, MeasureSpec.UNSPECIFIED);
            } else {
                heightSpec = MeasureSpec.makeMeasureSpec(lp.height, MeasureSpec.EXACTLY);
            }
            child.measure(widthSpec, heightSpec);

            final int childHeight = child.getMeasuredHeight();
            if (invalidateBefore || (childHeight != rec.height && rec.height > 0)) {
                invalidateLayoutRecordsBeforePosition(position);
            }
            rec.height = childHeight;

            // Iterate across each column that this child spans and add the margin calculated
            // for that column to mItemTops.  getMarginBelow() is expected to give us the correct
            // margin values at each column such that mItemTops ends up with a smooth edge across
            // the column spans.  We need to do this before actually laying down the child,
            // otherwise we risk overlapping one child over another.  mItemTops stores the top
            // index for where the next child should be laid out.  For RTL, we do the update
            // in reverse order.
            for (int i = 0; i < span; i++) {
                final int index = mIsRtlLayout ? nextCol - i : nextCol + i;
                mItemTops[index] += rec.getMarginBelow(i);
            }

            final int startFrom = mItemTops[nextCol];
            final int childBottom = startFrom;
            final int childTop = childBottom - childHeight;

            int childLeft = 0;
            int childRight = 0;
            // For LTR layout, the child's left is calculated as the
            // (column index from left) * (columnWidth plus item margins).
            // For RTL layout, the child's left is relative to its right, and its right coordinate
            // is calculated as the difference between the width of this grid and
            // (column index from right) * (columnWidth plus item margins).
            if (mIsRtlLayout) {
                childRight = (getWidth() - paddingRight) - (mColCount - nextCol - 1) * (colWidth + itemMargin);
                childLeft = childRight - child.getMeasuredWidth();
            } else {
                childLeft = paddingLeft + nextCol * (colWidth + itemMargin);
                childRight = childLeft + child.getMeasuredWidth();
            }
            child.layout(childLeft, childTop, childRight, childBottom);

            Log.v(TAG, "[fillUp] position: " + position + " id: " + lp.id + " childLeft: " + childLeft
                    + " childTop: " + childTop + " column: " + rec.column + " childHeight:" + childHeight);

            // Since we're filling up, once the child is laid out, update mItemTops again
            // to reflect the next available top value at this column.  This is simply the child's
            // top coordinates, minus any available margins set.  For LTR, we start at the column
            // that this child is laid out from (nextCol) and move right for span amount.  For RTL
            // layout, we start at the column that this child is laid out from and move left.
            for (int i = 0; i < span; i++) {
                final int index = mIsRtlLayout ? nextCol - i : nextCol + i;
                mItemTops[index] = childTop - rec.getMarginAbove(i) - itemMargin;
            }

            if (lp.id == mFocusedChildIdToScrollIntoView) {
                child.requestFocus();
            }

            nextCol = getNextColumnUp();
            mFirstPosition = position--;
        }

        int highestView = getHeight();
        for (int i = 0; i < mColCount; i++) {
            if (mItemTops[i] < highestView) {
                highestView = mItemTops[i];
            }
        }
        return gridTop - highestView;
    }

    /**
     * Should be called with mPopulating set to true
     *
     * @param fromPosition Position to start filling from
     * @param overhang the number of extra pixels to fill beyond the current bottom edge
     * @return the max overhang beyond the end of the view of any added items at the bottom
     */
    final int fillDown(int fromPosition, int overhang) {
        final int paddingLeft = getPaddingLeft();
        final int paddingRight = getPaddingRight();
        final int itemMargin = mItemMargin;
        final int availableWidth = (getWidth() - paddingLeft - paddingRight - itemMargin * (mColCount - 1));
        final int colWidth = availableWidth / mColCount;
        // The availableWidth may not be divisible by mColCount. Keep the
        // remainder. It will be added to the width of the last view in the row.
        final int remainder = availableWidth % mColCount;
        final int gridBottom = getHeight() - getPaddingBottom();
        final int fillTo = gridBottom + overhang;
        int nextCol = getNextColumnDown();
        int position = fromPosition;

        while (nextCol >= 0 && mItemBottoms[nextCol] < fillTo && position < mItemCount) {
            final View child = obtainView(position, null);
            final LayoutParams lp = (LayoutParams) child.getLayoutParams();
            if (child.getParent() != this) {
                if (mInLayout) {
                    addViewInLayout(child, -1, lp);
                } else {
                    addView(child);
                }
            }

            final int span = Math.min(mColCount, lp.span);

            LayoutRecord rec;
            if (span > 1) {
                rec = getNextRecordDown(position, span);
                nextCol = rec.column;
            } else {
                rec = mLayoutRecords.get(position);
            }

            boolean invalidateAfter = false;
            if (rec == null) {
                rec = new LayoutRecord();
                mLayoutRecords.put(position, rec);
                rec.column = nextCol;
                rec.span = span;
            } else if (span != rec.span) {
                rec.span = span;
                rec.column = nextCol;
                invalidateAfter = true;
            } else {
                nextCol = rec.column;
            }

            if (mHasStableIds) {
                rec.id = lp.id;
            }

            lp.column = nextCol;
            setReorderingArea(lp);

            int widthSize = colWidth * span + itemMargin * (span - 1);
            // If it is rtl, we layout the view from nextCol to nextCol - span +
            // 1. If it reaches the most left column, i.e. we added the
            // additional width. So the check it span == nextCol +1
            if ((mIsRtlLayout && span == nextCol + 1) || (!mIsRtlLayout && span + nextCol == mColCount)) {
                widthSize += remainder;
            }
            final int widthSpec = MeasureSpec.makeMeasureSpec(widthSize, MeasureSpec.EXACTLY);
            final int heightSpec;
            if (lp.height == LayoutParams.WRAP_CONTENT) {
                heightSpec = MeasureSpec.makeMeasureSpec(0, MeasureSpec.UNSPECIFIED);
            } else {
                heightSpec = MeasureSpec.makeMeasureSpec(lp.height, MeasureSpec.EXACTLY);
            }
            child.measure(widthSpec, heightSpec);

            final int childHeight = child.getMeasuredHeight();
            if (invalidateAfter || (childHeight != rec.height && rec.height > 0)) {
                invalidateLayoutRecordsAfterPosition(position);
            }

            rec.height = childHeight;

            // Before laying out the child, we need to make sure mItemBottoms is updated with the
            // correct values such that there is a smooth edge across the child's span.
            // getMarginAbove() is expected to give us these values.  For LTR layout, we start at
            // nextCol, and update forward for the number of columns this child spans.  For RTL
            // layout, we start at nextCol and update backwards for the same number of columns.
            for (int i = 0; i < span; i++) {
                final int index = mIsRtlLayout ? nextCol - i : nextCol + i;
                mItemBottoms[index] += rec.getMarginAbove(i);
            }

            final int startFrom = mItemBottoms[nextCol];
            final int childTop = startFrom + itemMargin;
            final int childBottom = childTop + childHeight;
            int childLeft = 0;
            int childRight = 0;
            if (mIsRtlLayout) {
                childRight = (getWidth() - paddingRight) - (mColCount - nextCol - 1) * (colWidth + itemMargin);
                childLeft = childRight - child.getMeasuredWidth();
            } else {
                childLeft = paddingLeft + nextCol * (colWidth + itemMargin);
                childRight = childLeft + child.getMeasuredWidth();
            }

            Log.v(TAG, "[fillDown] position: " + position + " id: " + lp.id + " childLeft: " + childLeft
                    + " childTop: " + childTop + " column: " + rec.column + " childHeight:" + childHeight);

            child.layout(childLeft, childTop, childRight, childBottom);

            // Once we've laid down the child, update mItemBottoms again to reflect the next
            // available set of bottom values for the next child.
            for (int i = 0; i < span; i++) {
                final int index = mIsRtlLayout ? nextCol - i : nextCol + i;
                mItemBottoms[index] = childBottom + rec.getMarginBelow(i);
            }

            if (lp.id == mFocusedChildIdToScrollIntoView) {
                child.requestFocus();
            }

            nextCol = getNextColumnDown();
            position++;
        }

        int lowestView = 0;
        for (int i = 0; i < mColCount; i++) {
            final int index = mIsRtlLayout ? mColCount - (i + 1) : i;
            if (mItemBottoms[index] > lowestView) {
                lowestView = mItemBottoms[index];
            }
        }

        return lowestView - gridBottom;
    }

    /**
     * @return column that the next view filling upwards should occupy. This is the bottom-most
     *         position available for a single-column item.
     */
    final int getNextColumnUp() {
        int result = -1;
        int bottomMost = Integer.MIN_VALUE;

        final int colCount = mColCount;
        for (int i = colCount - 1; i >= 0; i--) {
            final int index = mIsRtlLayout ? colCount - (i + 1) : i;
            final int top = mItemTops[index];
            if (top > bottomMost) {
                bottomMost = top;
                result = index;
            }
        }

        return result;
    }

    /**
     * Return a LayoutRecord for the given position
     * @param position
     * @param span
     * @return
     */
    final LayoutRecord getNextRecordUp(int position, int span) {
        LayoutRecord rec = mLayoutRecords.get(position);
        if (rec == null || rec.span != span) {
            if (span > mColCount) {
                throw new IllegalStateException(
                        "Span larger than column count! Span:" + span + " ColumnCount:" + mColCount);
            }
            rec = new LayoutRecord();
            rec.span = span;
            mLayoutRecords.put(position, rec);
        }
        int targetCol = -1;
        int bottomMost = Integer.MIN_VALUE;

        // For LTR layout, we start from the bottom-right corner upwards when we need to find the
        // NextRecordUp.  For RTL, we will start from bottom-left.
        final int colCount = mColCount;
        if (mIsRtlLayout) {
            for (int i = span - 1; i < colCount; i++) {
                int top = Integer.MAX_VALUE;
                for (int j = i; j > i - span; j--) {
                    final int singleTop = mItemTops[j];
                    if (singleTop < top) {
                        top = singleTop;
                    }
                }
                if (top > bottomMost) {
                    bottomMost = top;
                    targetCol = i;
                }
            }
        } else {
            for (int i = colCount - span; i >= 0; i--) {
                int top = Integer.MAX_VALUE;
                for (int j = i; j < i + span; j++) {
                    final int singleTop = mItemTops[j];
                    if (singleTop < top) {
                        top = singleTop;
                    }
                }
                if (top > bottomMost) {
                    bottomMost = top;
                    targetCol = i;
                }
            }
        }

        rec.column = targetCol;

        // Once we've found the target column for the view at this position, we update mItemTops
        // for all columns that this view will occupy.  We set the margin such that mItemTops is
        // equal for all columns in the view's span.  For LTR layout, we start at targetCol and
        // move right, and for RTL, we start at targetCol and move left.
        for (int i = 0; i < span; i++) {
            final int nextCol = mIsRtlLayout ? targetCol - i : targetCol + i;
            rec.setMarginBelow(i, mItemTops[nextCol] - bottomMost);
        }

        return rec;
    }

    /**
     * @return column that the next view filling downwards should occupy. This is the top-most
     *         position available.
     */
    final int getNextColumnDown() {
        int topMost = Integer.MAX_VALUE;
        int result = 0;
        final int colCount = mColCount;

        for (int i = 0; i < colCount; i++) {
            final int index = mIsRtlLayout ? colCount - (i + 1) : i;
            final int bottom = mItemBottoms[index];
            if (bottom < topMost) {
                topMost = bottom;
                result = index;
            }
        }

        return result;
    }

    final LayoutRecord getNextRecordDown(int position, int span) {
        LayoutRecord rec = mLayoutRecords.get(position);
        if (rec == null || rec.span != span) {
            if (span > mColCount) {
                throw new IllegalStateException(
                        "Span larger than column count! Span:" + span + " ColumnCount:" + mColCount);
            }

            rec = new LayoutRecord();
            rec.span = span;
            mLayoutRecords.put(position, rec);
        }

        int targetCol = -1;
        int topMost = Integer.MAX_VALUE;

        final int colCount = mColCount;

        // For LTR layout, we start from the top-left corner and move right-downwards, when we
        // need to find the NextRecordDown.  For RTL we will start from Top-Right corner, and move
        // left-downwards.
        if (mIsRtlLayout) {
            for (int i = colCount - 1; i >= span - 1; i--) {
                int bottom = Integer.MIN_VALUE;
                for (int j = i; j > i - span; j--) {
                    final int singleBottom = mItemBottoms[j];
                    if (singleBottom > bottom) {
                        bottom = singleBottom;
                    }
                }
                if (bottom < topMost) {
                    topMost = bottom;
                    targetCol = i;
                }
            }
        } else {
            for (int i = 0; i <= colCount - span; i++) {
                int bottom = Integer.MIN_VALUE;
                for (int j = i; j < i + span; j++) {
                    final int singleBottom = mItemBottoms[j];
                    if (singleBottom > bottom) {
                        bottom = singleBottom;
                    }
                }
                if (bottom < topMost) {
                    topMost = bottom;
                    targetCol = i;
                }
            }
        }

        rec.column = targetCol;

        // Once we've found the target column for the view at this position, we update mItemBottoms
        // for all columns that this view will occupy.  We set the margins such that mItemBottoms
        // is equal for all columns in the view's span.  For LTR layout, we start at targetCol and
        // move right, and for RTL, we start at targetCol and move left.
        for (int i = 0; i < span; i++) {
            final int nextCol = mIsRtlLayout ? targetCol - i : targetCol + i;
            rec.setMarginAbove(i, topMost - mItemBottoms[nextCol]);
        }

        return rec;
    }

    private int getItemWidth(int itemColumnSpan) {
        final int colWidth = (getWidth() - getPaddingLeft() - getPaddingRight() - mItemMargin * (mColCount - 1))
                / mColCount;
        return colWidth * itemColumnSpan + mItemMargin * (itemColumnSpan - 1);
    }

    /**
     * Obtain a populated view from the adapter.  This method checks to see if the view to populate
     * is already laid out on screen somewhere by comparing the item ids.
     *
     * If the view is already laid out, and the view type has not changed, populate the contents
     * and return.
     *
     * If the view is not laid out on screen somewhere, grab a view from the recycler and populate.
     *
     * NOTE: This method should be called during layout.
     *
     * TODO: This can probably be consolidated with the overloaded {@link #obtainView(int, View)}.
     *
     * @param position Position to get the view for.
     */
    final View obtainView(int position) {
        // TODO: This method currently does not support transient state views.

        final Object item = mAdapter.getItem(position);

        View scrap = null;
        final int positionViewType = mAdapter.getItemViewType(item, position);

        final long id = mAdapter.getItemId(item, position);
        final ViewRectPair viewRectPair = mChildRectsForAnimation.get(id);
        if (viewRectPair != null) {
            scrap = viewRectPair.view;

            // TODO: Make use of stable ids by retrieving the cached views using stable ids.  In
            // theory, we should maintain a list of active views, and then fetch the views
            // from that list.  If that fails, then we should go to the recycler.
            // For the collection holding stable ids, we must ensure that those views don't get
            // repurposed for other items at different positions.
        }

        final int scrapViewType = scrap != null && (scrap.getLayoutParams() instanceof LayoutParams)
                ? ((LayoutParams) scrap.getLayoutParams()).viewType
                : -1;

        if (scrap == null || scrapViewType != positionViewType) {
            // If there is no cached view or the cached view's type no longer match the type
            // of the item at the specified position, retrieve a new view from the recycler and
            // recycle the cached view.
            if (scrap != null) {
                // The cached view we had is not valid, so add it to the recycler and
                // remove it from the current layout.
                recycleView(scrap);
            }

            scrap = mRecycler.getScrapView(positionViewType);
        }

        final int itemColumnSpan = mAdapter.getItemColumnSpan(item, position);
        final int itemWidth = getItemWidth(itemColumnSpan);
        final View view = mAdapter.getView(item, position, scrap, this, itemWidth);

        ViewGroup.LayoutParams lp = view.getLayoutParams();
        if (view.getParent() != this) {
            if (lp == null) {
                lp = generateDefaultLayoutParams();
            } else if (!checkLayoutParams(lp)) {
                lp = generateLayoutParams(lp);
            }

            view.setLayoutParams(lp);
        }

        final LayoutParams sglp = (LayoutParams) view.getLayoutParams();
        sglp.position = position;
        sglp.viewType = positionViewType;
        sglp.id = id;
        sglp.span = itemColumnSpan;

        // When the view at the positions we are tracking update, make sure to
        // update our views as well. That way, we have the correct
        // rectangle for comparing when the drag target enters/ leaves the
        // placeholder view.
        if (isDragReorderingSupported() && mReorderHelper.getDraggedChildId() == id) {
            mReorderHelper.updateDraggedChildView(view);
            mReorderHelper.updateDraggedOverChildView(view);
        }
        return view;
    }

    /**
     * Obtain a populated view from the adapter. If optScrap is non-null and is not
     * reused it will be placed in the recycle bin.
     *
     * @param position position to get view for
     * @param optScrap Optional scrap view; will be reused if possible
     * @return A new view, a recycled view from mRecycler, or optScrap
     */
    final View obtainView(int position, View optScrap) {
        View view = mRecycler.getTransientStateView(position);
        final Object item = mAdapter.getItem(position);
        final int positionViewType = mAdapter.getItemViewType(item, position);

        if (view == null) {
            // Reuse optScrap if it's of the right type (and not null)
            final int optType = optScrap != null ? ((LayoutParams) optScrap.getLayoutParams()).viewType : -1;

            final View scrap = optType == positionViewType ? optScrap : mRecycler.getScrapView(positionViewType);

            final int itemColumnSpan = mAdapter.getItemColumnSpan(item, position);
            final int itemWidth = getItemWidth(itemColumnSpan);
            view = mAdapter.getView(item, position, scrap, this, itemWidth);

            if (view != scrap && scrap != null) {
                // The adapter didn't use it; put it back.
                mRecycler.addScrap(scrap);
            }

            ViewGroup.LayoutParams lp = view.getLayoutParams();

            if (view.getParent() != this) {
                if (lp == null) {
                    lp = generateDefaultLayoutParams();
                } else if (!checkLayoutParams(lp)) {
                    lp = generateLayoutParams(lp);
                }

                view.setLayoutParams(lp);
            }
        }

        final LayoutParams sglp = (LayoutParams) view.getLayoutParams();
        sglp.position = position;
        sglp.viewType = positionViewType;
        final long id = mAdapter.getItemIdFromView(view, position);
        sglp.id = id;
        sglp.span = mAdapter.getItemColumnSpan(item, position);

        // When the view at the positions we are tracking update, make sure to
        // update our views as well. That way, we have the correct
        // rectangle for comparing when the drag target enters/ leaves the
        // placeholder view.
        if (isDragReorderingSupported() && mReorderHelper.getDraggedChildId() == id) {
            mReorderHelper.updateDraggedChildView(view);
            mReorderHelper.updateDraggedOverChildView(view);
        }

        return view;
    }

    /**
     * Animation mode to play for new data coming in as well as the stale data that should be
     * animated out.
     * @param animationIn The animation to play to introduce new or updated data into view
     * @param animationOut The animation to play to transition stale data out of view.
     */
    public void setAnimationMode(SgvAnimationHelper.AnimationIn animationIn,
            SgvAnimationHelper.AnimationOut animationOut) {
        mAnimationInMode = animationIn;
        mAnimationOutMode = animationOut;
    }

    public SgvAnimationHelper.AnimationIn getAnimationInMode() {
        return mAnimationInMode;
    }

    public SgvAnimationHelper.AnimationOut getAnimationOutMode() {
        return mAnimationOutMode;
    }

    public GridAdapter getAdapter() {
        return mAdapter;
    }

    public void setAdapter(GridAdapter adapter) {
        if (mAdapter != null) {
            mAdapter.unregisterDataSetObserver(mObserver);
        }

        clearAllState();

        mAdapter = adapter;
        mDataChanged = true;
        mItemCount = adapter != null ? adapter.getCount() : 0;

        if (adapter != null) {
            adapter.registerDataSetObserver(mObserver);
            mRecycler.setViewTypeCount(adapter.getViewTypeCount());
            mHasStableIds = adapter.hasStableIds();
        } else {
            mHasStableIds = false;
        }

        if (isDragReorderingSupported()) {
            updateReorderStates(ReorderUtils.DRAG_STATE_NONE);
        }

        updateEmptyStatus();
    }

    public void setAdapter(GridAdapter adapter, ScrollState scrollState) {
        setAdapter(adapter);
        mCurrentScrollState = scrollState;
    }

    /**
     * Clear all state because the grid will be used for a completely different set of data.
     */
    private void clearAllState() {
        // Clear all layout records and views
        mLayoutRecords.clear();
        removeAllViews();

        mItemTops = null;
        mItemBottoms = null;

        setSelectionToTop();

        // Clear recycler because there could be different view types now
        mRecycler.clear();

        // Reset the last touch y coordinate so that any animation/events won't use stale values.
        mLastTouchY = 0;

        // Reset the first changed position to 0. At least we will update all views.
        mFirstChangedPosition = 0;
    }

    /**
     * Scroll the list so the first visible position in the grid is the first item in the adapter.
     */
    public void setSelectionToTop() {
        mCurrentScrollState = null;
        setFirstPositionAndOffsets(0 /* position */, getPaddingTop() /* offset */);
    }

    /**
     * Get {@link #mFirstPosition}, which is the adapter position of the View
     * returned by getChildAt(0).
     */
    public int getCurrentFirstPosition() {
        return mFirstPosition;
    }

    /**
     * Indicate whether the scrolling state is currently at the topmost of this grid
     * @return boolean Indicates whether the current view is the top most of this grid.
     */
    private boolean isSelectionAtTop() {
        if (mCurrentScrollState != null && mCurrentScrollState.getAdapterPosition() == 0) {
            // ScrollState is how far the top of the first child is from the top of the screen, and
            // does not include top padding when the adapter position is the first child. If the
            // vertical offset of the scroll state is exactly equal to {@link #mItemMargin}, then
            // the first item, and therefore the view of the grid, is at the top.
            return mCurrentScrollState.getVerticalOffset() == mItemMargin;
        }

        return false;
    }

    /**
     * Set the first position and offset so that on layout, we would start laying out starting
     * with the specified position at the top of the view.
     * @param position The child position to place at the top of this view.
     * @param offset The vertical layout offset of the view at the specified position.
     */
    public void setFirstPositionAndOffsets(int position, int offset) {
        // Reset the first visible position in the grid to be item 0
        mFirstPosition = position;
        if (mItemTops == null || mItemBottoms == null) {
            mItemTops = new int[mColCount];
            mItemBottoms = new int[mColCount];
        }

        calculateLayoutStartOffsets(offset);
    }

    /**
     * Restore the view to the states specified by the {@link ScrollState}.
     * @param scrollState {@link ScrollState} containing the scroll states to restore to.
     */
    private void restoreScrollPosition(ScrollState scrollState) {
        if (mAdapter == null || scrollState == null || mAdapter.getCount() == 0) {
            return;
        }

        Log.v(TAG, "[restoreScrollPosition] " + scrollState);

        int targetPosition = 0;
        long itemId = -1;

        final int originalPosition = scrollState.getAdapterPosition();
        final int adapterCount = mAdapter.getCount();
        // ScrollState is defined as the vertical offset of the first item that is laid out
        // on screen.  To restore scroll state, we check within a window to see if we can
        // find that original first item in this new data set.  If we can, restore that item
        // to the first position on screen, offset by its previous vertical offset.  If we
        // cannot find that item, then we'll simply layout out everything from the beginning
        // again.

        // TODO:  Perhaps it is more efficient if we check the cursor in one direction first
        // before going backwards, rather than jumping back and forth as we are doing now.
        for (int i = 0; i < SCROLL_RESTORE_WINDOW_SIZE; i++) {
            if (originalPosition + i < adapterCount) {
                itemId = mAdapter.getItemId(originalPosition + i);
                if (itemId != -1 && itemId == scrollState.getItemId()) {
                    targetPosition = originalPosition + i;
                    break;
                }
            }

            if (originalPosition - i >= 0 && originalPosition - i < adapterCount) {
                itemId = mAdapter.getItemId(originalPosition - i);
                if (itemId != -1 && itemId == scrollState.getItemId()) {
                    targetPosition = originalPosition - i;
                    break;
                }
            }
        }

        // layoutChildren(), fillDown() and fillUp() always apply mItemMargin when laying out
        // views.  Since restoring scroll position is effectively laying out a particular child
        // as the first child, we need to ensure we strip mItemMargin from the offset, as it
        // will be re-applied when the view is laid out.
        //
        // Since top padding varies with screen orientation and is not stored in the scroll
        // state when the scroll adapter position is the first child, we add it here.
        int offset = scrollState.getVerticalOffset() - mItemMargin;
        if (targetPosition == 0) {
            offset += getPaddingTop();
        }

        setFirstPositionAndOffsets(targetPosition, offset);
        mCurrentScrollState = null;
    }

    /**
     * Return the current scroll state of this view.
     * @return {@link ScrollState} The current scroll state
     */
    public ScrollState getScrollState() {
        final View v = getChildAt(0);
        if (v == null) {
            return null;
        }

        final LayoutParams lp = (LayoutParams) v.getLayoutParams();
        // Since top padding varies with screen orientation, it is not stored in the scroll state
        // when the scroll adapter position is the first child.
        final int offset = (lp.position == 0 ? v.getTop() - getPaddingTop() : v.getTop());
        return new ScrollState(lp.id, lp.position, offset);
    }

    /**
     * NOTE This method is borrowed from {@link ScrollView}.
     */
    @Override
    public boolean requestChildRectangleOnScreen(View child, Rect rectangle, boolean immediate) {
        // offset into coordinate space of this scroll view
        rectangle.offset(child.getLeft() - child.getScrollX(), child.getTop() - child.getScrollY());

        return scrollToChildRect(rectangle, immediate);
    }

    /**
     * If rect is off screen, scroll just enough to get it (or at least the
     * first screen size chunk of it) on screen.
     * NOTE This method is borrowed from {@link ScrollView}.
     *
     * @param rect      The rectangle.
     * @param immediate True to scroll immediately without animation. Not used here.
     * @return true if scrolling was performed
     */
    private boolean scrollToChildRect(Rect rect, boolean immediate) {
        final int delta = computeScrollDeltaToGetChildRectOnScreen(rect);
        final boolean scroll = delta != 0;
        if (scroll) {
            // TODO smoothScrollBy if immediate is false.
            scrollBy(0, delta);
        }
        return scroll;
    }

    @Override
    protected void onSizeChanged(int w, int h, int oldw, int oldh) {
        super.onSizeChanged(w, h, oldw, oldh);

        if (mOnSizeChangedListener != null) {
            mOnSizeChangedListener.onSizeChanged(w, h, oldw, oldh);
        }

        // NOTE Below is borrowed from {@link ScrollView}.
        final View currentFocused = findFocus();
        if (null == currentFocused || this == currentFocused) {
            return;
        }

        // If the currently-focused view was visible on the screen when the
        // screen was at the old height, then scroll the screen to make that
        // view visible with the new screen height.
        if (isWithinDeltaOfScreen(currentFocused, 0, oldh)) {
            currentFocused.getDrawingRect(mTempRect);
            offsetDescendantRectToMyCoords(currentFocused, mTempRect);
            scrollBy(0, computeScrollDeltaToGetChildRectOnScreen(mTempRect));
        }
    }

    /**
     *
     * NOTE This method is borrowed from {@link ScrollView}.
     *
     * @return whether the descendant of this scroll view is within delta
     *  pixels of being on the screen.
     */
    private boolean isWithinDeltaOfScreen(View descendant, int delta, int height) {
        descendant.getDrawingRect(mTempRect);
        offsetDescendantRectToMyCoords(descendant, mTempRect);

        return (mTempRect.bottom + delta) >= getScrollY() && (mTempRect.top - delta) <= (getScrollY() + height);
    }

    /**
     * NOTE: borrowed from {@link GridView}
     * Comments from {@link View}
     *
     * Compute the vertical extent of the vertical scrollbar's thumb within the vertical range.
     * This value is used to compute the length of the thumb within the scrollbar's track.
     * The range is expressed in arbitrary units that must be the same as the units used by
     * {@link #computeVerticalScrollRange} and {@link #computeVerticalScrollOffset}.
     *
     * The default extent is the drawing height of this view.
     *
     * @return the vertical extent of the scrollbar's thumb
     */
    @Override
    protected int computeVerticalScrollExtent() {

        final int count = getChildCount();
        if (count > 0) {
            if (mSmoothScrollbarEnabled) {
                final int rowCount = (count + mColCount - 1) / mColCount;
                int extent = rowCount * SCROLLING_ESTIMATED_ITEM_HEIGHT;

                View view = getChildAt(0);
                final int top = view.getTop();
                int height = view.getHeight();
                if (height > 0) {
                    extent += (top * SCROLLING_ESTIMATED_ITEM_HEIGHT) / height;
                }

                view = getChildAt(count - 1);
                final int bottom = view.getBottom();
                height = view.getHeight();
                if (height > 0) {
                    extent -= ((bottom - getHeight()) * SCROLLING_ESTIMATED_ITEM_HEIGHT) / height;
                }

                return extent;
            } else {
                return 1;
            }
        }
        return 0;
    }

    /**
     * NOTE: borrowed from {@link GridView} and altered as appropriate to accommodate for
     * {@link StaggeredGridView}
     *
     * Comments from {@link View}
     *
     * Compute the vertical offset of the vertical scrollbar's thumb within the horizontal range.
     * This value is used to compute the position of the thumb within the scrollbar's track.
     * The range is expressed in arbitrary units that must be the same as the units used by
     * {@link #computeVerticalScrollRange()} and {@link #computeVerticalScrollExtent()}.
     *
     * The default offset is the scroll offset of this view.
     *
     * @return the vertical offset of the scrollbar's thumb
     */
    @Override
    protected int computeVerticalScrollOffset() {
        final int firstPosition = mFirstPosition;
        final int childCount = getChildCount();
        final int paddingTop = getPaddingTop();

        if (firstPosition >= 0 && childCount > 0) {
            if (mSmoothScrollbarEnabled) {
                final View view = getChildAt(0);
                final int top = view.getTop();
                final int currentTopViewHeight = view.getHeight();
                if (currentTopViewHeight > 0) {
                    // In an ideal world, all items would have a fixed height that we would know
                    // a priori, calculating the scroll offset would simply be:
                    //     [A] (mFirstPosition * fixedHeight) - childView[0].top
                    //         where childView[0] is the first view on screen.
                    //
                    // However, given that we do not know the height ahead of time, and that each
                    // item in this grid can have varying heights, we'd need to assign an arbitrary
                    // item height (SCROLLING_ESTIMATED_ITEM_HEIGHT) in order to estimate the scroll
                    // offset.  The previous equation thus transforms to:
                    //     [B] (mFirstPosition * SCROLLING_ESTIMATED_ITEM_HEIGHT) -
                    //         ((childView[0].top * SCROLLING_ESTIMATED_ITEM_HEIGHT) /
                    //          childView[0].height)
                    //
                    // Equation [B] gives a pretty good calculation of the offset if this were a
                    // single column grid view, for a multi-column grid, one slight modification is
                    // needed:
                    //     [C] ((mFirstPosition * SCROLLING_ESTIMATED_ITEM_HEIGHT) / mColCount) -
                    //         ((childView[0].top * SCROLLING_ESTIMATED_ITEM_HEIGHT) /
                    //          childView[0].height)
                    final int estimatedScrollOffset = ((firstPosition * SCROLLING_ESTIMATED_ITEM_HEIGHT)
                            / mColCount) - ((top * SCROLLING_ESTIMATED_ITEM_HEIGHT) / currentTopViewHeight);

                    final int rowCount = (mItemCount + mColCount - 1) / mColCount;
                    final int overScrollCompensation = (int) ((float) getScrollY() / getHeight() * rowCount
                            * SCROLLING_ESTIMATED_ITEM_HEIGHT);

                    int val = Math.max(estimatedScrollOffset + overScrollCompensation, 0);
                    // If mFirstPosition is currently the very first item in the adapter, check to
                    // see if we need to take into account any top padding.  This is so that we
                    // don't return 0 when in fact the user may still be scrolling through some
                    // top padding.
                    if (firstPosition == 0 && paddingTop > 0) {
                        val += paddingTop - top + mItemMargin;
                    }
                    return val;
                }
            } else {
                int index;
                final int count = mItemCount;
                if (firstPosition == 0) {
                    index = 0;
                } else if (firstPosition + childCount == count) {
                    index = count;
                } else {
                    index = firstPosition + childCount / 2;
                }
                return (int) (firstPosition + childCount * (index / (float) count));
            }
        }

        return paddingTop;
    }

    /**
     * NOTE: borrowed from {@link GridView} and altered as appropriate to accommodate for
     * {@link StaggeredGridView}
     *
     * Comments from {@link View}
     *
     * Compute the vertical range that the vertical scrollbar represents.
     * The range is expressed in arbitrary units that must be the same as the units used by
     * {@link #computeVerticalScrollExtent} and {@link #computeVerticalScrollOffset}.
     *
     * The default range is the drawing height of this view.
     *
     * @return the total vertical range represented by the vertical scrollbar
     */
    @Override
    protected int computeVerticalScrollRange() {
        final int rowCount = (mItemCount + mColCount - 1) / mColCount;
        int result = Math.max(rowCount * SCROLLING_ESTIMATED_ITEM_HEIGHT, 0);

        if (mSmoothScrollbarEnabled) {
            if (getScrollY() != 0) {
                // Compensate for overscroll
                result += Math.abs(
                        (int) ((float) getScrollY() / getHeight() * rowCount * SCROLLING_ESTIMATED_ITEM_HEIGHT));
            }
        } else {
            result = mItemCount;
        }

        return result;
    }

    /**
     * Compute the amount to scroll in the Y direction in order to get
     * a rectangle completely on the screen (or, if taller than the screen,
     * at least the first screen size chunk of it).
     *
     * NOTE This method is borrowed from {@link ScrollView}.
     *
     * @param rect The rect.
     * @return The scroll delta.
     */
    protected int computeScrollDeltaToGetChildRectOnScreen(Rect rect) {
        if (getChildCount() == 0) {
            return 0;
        }

        final int height = getHeight();
        final int fadingEdge = getVerticalFadingEdgeLength();

        int screenTop = getScrollY();
        int screenBottom = screenTop + height;

        // leave room for top fading edge as long as rect isn't at very top
        if (rect.top > 0) {
            screenTop += fadingEdge;
        }

        // leave room for bottom fading edge as long as rect isn't at very bottom
        if (rect.bottom < getHeight()) {
            screenBottom -= fadingEdge;
        }

        int scrollYDelta = 0;

        if (rect.bottom > screenBottom && rect.top > screenTop) {
            // need to move down to get it in view: move down just enough so
            // that the entire rectangle is in view (or at least the first
            // screen size chunk).

            if (rect.height() > height) {
                // just enough to get screen size chunk on
                scrollYDelta = screenTop - rect.top;
            } else {
                // get entire rect at bottom of screen
                scrollYDelta = screenBottom - rect.bottom;
            }
        } else if (rect.top < screenTop && rect.bottom < screenBottom) {
            // need to move up to get it in view: move up just enough so that
            // entire rectangle is in view (or at least the first screen
            // size chunk of it).

            if (rect.height() > height) {
                // screen size chunk
                scrollYDelta = screenBottom - rect.bottom;
            } else {
                // entire rect at top
                scrollYDelta = screenTop - rect.top;
            }
        }
        return scrollYDelta;
    }

    @Override
    protected LayoutParams generateDefaultLayoutParams() {
        return new LayoutParams(LayoutParams.WRAP_CONTENT);
    }

    @Override
    protected LayoutParams generateLayoutParams(ViewGroup.LayoutParams lp) {
        return new LayoutParams(lp);
    }

    @Override
    protected boolean checkLayoutParams(ViewGroup.LayoutParams lp) {
        return lp instanceof LayoutParams;
    }

    @Override
    public ViewGroup.LayoutParams generateLayoutParams(AttributeSet attrs) {
        return new LayoutParams(getContext(), attrs);
    }

    @Override
    public Parcelable onSaveInstanceState() {
        final Parcelable superState = super.onSaveInstanceState();
        final SavedState ss = new SavedState(superState);
        final int position = mFirstPosition;
        ss.position = position;
        if (position >= 0 && mAdapter != null && position < mAdapter.getCount()) {
            ss.firstId = mAdapter.getItemId(position);
        }
        if (getChildCount() > 0) {
            // Since top padding varies with screen orientation, it is not stored in the scroll
            // state when the scroll adapter position is the first child.
            ss.topOffset = position == 0 ? getChildAt(0).getTop() - getPaddingTop() : getChildAt(0).getTop();
        }
        return ss;
    }

    @Override
    public void onRestoreInstanceState(Parcelable state) {
        final SavedState ss = (SavedState) state;
        super.onRestoreInstanceState(ss.getSuperState());
        mDataChanged = true;
        mFirstPosition = ss.position;
        mCurrentScrollState = new ScrollState(ss.firstId, ss.position, ss.topOffset);
        requestLayout();
    }

    public static class LayoutParams extends ViewGroup.LayoutParams {
        private static final int[] LAYOUT_ATTRS = new int[] { android.R.attr.layout_span };

        private static final int SPAN_INDEX = 0;

        /**
         * The number of columns this item should span
         */
        public int span = 1;

        /**
         * Item position this view represents
         */
        public int position = -1;

        /**
         * Type of this view as reported by the adapter
         */
        int viewType;

        /**
         * The column this view is occupying
         */
        int column;

        /**
         * The stable ID of the item this view displays
         */
        long id = -1;

        /**
         * The position where reordering can happen for this view
         */
        public int reorderingArea = ReorderUtils.REORDER_AREA_NONE;

        public LayoutParams(int height) {
            super(MATCH_PARENT, height);

            if (this.height == MATCH_PARENT) {
                Log.w(TAG, "Constructing LayoutParams with height FILL_PARENT - "
                        + "impossible! Falling back to WRAP_CONTENT");
                this.height = WRAP_CONTENT;
            }
        }

        public LayoutParams(Context c, AttributeSet attrs) {
            super(c, attrs);

            if (this.width != MATCH_PARENT) {
                Log.w(TAG, "Inflation setting LayoutParams width to " + this.width + " - must be MATCH_PARENT");
                this.width = MATCH_PARENT;
            }
            if (this.height == MATCH_PARENT) {
                Log.w(TAG, "Inflation setting LayoutParams height to MATCH_PARENT - "
                        + "impossible! Falling back to WRAP_CONTENT");
                this.height = WRAP_CONTENT;
            }

            final TypedArray a = c.obtainStyledAttributes(attrs, LAYOUT_ATTRS);
            span = a.getInteger(SPAN_INDEX, 1);
            a.recycle();
        }

        public LayoutParams(ViewGroup.LayoutParams other) {
            super(other);

            if (this.width != MATCH_PARENT) {
                Log.w(TAG, "Constructing LayoutParams with width " + this.width + " - must be MATCH_PARENT");
                this.width = MATCH_PARENT;
            }
            if (this.height == MATCH_PARENT) {
                Log.w(TAG, "Constructing LayoutParams with height MATCH_PARENT - "
                        + "impossible! Falling back to WRAP_CONTENT");
                this.height = WRAP_CONTENT;
            }
        }
    }

    private class RecycleBin {
        private ArrayList<View>[] mScrapViews;
        private int mViewTypeCount;
        private int mMaxScrap;

        private SparseArray<View> mTransientStateViews;

        public void setViewTypeCount(int viewTypeCount) {
            if (viewTypeCount < 1) {
                throw new IllegalArgumentException(
                        "Must have at least one view type (" + viewTypeCount + " types reported)");
            }
            if (viewTypeCount == mViewTypeCount) {
                return;
            }

            final ArrayList<View>[] scrapViews = new ArrayList[viewTypeCount];
            for (int i = 0; i < viewTypeCount; i++) {
                scrapViews[i] = new ArrayList<View>();
            }
            mViewTypeCount = viewTypeCount;
            mScrapViews = scrapViews;
        }

        public void clear() {
            final int typeCount = mViewTypeCount;
            for (int i = 0; i < typeCount; i++) {
                mScrapViews[i].clear();
            }
            if (mTransientStateViews != null) {
                mTransientStateViews.clear();
            }
        }

        public void clearTransientViews() {
            if (mTransientStateViews != null) {
                mTransientStateViews.clear();
            }
        }

        public void addScrap(View v) {
            if (!(v.getLayoutParams() instanceof LayoutParams)) {
                return;
            }

            final LayoutParams lp = (LayoutParams) v.getLayoutParams();
            if (ViewCompat.hasTransientState(v)) {
                if (mTransientStateViews == null) {
                    mTransientStateViews = new SparseArray<View>();
                }
                mTransientStateViews.put(lp.position, v);
                return;
            }

            final int childCount = getChildCount();
            if (childCount > mMaxScrap) {
                mMaxScrap = childCount;
            }

            // Clear possible modified states applied to the view when adding to the recycler.
            // This view may have been part of a cancelled animation, so clear that state so that
            // future consumer of this view won't have to deal with states from its past life.
            v.setTranslationX(0);
            v.setTranslationY(0);
            v.setRotation(0);
            v.setAlpha(1.0f);
            v.setScaleY(1.0f);

            final ArrayList<View> scrap = mScrapViews[lp.viewType];
            if (scrap.size() < mMaxScrap) {
                // The number of scraps have not yet exceeded our limit, check to see that this
                // view does not already exist in the recycler.  This can happen if a caller
                // mistakenly calls addScrap(view) multiple times for the same view.
                if (!scrap.contains(v)) {
                    scrap.add(v);
                }
            }
        }

        public View getTransientStateView(int position) {
            if (mTransientStateViews == null) {
                return null;
            }

            final View result = mTransientStateViews.get(position);
            if (result != null) {
                mTransientStateViews.remove(position);
            }
            return result;
        }

        public View getScrapView(int type) {
            final ArrayList<View> scrap = mScrapViews[type];
            if (scrap.isEmpty()) {
                return null;
            }

            final int index = scrap.size() - 1;
            final View result = scrap.remove(index);

            return result;
        }

        // TODO: Implement support to maintain a list of active views so that we can make use of
        // stable ids to retrieve the same view that is currently laid out for a particular item.
        // Currently, all views "recycled" are shoved into the same collection, this may not be
        // the most effective way.  Refer to the RecycleBin as implemented for AbsListView.
        public View getView(int type, long stableId) {
            final ArrayList<View> scrap = mScrapViews[type];
            if (scrap.isEmpty()) {
                return null;
            }

            for (int i = 0; i < scrap.size(); i++) {
                final View v = scrap.get(i);
                final LayoutParams lp = (LayoutParams) v.getLayoutParams();
                if (lp.id == stableId) {
                    scrap.remove(i);
                    return v;
                }
            }

            return null;
        }
    }

    private class AdapterDataSetObserver extends DataSetObserver {
        @Override
        public void onChanged() {
            mDataChanged = true;

            mItemCount = mAdapter.getCount();
            mFirstChangedPosition = mAdapter.getFirstChangedPosition();
            if (mFirstPosition >= mItemCount) {
                // If the latest data set has fewer data items than mFirstPosition, we will not be
                // able to accurately restore scroll state, so just reset to the top.
                mFirstPosition = 0;
                mCurrentScrollState = null;
            }

            // TODO: Consider matching these back up if we have stable IDs.
            mRecycler.clearTransientViews();

            if (mHasStableIds) {
                // If we will animate the transition to the new layout, cache the current positions
                // of the visible children. This is before any views get removed below.
                cacheChildRects();
            } else {
                // Clear all layout records
                mLayoutRecords.clear();

                // Reset item bottoms to be equal to item tops
                final int colCount = mColCount;
                for (int i = 0; i < colCount; i++) {
                    mItemBottoms[i] = mItemTops[i];
                }
            }

            updateEmptyStatus();

            // TODO: consider repopulating in a deferred runnable instead
            // (so that successive changes may still be batched)
            requestLayout();
        }

        @Override
        public void onInvalidated() {
        }
    }

    static class SavedState extends BaseSavedState {
        long firstId = -1;
        int position;

        // topOffset is the vertical value that the view specified by position should
        // start rendering from.  If it is 0, the view would be at the top of the grid.
        int topOffset;

        SavedState(Parcelable superState) {
            super(superState);
        }

        private SavedState(Parcel in) {
            super(in);
            firstId = in.readLong();
            position = in.readInt();
            topOffset = in.readInt();
        }

        @Override
        public void writeToParcel(Parcel out, int flags) {
            super.writeToParcel(out, flags);
            out.writeLong(firstId);
            out.writeInt(position);
            out.writeInt(topOffset);
        }

        @Override
        public String toString() {
            return "StaggereGridView.SavedState{" + Integer.toHexString(System.identityHashCode(this)) + " firstId="
                    + firstId + " position=" + position + "}";
        }

        public static final Parcelable.Creator<SavedState> CREATOR = new Parcelable.Creator<SavedState>() {
            @Override
            public SavedState createFromParcel(Parcel in) {
                return new SavedState(in);
            }

            @Override
            public SavedState[] newArray(int size) {
                return new SavedState[size];
            }
        };
    }

    public void setDropListener(ReorderListener listener) {
        mReorderHelper = new ReorderHelper(listener, this);
    }

    public void setScrollListener(ScrollListener listener) {
        mScrollListener = listener;
    }

    public void setOnSizeChangedListener(OnSizeChangedListener listener) {
        mOnSizeChangedListener = listener;
    }

    /**
     * Helper class to store a {@link View} with its corresponding layout positions
     * as a {@link Rect}.
     */
    private static class ViewRectPair {
        public final View view;
        public final Rect rect;

        public ViewRectPair(View v, Rect r) {
            view = v;
            rect = r;
        }
    }

    public static class ScrollState implements Parcelable {
        private final long mItemId;
        private final int mAdapterPosition;

        // The offset that the view specified by mAdapterPosition should start rendering from.  If
        // this value is 0, then the view would be rendered from the very top of this grid.
        private int mVerticalOffset;

        public ScrollState(long itemId, int adapterPosition, int offset) {
            mItemId = itemId;
            mAdapterPosition = adapterPosition;
            mVerticalOffset = offset;
        }

        private ScrollState(Parcel in) {
            mItemId = in.readLong();
            mAdapterPosition = in.readInt();
            mVerticalOffset = in.readInt();
        }

        public long getItemId() {
            return mItemId;
        }

        public int getAdapterPosition() {
            return mAdapterPosition;
        }

        public void setVerticalOffset(int offset) {
            mVerticalOffset = offset;
        }

        public int getVerticalOffset() {
            return mVerticalOffset;
        }

        @Override
        public int describeContents() {
            return 0;
        }

        @Override
        public void writeToParcel(Parcel dest, int flags) {
            dest.writeLong(mItemId);
            dest.writeInt(mAdapterPosition);
            dest.writeInt(mVerticalOffset);
        }

        public static final Parcelable.Creator<ScrollState> CREATOR = new Parcelable.Creator<ScrollState>() {
            @Override
            public ScrollState createFromParcel(Parcel source) {
                return new ScrollState(source);
            }

            @Override
            public ScrollState[] newArray(int size) {
                return new ScrollState[size];
            }
        };

        @Override
        public String toString() {
            return "ScrollState {mItemId=" + mItemId + " mAdapterPosition=" + mAdapterPosition + " mVerticalOffset="
                    + mVerticalOffset + "}";
        }
    }

    /**
     * Listener of {@Link StaggeredGridView} for grid size change.
     */
    public interface OnSizeChangedListener {
        void onSizeChanged(int width, int height, int oldWidth, int oldHeight);
    }

    /**
     * Listener of {@Link StaggeredGridView} for scroll change.
     */
    public interface ScrollListener {

        /**
         * Called when scroll happens on this view.
         *
         * @param offset The scroll offset amount.
         * @param currentScrollY The current y position of this view.
         * @param maxScrollY The maximum amount of scroll possible in this view.
         */
        void onScrollChanged(int offset, int currentScrollY, int maxScrollY);
    }

    /**
     * Listener of {@link StaggeredGridView} for animations.  This listener is responsible
     * for playing all animations created by this {@link StaggeredGridView}
     */
    public interface AnimationListener {
        /**
         * Called when animations are ready to be played
         * @param animationMode The current animation mode based on the state of the data.  Valid
         * animation modes are {@link ANIMATION_MODE_NONE}, {@link ANIMATION_MODE_NEW_DATA}, and
         * {@link ANIMATION_MODE_UPDATE_DATA}.
         * @param animators The list of animators to be played
         */
        void onAnimationReady(int animationMode, List<Animator> animators);
    }

    /**
     * Listener of {@link StaggeredGridView} for drag and drop reordering of child views.
     */
    public interface ReorderListener {

        /**
         * onPickedUp is called to notify listeners that an item has been picked up for reordering.
         * @param draggedChild the original child view that picked up.
         */
        void onPickedUp(View draggedChild);

        /**
         * onDrop is called to notify listeners that an intent to drop the
         * item at position "from" over the position "target"
         * @param draggedView the original child view that was dropped
         * @param sourcePosition the original position where the item was dragged from
         * @param targetPosition the target position where the item is dropped at
         */
        void onDrop(View draggedView, int sourcePosition, int targetPosition);

        /**
         * onCancelDrag is called to notify listeners that the drag event has been cancelled.
         * @param draggediew the original child view that was dragged.
         */
        void onCancelDrag(View draggediew);

        /**
         * onReorder is called to notify listeners that an intent to move the
         * item at position "from" to position "to"
         * @param draggedView the original child view that was dragged
         * @param id id of the original item that was picked up
         * @param from
         * @param to the target position where the item is dropped at
         */
        boolean onReorder(View draggedView, long id, int from, int to);

        /**
         * Event handler for a drag entering the {@link StaggeredGridView} element's
         * reordering area.
         * @param view The child view that just received an enter event on the reordering area.
         * @param position The adapter position of the view that just received an enter event.
         */
        void onEnterReorderArea(View view, int position);
    }
}