blob: aa44df4d7cd700e9031f73b5aaae8abf148742f7 [file] [log] [blame]
/*
* 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.android.deskclock.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 com.android.deskclock.widget.sgv.SgvAnimationHelper.AnimationIn;
import com.android.deskclock.widget.sgv.SgvAnimationHelper.AnimationOut;
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 AnimationIn mAnimationInMode = AnimationIn.NONE;
private AnimationOut mAnimationOutMode = 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 = AnimationIn.NONE;
mAnimationOutMode = 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, AnimationIn animationInMode,
int startDelay) {
if (animationInMode == AnimationIn.NONE) {
return;
}
switch (animationInMode) {
case FLY_UP_ALL_VIEWS:
addFlyInAllViewsAnimators(animators);
break;
case EXPAND_NEW_VIEWS:
addUpdateViewPositionsAnimators(animators, true /* cascade animation */,
AnimationIn.EXPAND_NEW_VIEWS, startDelay);
break;
case EXPAND_NEW_VIEWS_NO_CASCADE:
addUpdateViewPositionsAnimators(animators, false /* cascade animation */,
AnimationIn.EXPAND_NEW_VIEWS_NO_CASCADE, startDelay);
break;
case SLIDE_IN_NEW_VIEWS:
addUpdateViewPositionsAnimators(animators, true /* cascade animation */,
AnimationIn.SLIDE_IN_NEW_VIEWS, startDelay);
break;
case FLY_IN_NEW_VIEWS:
addUpdateViewPositionsAnimators(animators, true /* cascade animation */,
AnimationIn.FLY_IN_NEW_VIEWS, startDelay);
break;
case FADE:
addUpdateViewPositionsAnimators(animators, true /* cascade animation */,
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,
AnimationOut animationOutMode) {
if (animationOutMode == 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, 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 == 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 == 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 != 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(AnimationIn animationIn, AnimationOut animationOut) {
mAnimationInMode = animationIn;
mAnimationOutMode = animationOut;
}
public AnimationIn getAnimationInMode() {
return mAnimationInMode;
}
public 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);
}
}