Gradle dependencies
compile group: 'androidx.recyclerview', name: 'recyclerview', version: '1.4.0-beta01'
- groupId: androidx.recyclerview
- artifactId: recyclerview
- version: 1.4.0-beta01
Artifact androidx.recyclerview:recyclerview:1.4.0-beta01 it located at Google repository (https://maven.google.com/)
Androidx artifact mapping:
androidx.recyclerview:recyclerview com.android.support:recyclerview-v7
Androidx class mapping:
androidx.recyclerview.widget.StaggeredGridLayoutManager android.support.v7.widget.StaggeredGridLayoutManager
Overview
A LayoutManager that lays out children in a staggered grid formation.
It supports horizontal & vertical layout as well as an ability to layout children in reverse.
Staggered grids are likely to have gaps at the edges of the layout. To avoid these gaps,
StaggeredGridLayoutManager can offset spans independently or move items between spans. You can
control this behavior via StaggeredGridLayoutManager.setGapStrategy(int).
Summary
Constructors |
---|
public | StaggeredGridLayoutManager(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes)
Constructor used when layout manager is set in XML by RecyclerView attribute
"layoutManager". |
public | StaggeredGridLayoutManager(int spanCount, int orientation)
Creates a StaggeredGridLayoutManager with given parameters. |
Methods |
---|
public void | assertNotInLayoutOrScroll(java.lang.String message)
Checks if RecyclerView is in the middle of a layout or scroll and throws an
java.lang.IllegalStateException if it is. |
public boolean | canScrollHorizontally()
Query if horizontal scrolling is currently supported. |
public boolean | canScrollVertically()
Query if vertical scrolling is currently supported. |
public boolean | checkLayoutParams(RecyclerView.LayoutParams lp)
Determines the validity of the supplied LayoutParams object. |
public void | collectAdjacentPrefetchPositions(int dx, int dy, RecyclerView.State state, RecyclerView.LayoutManager.LayoutPrefetchRegistry layoutPrefetchRegistry)
Gather all positions from the LayoutManager to be prefetched, given specified momentum. |
public int | computeHorizontalScrollExtent(RecyclerView.State state)
Override this method if you want to support scroll bars. |
public int | computeHorizontalScrollOffset(RecyclerView.State state)
Override this method if you want to support scroll bars. |
public int | computeHorizontalScrollRange(RecyclerView.State state)
Override this method if you want to support scroll bars. |
public PointF | computeScrollVectorForPosition(int targetPosition)
|
public int | computeVerticalScrollExtent(RecyclerView.State state)
Override this method if you want to support scroll bars. |
public int | computeVerticalScrollOffset(RecyclerView.State state)
Override this method if you want to support scroll bars. |
public int | computeVerticalScrollRange(RecyclerView.State state)
Override this method if you want to support scroll bars. |
public int[] | findFirstCompletelyVisibleItemPositions(int[] into[])
Returns the adapter position of the first completely visible view for each span. |
public int[] | findFirstVisibleItemPositions(int[] into[])
Returns the adapter position of the first visible view for each span. |
public int[] | findLastCompletelyVisibleItemPositions(int[] into[])
Returns the adapter position of the last completely visible view for each span. |
public int[] | findLastVisibleItemPositions(int[] into[])
Returns the adapter position of the last visible view for each span. |
public abstract RecyclerView.LayoutParams | generateDefaultLayoutParams()
Create a default LayoutParams object for a child of the RecyclerView. |
public RecyclerView.LayoutParams | generateLayoutParams(Context c, AttributeSet attrs)
Create a LayoutParams object suitable for this LayoutManager from
an inflated layout resource. |
public RecyclerView.LayoutParams | generateLayoutParams(ViewGroup.LayoutParams lp)
Create a LayoutParams object suitable for this LayoutManager, copying relevant
values from the supplied LayoutParams object if possible. |
public int | getColumnCountForAccessibility(RecyclerView.Recycler recycler, RecyclerView.State state)
Returns the number of columns for accessibility. |
public int | getGapStrategy()
Returns the current gap handling strategy for StaggeredGridLayoutManager. |
public int | getOrientation()
|
public boolean | getReverseLayout()
Returns whether views are laid out in reverse order or not. |
public int | getRowCountForAccessibility(RecyclerView.Recycler recycler, RecyclerView.State state)
Returns the number of rows for accessibility. |
public int | getSpanCount()
Returns the number of spans laid out by StaggeredGridLayoutManager. |
public void | invalidateSpanAssignments()
For consistency, StaggeredGridLayoutManager keeps a mapping between spans and items. |
public boolean | isAutoMeasureEnabled()
Returns whether the measuring pass of layout should use the AutoMeasure mechanism of
RecyclerView or if it should be done by the LayoutManager's implementation of
RecyclerView.LayoutManager. |
public boolean | isLayoutReversed()
Query if the layout is in reverse order. |
public void | offsetChildrenHorizontal(int dx)
Offset all child views attached to the parent RecyclerView by dx pixels along
the horizontal axis. |
public void | offsetChildrenVertical(int dy)
Offset all child views attached to the parent RecyclerView by dy pixels along
the vertical axis. |
public void | onAdapterChanged(RecyclerView.Adapter oldAdapter, RecyclerView.Adapter newAdapter)
Called if the RecyclerView this LayoutManager is bound to has a different adapter set via
RecyclerView.setAdapter(RecyclerView.Adapter) or
RecyclerView.swapAdapter(RecyclerView.Adapter, boolean). |
public void | onDetachedFromWindow(RecyclerView view, RecyclerView.Recycler recycler)
Called when this LayoutManager is detached from its parent RecyclerView or when
its parent RecyclerView is detached from its window. |
public View | onFocusSearchFailed(View focused, int direction, RecyclerView.Recycler recycler, RecyclerView.State state)
Called when searching for a focusable view in the given direction has failed
for the current content of the RecyclerView. |
public void | onInitializeAccessibilityEvent(AccessibilityEvent event)
|
public void | onInitializeAccessibilityNodeInfo(RecyclerView.Recycler recycler, RecyclerView.State state, AccessibilityNodeInfoCompat info)
Called by the AccessibilityDelegate when the information about the current layout should
be populated. |
public void | onInitializeAccessibilityNodeInfoForItem(RecyclerView.Recycler recycler, RecyclerView.State state, View host, AccessibilityNodeInfoCompat info)
Called by the AccessibilityDelegate when the accessibility information for a specific
item should be populated. |
public void | onItemsAdded(RecyclerView recyclerView, int positionStart, int itemCount)
Called when items have been added to the adapter. |
public void | onItemsChanged(RecyclerView recyclerView)
Called in response to a call to RecyclerView.Adapter.notifyDataSetChanged() or
RecyclerView.swapAdapter(RecyclerView.Adapter, boolean) ()} and signals that the the entire
data set has changed. |
public void | onItemsMoved(RecyclerView recyclerView, int from, int to, int itemCount)
Called when an item is moved withing the adapter. |
public void | onItemsRemoved(RecyclerView recyclerView, int positionStart, int itemCount)
Called when items have been removed from the adapter. |
public void | onItemsUpdated(RecyclerView recyclerView, int positionStart, int itemCount, java.lang.Object payload)
Called when items have been changed in the adapter and with optional payload. |
public void | onLayoutChildren(RecyclerView.Recycler recycler, RecyclerView.State state)
Lay out all relevant child views from the given adapter. |
public void | onLayoutCompleted(RecyclerView.State state)
Called after a full layout calculation is finished. |
public void | onRestoreInstanceState(Parcelable state)
Called when the RecyclerView is ready to restore the state based on a previous
RecyclerView. |
public Parcelable | onSaveInstanceState()
Called when the LayoutManager should save its state. |
public void | onScrollStateChanged(int state)
RecyclerView calls this method to notify LayoutManager that scroll state has changed. |
public int | scrollHorizontallyBy(int dx, RecyclerView.Recycler recycler, RecyclerView.State state)
Scroll horizontally by dx pixels in screen coordinates and return the distance traveled. |
public void | scrollToPosition(int position)
Scroll to the specified adapter position. |
public void | scrollToPositionWithOffset(int position, int offset)
Scroll to the specified adapter position with the given offset from layout start. |
public int | scrollVerticallyBy(int dy, RecyclerView.Recycler recycler, RecyclerView.State state)
Scroll vertically by dy pixels in screen coordinates and return the distance traveled. |
public void | setGapStrategy(int gapStrategy)
Sets the gap handling strategy for StaggeredGridLayoutManager. |
public void | setMeasuredDimension(Rect childrenBounds, int wSpec, int hSpec)
Sets the measured dimensions from the given bounding box of the children and the
measurement specs that were passed into RecyclerView.onMeasure(int, int). |
public void | setOrientation(int orientation)
Sets the orientation of the layout. |
public void | setReverseLayout(boolean reverseLayout)
Sets whether LayoutManager should start laying out items from the end of the UI. |
public void | setSpanCount(int spanCount)
Sets the number of spans for the layout. |
public void | smoothScrollToPosition(RecyclerView recyclerView, RecyclerView.State state, int position)
Smooth scroll to the specified adapter position. |
public boolean | supportsPredictiveItemAnimations()
Returns whether this LayoutManager supports "predictive item animations". |
from RecyclerView.LayoutManager | addDisappearingView, addDisappearingView, addView, addView, assertInLayoutOrScroll, attachView, attachView, attachView, calculateItemDecorationsForChild, chooseSize, collectInitialPrefetchPositions, detachAndScrapAttachedViews, detachAndScrapView, detachAndScrapViewAt, detachView, detachViewAt, endAnimation, findContainingItemView, findViewByPosition, getBaseline, getBottomDecorationHeight, getChildAt, getChildCount, getChildMeasureSpec, getChildMeasureSpec, getClipToPadding, getDecoratedBottom, getDecoratedBoundsWithMargins, getDecoratedLeft, getDecoratedMeasuredHeight, getDecoratedMeasuredWidth, getDecoratedRight, getDecoratedTop, getFocusedChild, getHeight, getHeightMode, getItemCount, getItemViewType, getLayoutDirection, getLeftDecorationWidth, getMinimumHeight, getMinimumWidth, getPaddingBottom, getPaddingEnd, getPaddingLeft, getPaddingRight, getPaddingStart, getPaddingTop, getPosition, getProperties, getRightDecorationWidth, getSelectionModeForAccessibility, getTopDecorationHeight, getTransformedBoundingBox, getWidth, getWidthMode, hasFocus, ignoreView, isAttachedToWindow, isFocused, isItemPrefetchEnabled, isLayoutHierarchical, isMeasurementCacheEnabled, isSmoothScrolling, isViewPartiallyVisible, layoutDecorated, layoutDecoratedWithMargins, measureChild, measureChildWithMargins, moveView, onAddFocusables, onAttachedToWindow, onDetachedFromWindow, onInitializeAccessibilityEvent, onInterceptFocusSearch, onItemsUpdated, onMeasure, onRequestChildFocus, onRequestChildFocus, performAccessibilityAction, performAccessibilityActionForItem, postOnAnimation, removeAllViews, removeAndRecycleAllViews, removeAndRecycleView, removeAndRecycleViewAt, removeCallbacks, removeDetachedView, removeView, removeViewAt, requestChildRectangleOnScreen, requestChildRectangleOnScreen, requestLayout, requestSimpleAnimationsInNextLayout, setAutoMeasureEnabled, setItemPrefetchEnabled, setMeasuredDimension, setMeasurementCacheEnabled, startSmoothScroll, stopIgnoringView |
from java.lang.Object | clone, equals, finalize, getClass, hashCode, notify, notifyAll, toString, wait, wait, wait |
Fields
public static final int
HORIZONTALpublic static final int
VERTICALpublic static final int
GAP_HANDLING_NONEDoes not do anything to hide gaps.
public static final int
GAP_HANDLING_LAZYDeprecated: No longer supported.
public static final int
GAP_HANDLING_MOVE_ITEMS_BETWEEN_SPANSWhen scroll state is changed to RecyclerView.SCROLL_STATE_IDLE, StaggeredGrid will
check if there are gaps in the because of full span items. If it finds, it will re-layout
and move items to correct positions with animations.
For example, if LayoutManager ends up with the following layout due to adapter changes:
AAA
_BC
DDD
It will animate to the following state:
AAA
BC_
DDD
Constructors
public
StaggeredGridLayoutManager(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes)
Constructor used when layout manager is set in XML by RecyclerView attribute
"layoutManager". Defaults to single column and vertical.
public
StaggeredGridLayoutManager(int spanCount, int orientation)
Creates a StaggeredGridLayoutManager with given parameters.
Parameters:
spanCount: If orientation is vertical, spanCount is number of columns. If
orientation is horizontal, spanCount is number of rows.
orientation: StaggeredGridLayoutManager.VERTICAL or StaggeredGridLayoutManager.HORIZONTAL
Methods
public boolean
isAutoMeasureEnabled()
Returns whether the measuring pass of layout should use the AutoMeasure mechanism of
RecyclerView or if it should be done by the LayoutManager's implementation of
RecyclerView.LayoutManager.
This method returns false by default (it actually returns the value passed to the
deprecated RecyclerView.LayoutManager.setAutoMeasureEnabled(boolean)) and should be overridden to return
true if a LayoutManager wants to be auto measured by the RecyclerView.
If this method is overridden to return true,
RecyclerView.LayoutManager should not be overridden.
AutoMeasure is a RecyclerView mechanism that handles the measuring pass of layout in a
simple and contract satisfying way, including the wrapping of children laid out by
LayoutManager. Simply put, it handles wrapping children by calling
RecyclerView.LayoutManager during a call to
RecyclerView.onMeasure(int, int), and then calculating desired dimensions based
on children's dimensions and positions. It does this while supporting all existing
animation capabilities of the RecyclerView.
More specifically:
- When RecyclerView.onMeasure(int, int) is called, if the provided measure
specs both have a mode of , RecyclerView will set its
measured dimensions accordingly and return, allowing layout to continue as normal
(Actually, RecyclerView will call
RecyclerView.LayoutManager for backwards compatibility
reasons but it should not be overridden if AutoMeasure is being used).
- If one of the layout specs is not EXACT, the RecyclerView will start the
layout process. It will first process all pending Adapter updates and
then decide whether to run a predictive layout. If it decides to do so, it will first
call RecyclerView.LayoutManager with RecyclerView.State.isPreLayout() set to
true. At this stage, RecyclerView.LayoutManager.getWidth() and RecyclerView.LayoutManager.getHeight() will still
return the width and height of the RecyclerView as of the last layout calculation.
After handling the predictive case, RecyclerView will call
RecyclerView.LayoutManager with RecyclerView.State.isMeasuring() set to
true and RecyclerView.State.isPreLayout() set to false. The LayoutManager can
access the measurement specs via RecyclerView.LayoutManager.getHeight(), RecyclerView.LayoutManager.getHeightMode(),
RecyclerView.LayoutManager.getWidth() and RecyclerView.LayoutManager.getWidthMode().
- After the layout calculation, RecyclerView sets the measured width & height by
calculating the bounding box for the children (+ RecyclerView's padding). The
LayoutManagers can override RecyclerView.LayoutManager.setMeasuredDimension(Rect, int, int) to choose
different values. For instance, GridLayoutManager overrides this value to handle the case
where if it is vertical and has 3 columns but only 2 items, it should still measure its
width to fit 3 items, not 2.
- Any following calls to RecyclerView.onMeasure(int, int) will run
RecyclerView.LayoutManager with RecyclerView.State.isMeasuring() set to
true and RecyclerView.State.isPreLayout() set to false. RecyclerView will
take care of which views are actually added / removed / moved / changed for animations so
that the LayoutManager should not worry about them and handle each
RecyclerView.LayoutManager call as if it is the last one.
- When measure is complete and RecyclerView's
RecyclerView.onLayout(boolean, int, int, int, int) method is called, RecyclerView checks
whether it already did layout calculations during the measure pass and if so, it re-uses
that information. It may still decide to call RecyclerView.LayoutManager
if the last measure spec was different from the final dimensions or adapter contents
have changed between the measure call and the layout call.
- Finally, animations are calculated and run as usual.
Returns:
True
if the measuring pass of layout should use the AutoMeasure
mechanism of RecyclerView or False
if it should be done by the
LayoutManager's implementation of
RecyclerView.LayoutManager.
See also: RecyclerView.LayoutManager.setMeasuredDimension(Rect, int, int), RecyclerView.LayoutManager
public void
onScrollStateChanged(int state)
RecyclerView calls this method to notify LayoutManager that scroll state has changed.
Parameters:
state: The new scroll state for RecyclerView
Called when this LayoutManager is detached from its parent RecyclerView or when
its parent RecyclerView is detached from its window.
LayoutManager should clear all of its View references as another LayoutManager might be
assigned to the RecyclerView.
If the RecyclerView is re-attached with the same LayoutManager and Adapter, it may not
call RecyclerView.LayoutManager if nothing has changed and a layout was
not requested on the RecyclerView while it was detached.
If your LayoutManager has View references that it cleans in on-detach, it should also
call RecyclerView.requestLayout() to ensure that it is re-laid out when
RecyclerView is re-attached.
Subclass implementations should always call through to the superclass implementation.
Parameters:
view: The RecyclerView this LayoutManager is bound to
recycler: The recycler to use if you prefer to recycle your children instead of
keeping them around.
See also: RecyclerView.LayoutManager.onAttachedToWindow(RecyclerView)
public void
setSpanCount(int spanCount)
Sets the number of spans for the layout. This will invalidate all of the span assignments
for Views.
Calling this method will automatically result in a new layout request unless the spanCount
parameter is equal to current span count.
Parameters:
spanCount: Number of spans to layout
public void
setOrientation(int orientation)
Sets the orientation of the layout. StaggeredGridLayoutManager will do its best to keep
scroll position if this method is called after views are laid out.
Parameters:
orientation: StaggeredGridLayoutManager.HORIZONTAL or StaggeredGridLayoutManager.VERTICAL
public void
setReverseLayout(boolean reverseLayout)
Sets whether LayoutManager should start laying out items from the end of the UI. The order
items are traversed is not affected by this call.
For vertical layout, if it is set to true
, first item will be at the bottom of
the list.
For horizontal layouts, it depends on the layout direction.
When set to true, If RecyclerView is LTR, than it will layout from RTL, if
RecyclerView} is RTL, it will layout from LTR.
Parameters:
reverseLayout: Whether layout should be in reverse or not
public int
getGapStrategy()
Returns the current gap handling strategy for StaggeredGridLayoutManager.
Staggered grid may have gaps in the layout due to changes in the adapter. To avoid gaps,
StaggeredGridLayoutManager provides 2 options. Check StaggeredGridLayoutManager.GAP_HANDLING_NONE and
StaggeredGridLayoutManager.GAP_HANDLING_MOVE_ITEMS_BETWEEN_SPANS for details.
By default, StaggeredGridLayoutManager uses StaggeredGridLayoutManager.GAP_HANDLING_MOVE_ITEMS_BETWEEN_SPANS.
Returns:
Current gap handling strategy.
See also: StaggeredGridLayoutManager.setGapStrategy(int), StaggeredGridLayoutManager.GAP_HANDLING_NONE, StaggeredGridLayoutManager.GAP_HANDLING_MOVE_ITEMS_BETWEEN_SPANS
public void
setGapStrategy(int gapStrategy)
Sets the gap handling strategy for StaggeredGridLayoutManager. If the gapStrategy parameter
is different than the current strategy, calling this method will trigger a layout request.
Parameters:
gapStrategy: The new gap handling strategy. Should be
StaggeredGridLayoutManager.GAP_HANDLING_MOVE_ITEMS_BETWEEN_SPANS or StaggeredGridLayoutManager.GAP_HANDLING_NONE.
See also: StaggeredGridLayoutManager.getGapStrategy()
public void
assertNotInLayoutOrScroll(java.lang.String message)
Checks if RecyclerView is in the middle of a layout or scroll and throws an
java.lang.IllegalStateException
if it is.
Parameters:
message: The message for the exception. Can be null.
See also: RecyclerView.LayoutManager.assertInLayoutOrScroll(String)
public int
getSpanCount()
Returns the number of spans laid out by StaggeredGridLayoutManager.
Returns:
Number of spans in the layout
public void
invalidateSpanAssignments()
For consistency, StaggeredGridLayoutManager keeps a mapping between spans and items.
If you need to cancel current assignments, you can call this method which will clear all
assignments and request a new layout.
public boolean
getReverseLayout()
Returns whether views are laid out in reverse order or not.
Not that this value is not affected by RecyclerView's layout direction.
Returns:
True if layout is reversed, false otherwise
See also: StaggeredGridLayoutManager.setReverseLayout(boolean)
public void
setMeasuredDimension(Rect childrenBounds, int wSpec, int hSpec)
Sets the measured dimensions from the given bounding box of the children and the
measurement specs that were passed into RecyclerView.onMeasure(int, int). It is
only called if a LayoutManager returns true
from
RecyclerView.LayoutManager.isAutoMeasureEnabled() and it is called after the RecyclerView calls
RecyclerView.LayoutManager in the execution of
RecyclerView.onMeasure(int, int).
This method must call RecyclerView.LayoutManager.setMeasuredDimension(int, int).
The default implementation adds the RecyclerView's padding to the given bounding box
then caps the value to be within the given measurement specs.
Parameters:
childrenBounds: The bounding box of all children
wSpec: The widthMeasureSpec that was passed into the RecyclerView.
hSpec: The heightMeasureSpec that was passed into the RecyclerView.
See also: RecyclerView.LayoutManager.isAutoMeasureEnabled(), RecyclerView.LayoutManager.setMeasuredDimension(int, int)
Lay out all relevant child views from the given adapter.
The LayoutManager is in charge of the behavior of item animations. By default,
RecyclerView has a non-null ItemAnimator, and simple
item animations are enabled. This means that add/remove operations on the
adapter will result in animations to add new or appearing items, removed or
disappearing items, and moved items. If a LayoutManager returns false from
RecyclerView.LayoutManager.supportsPredictiveItemAnimations(), which is the default, and runs a
normal layout operation during RecyclerView.LayoutManager, the
RecyclerView will have enough information to run those animations in a simple
way. For example, the default ItemAnimator, DefaultItemAnimator, will
simply fade views in and out, whether they are actually added/removed or whether
they are moved on or off the screen due to other add/remove operations.
A LayoutManager wanting a better item animation experience, where items can be
animated onto and off of the screen according to where the items exist when they
are not on screen, then the LayoutManager should return true from
RecyclerView.LayoutManager.supportsPredictiveItemAnimations() and add additional logic to
RecyclerView.LayoutManager. Supporting predictive animations
means that RecyclerView.LayoutManager will be called twice;
once as a "pre" layout step to determine where items would have been prior to
a real layout, and again to do the "real" layout. In the pre-layout phase,
items will remember their pre-layout positions to allow them to be laid out
appropriately. Also, removed items will
be returned from the scrap to help determine correct placement of other items.
These removed items should not be added to the child list, but should be used
to help calculate correct positioning of other views, including views that
were not previously onscreen (referred to as APPEARING views), but whose
pre-layout offscreen position can be determined given the extra
information about the pre-layout removed views.
The second layout pass is the real layout in which only non-removed views
will be used. The only additional requirement during this pass is, if
RecyclerView.LayoutManager.supportsPredictiveItemAnimations() returns true, to note which
views exist in the child list prior to layout and which are not there after
layout (referred to as DISAPPEARING views), and to position/layout those views
appropriately, without regard to the actual bounds of the RecyclerView. This allows
the animation system to know the location to which to animate these disappearing
views.
The default LayoutManager implementations for RecyclerView handle all of these
requirements for animations already. Clients of RecyclerView can either use one
of these layout managers directly or look at their implementations of
onLayoutChildren() to see how they account for the APPEARING and
DISAPPEARING views.
Parameters:
recycler: Recycler to use for fetching potentially cached views for a
position
state: Transient state of RecyclerView
Called if the RecyclerView this LayoutManager is bound to has a different adapter set via
RecyclerView.setAdapter(RecyclerView.Adapter) or
RecyclerView.swapAdapter(RecyclerView.Adapter, boolean). The LayoutManager may use this
opportunity to clear caches and configure state such that it can relayout appropriately
with the new data and potentially new view types.
The default implementation removes all currently attached views.
Parameters:
oldAdapter: The previous adapter instance. Will be null if there was previously no
adapter.
newAdapter: The new adapter instance. Might be null if
RecyclerView.setAdapter(RecyclerView.Adapter) is called with
null.
Called after a full layout calculation is finished. The layout calculation may include
multiple RecyclerView.LayoutManager calls due to animations or
layout measurement but it will include only one RecyclerView.LayoutManager call.
This method will be called at the end of View
call.
This is a good place for the LayoutManager to do some cleanup like pending scroll
position, saved state etc.
Parameters:
state: Transient state of RecyclerView
public boolean
supportsPredictiveItemAnimations()
Returns whether this LayoutManager supports "predictive item animations".
"Predictive item animations" are automatically created animations that show
where items came from, and where they are going to, as items are added, removed,
or moved within a layout.
A LayoutManager wishing to support predictive item animations must override this
method to return true (the default implementation returns false) and must obey certain
behavioral contracts outlined in RecyclerView.LayoutManager.
Whether item animations actually occur in a RecyclerView is actually determined by both
the return value from this method and the
ItemAnimator set on the
RecyclerView itself. If the RecyclerView has a non-null ItemAnimator but this
method returns false, then only "simple item animations" will be enabled in the
RecyclerView, in which views whose position are changing are simply faded in/out. If the
RecyclerView has a non-null ItemAnimator and this method returns true, then predictive
item animations will be enabled in the RecyclerView.
Returns:
true if this LayoutManager supports predictive item animations, false otherwise.
public int[]
findFirstVisibleItemPositions(int[] into[])
Returns the adapter position of the first visible view for each span.
Note that, this value is not affected by layout orientation or item order traversal.
(StaggeredGridLayoutManager.setReverseLayout(boolean)). Views are sorted by their positions in the adapter,
not in the layout.
If RecyclerView has item decorators, they will be considered in calculations as well.
StaggeredGridLayoutManager may pre-cache some views that are not necessarily visible. Those
views are ignored in this method.
Parameters:
into: An array to put the results into. If you don't provide any, LayoutManager will
create a new one.
Returns:
The adapter position of the first visible item in each span. If a span does not have
any items, RecyclerView.NO_POSITION is returned for that span.
See also: StaggeredGridLayoutManager.findFirstCompletelyVisibleItemPositions(int[]), StaggeredGridLayoutManager.findLastVisibleItemPositions(int[])
public int[]
findFirstCompletelyVisibleItemPositions(int[] into[])
Returns the adapter position of the first completely visible view for each span.
Note that, this value is not affected by layout orientation or item order traversal.
(StaggeredGridLayoutManager.setReverseLayout(boolean)). Views are sorted by their positions in the adapter,
not in the layout.
If RecyclerView has item decorators, they will be considered in calculations as well.
StaggeredGridLayoutManager may pre-cache some views that are not necessarily visible. Those
views are ignored in this method.
Parameters:
into: An array to put the results into. If you don't provide any, LayoutManager will
create a new one.
Returns:
The adapter position of the first fully visible item in each span. If a span does
not have any items, RecyclerView.NO_POSITION is returned for that span.
See also: StaggeredGridLayoutManager.findFirstVisibleItemPositions(int[]), StaggeredGridLayoutManager.findLastCompletelyVisibleItemPositions(int[])
public int[]
findLastVisibleItemPositions(int[] into[])
Returns the adapter position of the last visible view for each span.
Note that, this value is not affected by layout orientation or item order traversal.
(StaggeredGridLayoutManager.setReverseLayout(boolean)). Views are sorted by their positions in the adapter,
not in the layout.
If RecyclerView has item decorators, they will be considered in calculations as well.
StaggeredGridLayoutManager may pre-cache some views that are not necessarily visible. Those
views are ignored in this method.
Parameters:
into: An array to put the results into. If you don't provide any, LayoutManager will
create a new one.
Returns:
The adapter position of the last visible item in each span. If a span does not have
any items, RecyclerView.NO_POSITION is returned for that span.
See also: StaggeredGridLayoutManager.findLastCompletelyVisibleItemPositions(int[]), StaggeredGridLayoutManager.findFirstVisibleItemPositions(int[])
public int[]
findLastCompletelyVisibleItemPositions(int[] into[])
Returns the adapter position of the last completely visible view for each span.
Note that, this value is not affected by layout orientation or item order traversal.
(StaggeredGridLayoutManager.setReverseLayout(boolean)). Views are sorted by their positions in the adapter,
not in the layout.
If RecyclerView has item decorators, they will be considered in calculations as well.
StaggeredGridLayoutManager may pre-cache some views that are not necessarily visible. Those
views are ignored in this method.
Parameters:
into: An array to put the results into. If you don't provide any, LayoutManager will
create a new one.
Returns:
The adapter position of the last fully visible item in each span. If a span does not
have any items, RecyclerView.NO_POSITION is returned for that span.
See also: StaggeredGridLayoutManager.findFirstCompletelyVisibleItemPositions(int[]), StaggeredGridLayoutManager.findLastVisibleItemPositions(int[])
Override this method if you want to support scroll bars.
Read RecyclerView.computeHorizontalScrollOffset() for details.
Default implementation returns 0.
Parameters:
state: Current State of RecyclerView where you can find total item count
Returns:
The horizontal offset of the scrollbar's thumb
See also: RecyclerView.computeHorizontalScrollOffset()
Override this method if you want to support scroll bars.
Read RecyclerView.computeVerticalScrollOffset() for details.
Default implementation returns 0.
Parameters:
state: Current State of RecyclerView where you can find total item count
Returns:
The vertical offset of the scrollbar's thumb
See also: RecyclerView.computeVerticalScrollOffset()
Override this method if you want to support scroll bars.
Read RecyclerView.computeHorizontalScrollExtent() for details.
Default implementation returns 0.
Parameters:
state: Current state of RecyclerView
Returns:
The horizontal extent of the scrollbar's thumb
See also: RecyclerView.computeHorizontalScrollExtent()
Override this method if you want to support scroll bars.
Read RecyclerView.computeVerticalScrollExtent() for details.
Default implementation returns 0.
Parameters:
state: Current state of RecyclerView
Returns:
The vertical extent of the scrollbar's thumb
See also: RecyclerView.computeVerticalScrollExtent()
Override this method if you want to support scroll bars.
Read RecyclerView.computeHorizontalScrollRange() for details.
Default implementation returns 0.
Parameters:
state: Current State of RecyclerView where you can find total item count
Returns:
The total horizontal range represented by the horizontal scrollbar
See also: RecyclerView.computeHorizontalScrollRange()
Override this method if you want to support scroll bars.
Read RecyclerView.computeVerticalScrollRange() for details.
Default implementation returns 0.
Parameters:
state: Current State of RecyclerView where you can find total item count
Returns:
The total vertical range represented by the vertical scrollbar
See also: RecyclerView.computeVerticalScrollRange()
public void
onRestoreInstanceState(Parcelable state)
Called when the RecyclerView is ready to restore the state based on a previous
RecyclerView.
Notice that this might happen after an actual layout, based on how Adapter prefers to
restore State. See RecyclerView.Adapter.getStateRestorationPolicy() for more information.
Parameters:
state: The parcelable that was returned by the previous LayoutManager's
RecyclerView.LayoutManager.onSaveInstanceState() method.
public Parcelable
onSaveInstanceState()
Called when the LayoutManager should save its state. This is a good time to save your
scroll position, configuration and anything else that may be required to restore the same
layout state if the LayoutManager is recreated.
RecyclerView does NOT verify if the LayoutManager has changed between state save and
restore. This will let you share information between your LayoutManagers but it is also
your responsibility to make sure they use the same parcelable class.
Returns:
Necessary information for LayoutManager to be able to restore its state
Called by the AccessibilityDelegate when the information about the current layout should
be populated.
Default implementation adds a AccessibilityNodeInfoCompat.CollectionInfoCompat.
You should override
RecyclerView.LayoutManager.getRowCountForAccessibility(RecyclerView.Recycler, RecyclerView.State),
RecyclerView.LayoutManager.getColumnCountForAccessibility(RecyclerView.Recycler, RecyclerView.State),
RecyclerView.LayoutManager.isLayoutHierarchical(RecyclerView.Recycler, RecyclerView.State) and
RecyclerView.LayoutManager.getSelectionModeForAccessibility(RecyclerView.Recycler, RecyclerView.State) for
more accurate accessibility information.
Parameters:
recycler: The Recycler that can be used to convert view positions into adapter
positions
state: The current state of RecyclerView
info: The info that should be filled by the LayoutManager
See also: View
, RecyclerView.LayoutManager.getRowCountForAccessibility(RecyclerView.Recycler, RecyclerView.State), RecyclerView.LayoutManager.getColumnCountForAccessibility(RecyclerView.Recycler, RecyclerView.State), RecyclerView.LayoutManager.isLayoutHierarchical(RecyclerView.Recycler, RecyclerView.State), RecyclerView.LayoutManager.getSelectionModeForAccessibility(RecyclerView.Recycler, RecyclerView.State)
Called by the AccessibilityDelegate when the accessibility information for a specific
item should be populated.
Default implementation adds basic positioning information about the item.
Parameters:
recycler: The Recycler that can be used to convert view positions into adapter
positions
state: The current state of RecyclerView
host: The child for which accessibility node info should be populated
info: The info to fill out about the item
See also: android.widget.AbsListView
public void
onInitializeAccessibilityEvent(AccessibilityEvent event)
Returns the number of rows for accessibility.
Default implementation returns the number of items in the adapter if LayoutManager
supports vertical scrolling or 1 if LayoutManager does not support vertical
scrolling.
Parameters:
recycler: The Recycler that can be used to convert view positions into adapter
positions
state: The current state of RecyclerView
Returns:
The number of rows in LayoutManager for accessibility.
Returns the number of columns for accessibility.
Default implementation returns the number of items in the adapter if LayoutManager
supports horizontal scrolling or 1 if LayoutManager does not support horizontal
scrolling.
Parameters:
recycler: The Recycler that can be used to convert view positions into adapter
positions
state: The current state of RecyclerView
Returns:
The number of rows in LayoutManager for accessibility.
public void
offsetChildrenHorizontal(int dx)
Offset all child views attached to the parent RecyclerView by dx pixels along
the horizontal axis.
Parameters:
dx: Pixels to offset by
public void
offsetChildrenVertical(int dy)
Offset all child views attached to the parent RecyclerView by dy pixels along
the vertical axis.
Parameters:
dy: Pixels to offset by
public void
onItemsRemoved(
RecyclerView recyclerView, int positionStart, int itemCount)
Called when items have been removed from the adapter.
public void
onItemsAdded(
RecyclerView recyclerView, int positionStart, int itemCount)
Called when items have been added to the adapter. The LayoutManager may choose to
requestLayout if the inserted items would require refreshing the currently visible set
of child views. (e.g. currently empty space would be filled by appended items, etc.)
Called in response to a call to RecyclerView.Adapter.notifyDataSetChanged() or
RecyclerView.swapAdapter(RecyclerView.Adapter, boolean) ()} and signals that the the entire
data set has changed.
public void
onItemsMoved(
RecyclerView recyclerView, int from, int to, int itemCount)
Called when an item is moved withing the adapter.
Note that, an item may also change position in response to another ADD/REMOVE/MOVE
operation. This callback is only called if and only if RecyclerView.Adapter.notifyItemMoved(int, int)
is called.
public void
onItemsUpdated(
RecyclerView recyclerView, int positionStart, int itemCount, java.lang.Object payload)
Called when items have been changed in the adapter and with optional payload.
Default implementation calls RecyclerView.LayoutManager.onItemsUpdated(RecyclerView, int, int).
public boolean
canScrollVertically()
Query if vertical scrolling is currently supported. The default implementation
returns false.
Returns:
True if this LayoutManager can scroll the current contents vertically
public boolean
canScrollHorizontally()
Query if horizontal scrolling is currently supported. The default implementation
returns false.
Returns:
True if this LayoutManager can scroll the current contents horizontally
public boolean
isLayoutReversed()
Query if the layout is in reverse order. This will affect, for example, keyboard
navigation via page up/page down. The default implementation returns false.
Returns:
true if this LayoutManager is currently in reverse order.
Scroll horizontally by dx pixels in screen coordinates and return the distance traveled.
The default implementation does nothing and returns 0.
Parameters:
dx: distance to scroll by in pixels. X increases as scroll position
approaches the right.
recycler: Recycler to use for fetching potentially cached views for a
position
state: Transient state of RecyclerView
Returns:
The actual distance scrolled. The return value will be negative if dx was
negative and scrolling proceeeded in that direction.
Math.abs(result)
may be less than dx if a boundary was reached.
Scroll vertically by dy pixels in screen coordinates and return the distance traveled.
The default implementation does nothing and returns 0.
Parameters:
dy: distance to scroll in pixels. Y increases as scroll position
approaches the bottom.
recycler: Recycler to use for fetching potentially cached views for a
position
state: Transient state of RecyclerView
Returns:
The actual distance scrolled. The return value will be negative if dy was
negative and scrolling proceeeded in that direction.
Math.abs(result)
may be less than dy if a boundary was reached.
public PointF
computeScrollVectorForPosition(int targetPosition)
Smooth scroll to the specified adapter position.
To support smooth scrolling, override this method, create your RecyclerView.SmoothScroller
instance and call RecyclerView.LayoutManager.
Parameters:
recyclerView: The RecyclerView to which this layout manager is attached
state: Current State of RecyclerView
position: Scroll to this adapter position.
public void
scrollToPosition(int position)
Scroll to the specified adapter position.
Actual position of the item on the screen depends on the LayoutManager implementation.
Parameters:
position: Scroll to this adapter position.
public void
scrollToPositionWithOffset(int position, int offset)
Scroll to the specified adapter position with the given offset from layout start.
Note that scroll position change will not be reflected until the next layout call.
If you are just trying to make a position visible, use StaggeredGridLayoutManager.scrollToPosition(int).
Parameters:
position: Index (starting at 0) of the reference item.
offset: The distance (in pixels) between the start edge of the item view and
start edge of the RecyclerView.
See also: StaggeredGridLayoutManager.setReverseLayout(boolean), StaggeredGridLayoutManager.scrollToPosition(int)
Gather all positions from the LayoutManager to be prefetched, given specified momentum.
If item prefetch is enabled, this method is called in between traversals to gather
which positions the LayoutManager will soon need, given upcoming movement in subsequent
traversals.
The LayoutManager should call RecyclerView.LayoutManager.LayoutPrefetchRegistry.addPosition(int, int) for
each item to be prepared, and these positions will have their ViewHolders created and
bound, if there is sufficient time available, in advance of being needed by a
scroll or layout.
Parameters:
dx: X movement component.
dy: Y movement component.
state: State of RecyclerView
layoutPrefetchRegistry: PrefetchRegistry to add prefetch entries into.
See also: RecyclerView.LayoutManager.isItemPrefetchEnabled(), RecyclerView.LayoutManager.collectInitialPrefetchPositions(int, RecyclerView.LayoutManager.LayoutPrefetchRegistry)
Create a default LayoutParams
object for a child of the RecyclerView.
LayoutManagers will often want to use a custom LayoutParams
type
to store extra information specific to the layout. Client code should subclass
RecyclerView.LayoutParams for this purpose.
Important: if you use your own custom LayoutParams
type
you must also override
RecyclerView.LayoutManager,
RecyclerView.LayoutManager and
RecyclerView.LayoutManager.
Returns:
A new LayoutParams for a child view
Create a LayoutParams object suitable for this LayoutManager from
an inflated layout resource.
Important: if you use your own custom LayoutParams
type
you must also override
RecyclerView.LayoutManager,
RecyclerView.LayoutManager and
RecyclerView.LayoutManager.
Parameters:
c: Context for obtaining styled attributes
attrs: AttributeSet describing the supplied arguments
Returns:
a new LayoutParams object
Create a LayoutParams object suitable for this LayoutManager, copying relevant
values from the supplied LayoutParams object if possible.
Important: if you use your own custom LayoutParams
type
you must also override
RecyclerView.LayoutManager,
RecyclerView.LayoutManager and
RecyclerView.LayoutManager.
Parameters:
lp: Source LayoutParams object to copy values from
Returns:
a new LayoutParams object
Determines the validity of the supplied LayoutParams object.
This should check to make sure that the object is of the correct type
and all values are within acceptable ranges. The default implementation
returns true
for non-null params.
Parameters:
lp: LayoutParams object to check
Returns:
true if this LayoutParams object is valid, false otherwise
public int
getOrientation()
Called when searching for a focusable view in the given direction has failed
for the current content of the RecyclerView.
This is the LayoutManager's opportunity to populate views in the given direction
to fulfill the request if it can. The LayoutManager should attach and return
the view to be focused, if a focusable view in the given direction is found.
Otherwise, if all the existing (or the newly populated views) are unfocusable, it returns
the next unfocusable view to become visible on the screen. This unfocusable view is
typically the first view that's either partially or fully out of RV's padded bounded
area in the given direction. The default implementation returns null.
Parameters:
focused: The currently focused view
direction: One of View
, View
,
View
, View
,
View
, View
or 0 for not applicable
recycler: The recycler to use for obtaining views for currently offscreen items
state: Transient state of RecyclerView
Returns:
The chosen view to be focused if a focusable view is found, otherwise an
unfocusable view to become visible onto the screen, else null.
Source
/*
* Copyright 2018 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 androidx.recyclerview.widget;
import static androidx.annotation.RestrictTo.Scope.LIBRARY;
import android.annotation.SuppressLint;
import android.content.Context;
import android.graphics.PointF;
import android.graphics.Rect;
import android.os.Parcel;
import android.os.Parcelable;
import android.util.AttributeSet;
import android.util.Log;
import android.view.View;
import android.view.ViewGroup;
import android.view.accessibility.AccessibilityEvent;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.annotation.RestrictTo;
import androidx.core.view.accessibility.AccessibilityNodeInfoCompat;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.BitSet;
import java.util.List;
/**
* A LayoutManager that lays out children in a staggered grid formation.
* It supports horizontal & vertical layout as well as an ability to layout children in reverse.
* <p>
* Staggered grids are likely to have gaps at the edges of the layout. To avoid these gaps,
* StaggeredGridLayoutManager can offset spans independently or move items between spans. You can
* control this behavior via {@link #setGapStrategy(int)}.
*/
public class StaggeredGridLayoutManager extends RecyclerView.LayoutManager implements
RecyclerView.SmoothScroller.ScrollVectorProvider {
private static final String TAG = "StaggeredGridLManager";
static final boolean DEBUG = false;
public static final int HORIZONTAL = RecyclerView.HORIZONTAL;
public static final int VERTICAL = RecyclerView.VERTICAL;
/**
* Does not do anything to hide gaps.
*/
public static final int GAP_HANDLING_NONE = 0;
/**
* @deprecated No longer supported.
*/
@SuppressWarnings("unused")
@Deprecated
public static final int GAP_HANDLING_LAZY = 1;
/**
* When scroll state is changed to {@link RecyclerView#SCROLL_STATE_IDLE}, StaggeredGrid will
* check if there are gaps in the because of full span items. If it finds, it will re-layout
* and move items to correct positions with animations.
* <p>
* For example, if LayoutManager ends up with the following layout due to adapter changes:
* <pre>
* AAA
* _BC
* DDD
* </pre>
* <p>
* It will animate to the following state:
* <pre>
* AAA
* BC_
* DDD
* </pre>
*/
public static final int GAP_HANDLING_MOVE_ITEMS_BETWEEN_SPANS = 2;
static final int INVALID_OFFSET = Integer.MIN_VALUE;
/**
* While trying to find next view to focus, LayoutManager will not try to scroll more
* than this factor times the total space of the list. If layout is vertical, total space is the
* height minus padding, if layout is horizontal, total space is the width minus padding.
*/
private static final float MAX_SCROLL_FACTOR = 1 / 3f;
/**
* Number of spans
*/
private int mSpanCount = -1;
Span[] mSpans;
/**
* Primary orientation is the layout's orientation, secondary orientation is the orientation
* for spans. Having both makes code much cleaner for calculations.
*/
@NonNull
OrientationHelper mPrimaryOrientation;
@NonNull
OrientationHelper mSecondaryOrientation;
private int mOrientation;
/**
* The width or height per span, depending on the orientation.
*/
private int mSizePerSpan;
@NonNull
private final LayoutState mLayoutState;
boolean mReverseLayout = false;
/**
* Aggregated reverse layout value that takes RTL into account.
*/
boolean mShouldReverseLayout = false;
/**
* Temporary variable used during fill method to check which spans needs to be filled.
*/
private BitSet mRemainingSpans;
/**
* When LayoutManager needs to scroll to a position, it sets this variable and requests a
* layout which will check this variable and re-layout accordingly.
*/
int mPendingScrollPosition = RecyclerView.NO_POSITION;
/**
* Used to keep the offset value when {@link #scrollToPositionWithOffset(int, int)} is
* called.
*/
int mPendingScrollPositionOffset = INVALID_OFFSET;
/**
* Keeps the mapping between the adapter positions and spans. This is necessary to provide
* a consistent experience when user scrolls the list.
*/
LazySpanLookup mLazySpanLookup = new LazySpanLookup();
/**
* how we handle gaps in UI.
*/
private int mGapStrategy = GAP_HANDLING_MOVE_ITEMS_BETWEEN_SPANS;
/**
* Saved state needs this information to properly layout on restore.
*/
private boolean mLastLayoutFromEnd;
/**
* Saved state and onLayout needs this information to re-layout properly
*/
private boolean mLastLayoutRTL;
/**
* SavedState is not handled until a layout happens. This is where we keep it until next
* layout.
*/
private SavedState mPendingSavedState;
/**
* Re-used measurement specs. updated by onLayout.
*/
private int mFullSizeSpec;
/**
* Re-used rectangle to get child decor offsets.
*/
private final Rect mTmpRect = new Rect();
/**
* Re-used anchor info.
*/
private final AnchorInfo mAnchorInfo = new AnchorInfo();
/**
* If a full span item is invalid / or created in reverse direction; it may create gaps in
* the UI. While laying out, if such case is detected, we set this flag.
* <p>
* After scrolling stops, we check this flag and if it is set, re-layout.
*/
private boolean mLaidOutInvalidFullSpan = false;
/**
* Works the same way as {@link android.widget.AbsListView#setSmoothScrollbarEnabled(boolean)}.
* see {@link android.widget.AbsListView#setSmoothScrollbarEnabled(boolean)}
*/
private boolean mSmoothScrollbarEnabled = true;
/**
* Temporary array used (solely in {@link #collectAdjacentPrefetchPositions}) for stashing and
* sorting distances to views being prefetched.
*/
private int[] mPrefetchDistances;
private final Runnable mCheckForGapsRunnable = new Runnable() {
@Override
public void run() {
checkForGaps();
}
};
/**
* Constructor used when layout manager is set in XML by RecyclerView attribute
* "layoutManager". Defaults to single column and vertical.
*/
@SuppressWarnings("unused")
public StaggeredGridLayoutManager(Context context, AttributeSet attrs, int defStyleAttr,
int defStyleRes) {
Properties properties = getProperties(context, attrs, defStyleAttr, defStyleRes);
setOrientation(properties.orientation);
setSpanCount(properties.spanCount);
setReverseLayout(properties.reverseLayout);
mLayoutState = new LayoutState();
createOrientationHelpers();
}
/**
* Creates a StaggeredGridLayoutManager with given parameters.
*
* @param spanCount If orientation is vertical, spanCount is number of columns. If
* orientation is horizontal, spanCount is number of rows.
* @param orientation {@link #VERTICAL} or {@link #HORIZONTAL}
*/
public StaggeredGridLayoutManager(int spanCount, int orientation) {
mOrientation = orientation;
setSpanCount(spanCount);
mLayoutState = new LayoutState();
createOrientationHelpers();
}
@Override
public boolean isAutoMeasureEnabled() {
return mGapStrategy != GAP_HANDLING_NONE;
}
private void createOrientationHelpers() {
mPrimaryOrientation = OrientationHelper.createOrientationHelper(this, mOrientation);
mSecondaryOrientation = OrientationHelper
.createOrientationHelper(this, 1 - mOrientation);
}
/**
* Checks for gaps in the UI that may be caused by adapter changes.
* <p>
* When a full span item is laid out in reverse direction, it sets a flag which we check when
* scroll is stopped (or re-layout happens) and re-layout after first valid item.
*/
boolean checkForGaps() {
if (getChildCount() == 0 || mGapStrategy == GAP_HANDLING_NONE || !isAttachedToWindow()) {
return false;
}
final int minPos, maxPos;
if (mShouldReverseLayout) {
minPos = getLastChildPosition();
maxPos = getFirstChildPosition();
} else {
minPos = getFirstChildPosition();
maxPos = getLastChildPosition();
}
if (minPos == 0) {
View gapView = hasGapsToFix();
if (gapView != null) {
mLazySpanLookup.clear();
requestSimpleAnimationsInNextLayout();
requestLayout();
return true;
}
}
if (!mLaidOutInvalidFullSpan) {
return false;
}
int invalidGapDir = mShouldReverseLayout ? LayoutState.LAYOUT_START : LayoutState.LAYOUT_END;
final LazySpanLookup.FullSpanItem invalidFsi = mLazySpanLookup
.getFirstFullSpanItemInRange(minPos, maxPos + 1, invalidGapDir, true);
if (invalidFsi == null) {
mLaidOutInvalidFullSpan = false;
mLazySpanLookup.forceInvalidateAfter(maxPos + 1);
return false;
}
final LazySpanLookup.FullSpanItem validFsi = mLazySpanLookup
.getFirstFullSpanItemInRange(minPos, invalidFsi.mPosition,
invalidGapDir * -1, true);
if (validFsi == null) {
mLazySpanLookup.forceInvalidateAfter(invalidFsi.mPosition);
} else {
mLazySpanLookup.forceInvalidateAfter(validFsi.mPosition + 1);
}
requestSimpleAnimationsInNextLayout();
requestLayout();
return true;
}
@Override
public void onScrollStateChanged(int state) {
if (state == RecyclerView.SCROLL_STATE_IDLE) {
checkForGaps();
}
}
@Override
public void onDetachedFromWindow(RecyclerView view, RecyclerView.Recycler recycler) {
super.onDetachedFromWindow(view, recycler);
removeCallbacks(mCheckForGapsRunnable);
for (int i = 0; i < mSpanCount; i++) {
mSpans[i].clear();
}
// SGLM will require fresh layout call to recover state after detach
view.requestLayout();
}
/**
* Checks for gaps if we've reached to the top of the list.
* <p>
* Intermediate gaps created by full span items are tracked via mLaidOutInvalidFullSpan field.
*/
View hasGapsToFix() {
int startChildIndex = 0;
int endChildIndex = getChildCount() - 1;
BitSet mSpansToCheck = new BitSet(mSpanCount);
mSpansToCheck.set(0, mSpanCount, true);
final int firstChildIndex, childLimit;
final int preferredSpanDir = mOrientation == VERTICAL && isLayoutRTL() ? 1 : -1;
if (mShouldReverseLayout) {
firstChildIndex = endChildIndex;
childLimit = startChildIndex - 1;
} else {
firstChildIndex = startChildIndex;
childLimit = endChildIndex + 1;
}
final int nextChildDiff = firstChildIndex < childLimit ? 1 : -1;
for (int i = firstChildIndex; i != childLimit; i += nextChildDiff) {
View child = getChildAt(i);
LayoutParams lp = (LayoutParams) child.getLayoutParams();
if (mSpansToCheck.get(lp.mSpan.mIndex)) {
if (checkSpanForGap(lp.mSpan)) {
return child;
}
mSpansToCheck.clear(lp.mSpan.mIndex);
}
if (lp.mFullSpan) {
continue; // quick reject
}
if (i + nextChildDiff != childLimit) {
View nextChild = getChildAt(i + nextChildDiff);
boolean compareSpans = false;
if (mShouldReverseLayout) {
// ensure child's end is below nextChild's end
int myEnd = mPrimaryOrientation.getDecoratedEnd(child);
int nextEnd = mPrimaryOrientation.getDecoratedEnd(nextChild);
if (myEnd < nextEnd) {
return child; //i should have a better position
} else if (myEnd == nextEnd) {
compareSpans = true;
}
} else {
int myStart = mPrimaryOrientation.getDecoratedStart(child);
int nextStart = mPrimaryOrientation.getDecoratedStart(nextChild);
if (myStart > nextStart) {
return child; //i should have a better position
} else if (myStart == nextStart) {
compareSpans = true;
}
}
if (compareSpans) {
// equal, check span indices.
LayoutParams nextLp = (LayoutParams) nextChild.getLayoutParams();
if (lp.mSpan.mIndex - nextLp.mSpan.mIndex < 0 != preferredSpanDir < 0) {
return child;
}
}
}
}
// everything looks good
return null;
}
private boolean checkSpanForGap(Span span) {
if (mShouldReverseLayout) {
if (span.getEndLine() < mPrimaryOrientation.getEndAfterPadding()) {
// if it is full span, it is OK
final View endView = span.mViews.get(span.mViews.size() - 1);
final LayoutParams lp = span.getLayoutParams(endView);
return !lp.mFullSpan;
}
} else if (span.getStartLine() > mPrimaryOrientation.getStartAfterPadding()) {
// if it is full span, it is OK
final View startView = span.mViews.get(0);
final LayoutParams lp = span.getLayoutParams(startView);
return !lp.mFullSpan;
}
return false;
}
/**
* Sets the number of spans for the layout. This will invalidate all of the span assignments
* for Views.
* <p>
* Calling this method will automatically result in a new layout request unless the spanCount
* parameter is equal to current span count.
*
* @param spanCount Number of spans to layout
*/
public void setSpanCount(int spanCount) {
assertNotInLayoutOrScroll(null);
if (spanCount != mSpanCount) {
invalidateSpanAssignments();
mSpanCount = spanCount;
mRemainingSpans = new BitSet(mSpanCount);
mSpans = new Span[mSpanCount];
for (int i = 0; i < mSpanCount; i++) {
mSpans[i] = new Span(i);
}
requestLayout();
}
}
/**
* Sets the orientation of the layout. StaggeredGridLayoutManager will do its best to keep
* scroll position if this method is called after views are laid out.
*
* @param orientation {@link #HORIZONTAL} or {@link #VERTICAL}
*/
public void setOrientation(int orientation) {
if (orientation != HORIZONTAL && orientation != VERTICAL) {
throw new IllegalArgumentException("invalid orientation.");
}
assertNotInLayoutOrScroll(null);
if (orientation == mOrientation) {
return;
}
mOrientation = orientation;
OrientationHelper tmp = mPrimaryOrientation;
mPrimaryOrientation = mSecondaryOrientation;
mSecondaryOrientation = tmp;
requestLayout();
}
/**
* Sets whether LayoutManager should start laying out items from the end of the UI. The order
* items are traversed is not affected by this call.
* <p>
* For vertical layout, if it is set to <code>true</code>, first item will be at the bottom of
* the list.
* <p>
* For horizontal layouts, it depends on the layout direction.
* When set to true, If {@link RecyclerView} is LTR, than it will layout from RTL, if
* {@link RecyclerView}} is RTL, it will layout from LTR.
*
* @param reverseLayout Whether layout should be in reverse or not
*/
public void setReverseLayout(boolean reverseLayout) {
assertNotInLayoutOrScroll(null);
if (mPendingSavedState != null && mPendingSavedState.mReverseLayout != reverseLayout) {
mPendingSavedState.mReverseLayout = reverseLayout;
}
mReverseLayout = reverseLayout;
requestLayout();
}
/**
* Returns the current gap handling strategy for StaggeredGridLayoutManager.
* <p>
* Staggered grid may have gaps in the layout due to changes in the adapter. To avoid gaps,
* StaggeredGridLayoutManager provides 2 options. Check {@link #GAP_HANDLING_NONE} and
* {@link #GAP_HANDLING_MOVE_ITEMS_BETWEEN_SPANS} for details.
* <p>
* By default, StaggeredGridLayoutManager uses {@link #GAP_HANDLING_MOVE_ITEMS_BETWEEN_SPANS}.
*
* @return Current gap handling strategy.
* @see #setGapStrategy(int)
* @see #GAP_HANDLING_NONE
* @see #GAP_HANDLING_MOVE_ITEMS_BETWEEN_SPANS
*/
public int getGapStrategy() {
return mGapStrategy;
}
/**
* Sets the gap handling strategy for StaggeredGridLayoutManager. If the gapStrategy parameter
* is different than the current strategy, calling this method will trigger a layout request.
*
* @param gapStrategy The new gap handling strategy. Should be
* {@link #GAP_HANDLING_MOVE_ITEMS_BETWEEN_SPANS} or {@link
* #GAP_HANDLING_NONE}.
* @see #getGapStrategy()
*/
public void setGapStrategy(int gapStrategy) {
assertNotInLayoutOrScroll(null);
if (gapStrategy == mGapStrategy) {
return;
}
if (gapStrategy != GAP_HANDLING_NONE
&& gapStrategy != GAP_HANDLING_MOVE_ITEMS_BETWEEN_SPANS) {
throw new IllegalArgumentException("invalid gap strategy. Must be GAP_HANDLING_NONE "
+ "or GAP_HANDLING_MOVE_ITEMS_BETWEEN_SPANS");
}
mGapStrategy = gapStrategy;
requestLayout();
}
@Override
public void assertNotInLayoutOrScroll(String message) {
if (mPendingSavedState == null) {
super.assertNotInLayoutOrScroll(message);
}
}
/**
* Returns the number of spans laid out by StaggeredGridLayoutManager.
*
* @return Number of spans in the layout
*/
public int getSpanCount() {
return mSpanCount;
}
/**
* For consistency, StaggeredGridLayoutManager keeps a mapping between spans and items.
* <p>
* If you need to cancel current assignments, you can call this method which will clear all
* assignments and request a new layout.
*/
public void invalidateSpanAssignments() {
mLazySpanLookup.clear();
requestLayout();
}
/**
* Calculates the views' layout order. (e.g. from end to start or start to end)
* RTL layout support is applied automatically. So if layout is RTL and
* {@link #getReverseLayout()} is {@code true}, elements will be laid out starting from left.
*/
private void resolveShouldLayoutReverse() {
// A == B is the same result, but we rather keep it readable
if (mOrientation == VERTICAL || !isLayoutRTL()) {
mShouldReverseLayout = mReverseLayout;
} else {
mShouldReverseLayout = !mReverseLayout;
}
}
boolean isLayoutRTL() {
return getLayoutDirection() == View.LAYOUT_DIRECTION_RTL;
}
/**
* Returns whether views are laid out in reverse order or not.
* <p>
* Not that this value is not affected by RecyclerView's layout direction.
*
* @return True if layout is reversed, false otherwise
* @see #setReverseLayout(boolean)
*/
public boolean getReverseLayout() {
return mReverseLayout;
}
@Override
public void setMeasuredDimension(Rect childrenBounds, int wSpec, int hSpec) {
// we don't like it to wrap content in our non-scroll direction.
final int width, height;
final int horizontalPadding = getPaddingLeft() + getPaddingRight();
final int verticalPadding = getPaddingTop() + getPaddingBottom();
if (mOrientation == VERTICAL) {
final int usedHeight = childrenBounds.height() + verticalPadding;
height = chooseSize(hSpec, usedHeight, getMinimumHeight());
width = chooseSize(wSpec, mSizePerSpan * mSpanCount + horizontalPadding,
getMinimumWidth());
} else {
final int usedWidth = childrenBounds.width() + horizontalPadding;
width = chooseSize(wSpec, usedWidth, getMinimumWidth());
height = chooseSize(hSpec, mSizePerSpan * mSpanCount + verticalPadding,
getMinimumHeight());
}
setMeasuredDimension(width, height);
}
@Override
public void onLayoutChildren(RecyclerView.Recycler recycler, RecyclerView.State state) {
onLayoutChildren(recycler, state, true);
}
@Override
public void onAdapterChanged(@Nullable RecyclerView.Adapter oldAdapter,
@Nullable RecyclerView.Adapter newAdapter) {
// RV will remove all views so we should clear all spans and assignments of views into spans
mLazySpanLookup.clear();
for (int i = 0; i < mSpanCount; i++) {
mSpans[i].clear();
}
}
private void onLayoutChildren(RecyclerView.Recycler recycler, RecyclerView.State state,
boolean shouldCheckForGaps) {
final AnchorInfo anchorInfo = mAnchorInfo;
if (mPendingSavedState != null || mPendingScrollPosition != RecyclerView.NO_POSITION) {
if (state.getItemCount() == 0) {
removeAndRecycleAllViews(recycler);
anchorInfo.reset();
return;
}
}
boolean recalculateAnchor = !anchorInfo.mValid || mPendingScrollPosition != RecyclerView.NO_POSITION
|| mPendingSavedState != null;
if (recalculateAnchor) {
anchorInfo.reset();
if (mPendingSavedState != null) {
applyPendingSavedState(anchorInfo);
} else {
resolveShouldLayoutReverse();
anchorInfo.mLayoutFromEnd = mShouldReverseLayout;
}
updateAnchorInfoForLayout(state, anchorInfo);
anchorInfo.mValid = true;
}
if (mPendingSavedState == null && mPendingScrollPosition == RecyclerView.NO_POSITION) {
if (anchorInfo.mLayoutFromEnd != mLastLayoutFromEnd
|| isLayoutRTL() != mLastLayoutRTL) {
mLazySpanLookup.clear();
anchorInfo.mInvalidateOffsets = true;
}
}
if (getChildCount() > 0 && (mPendingSavedState == null
|| mPendingSavedState.mSpanOffsetsSize < 1)) {
if (anchorInfo.mInvalidateOffsets) {
for (int i = 0; i < mSpanCount; i++) {
// Scroll to position is set, clear.
mSpans[i].clear();
if (anchorInfo.mOffset != INVALID_OFFSET) {
mSpans[i].setLine(anchorInfo.mOffset);
}
}
} else {
if (recalculateAnchor || mAnchorInfo.mSpanReferenceLines == null) {
for (int i = 0; i < mSpanCount; i++) {
mSpans[i].cacheReferenceLineAndClear(mShouldReverseLayout,
anchorInfo.mOffset);
}
mAnchorInfo.saveSpanReferenceLines(mSpans);
} else {
for (int i = 0; i < mSpanCount; i++) {
final Span span = mSpans[i];
span.clear();
span.setLine(mAnchorInfo.mSpanReferenceLines[i]);
}
}
}
}
detachAndScrapAttachedViews(recycler);
mLayoutState.mRecycle = false;
mLaidOutInvalidFullSpan = false;
updateMeasureSpecs(mSecondaryOrientation.getTotalSpace());
updateLayoutState(anchorInfo.mPosition, state);
if (anchorInfo.mLayoutFromEnd) {
// Layout start.
setLayoutStateDirection(LayoutState.LAYOUT_START);
fill(recycler, mLayoutState, state);
// Layout end.
setLayoutStateDirection(LayoutState.LAYOUT_END);
mLayoutState.mCurrentPosition = anchorInfo.mPosition + mLayoutState.mItemDirection;
fill(recycler, mLayoutState, state);
} else {
// Layout end.
setLayoutStateDirection(LayoutState.LAYOUT_END);
fill(recycler, mLayoutState, state);
// Layout start.
setLayoutStateDirection(LayoutState.LAYOUT_START);
mLayoutState.mCurrentPosition = anchorInfo.mPosition + mLayoutState.mItemDirection;
fill(recycler, mLayoutState, state);
}
repositionToWrapContentIfNecessary();
if (getChildCount() > 0) {
if (mShouldReverseLayout) {
fixEndGap(recycler, state, true);
fixStartGap(recycler, state, false);
} else {
fixStartGap(recycler, state, true);
fixEndGap(recycler, state, false);
}
}
boolean hasGaps = false;
if (shouldCheckForGaps && !state.isPreLayout()) {
final boolean needToCheckForGaps = mGapStrategy != GAP_HANDLING_NONE
&& getChildCount() > 0
&& (mLaidOutInvalidFullSpan || hasGapsToFix() != null);
if (needToCheckForGaps) {
removeCallbacks(mCheckForGapsRunnable);
if (checkForGaps()) {
hasGaps = true;
}
}
}
if (state.isPreLayout()) {
mAnchorInfo.reset();
}
mLastLayoutFromEnd = anchorInfo.mLayoutFromEnd;
mLastLayoutRTL = isLayoutRTL();
if (hasGaps) {
mAnchorInfo.reset();
onLayoutChildren(recycler, state, false);
}
}
@Override
public void onLayoutCompleted(RecyclerView.State state) {
super.onLayoutCompleted(state);
mPendingScrollPosition = RecyclerView.NO_POSITION;
mPendingScrollPositionOffset = INVALID_OFFSET;
mPendingSavedState = null; // we don't need this anymore
mAnchorInfo.reset();
}
private void repositionToWrapContentIfNecessary() {
if (mSecondaryOrientation.getMode() == View.MeasureSpec.EXACTLY) {
return; // nothing to do
}
float maxSize = 0;
final int childCount = getChildCount();
for (int i = 0; i < childCount; i++) {
View child = getChildAt(i);
float size = mSecondaryOrientation.getDecoratedMeasurement(child);
if (size < maxSize) {
continue;
}
LayoutParams layoutParams = (LayoutParams) child.getLayoutParams();
if (layoutParams.isFullSpan()) {
size = 1f * size / mSpanCount;
}
maxSize = Math.max(maxSize, size);
}
int before = mSizePerSpan;
int desired = Math.round(maxSize * mSpanCount);
if (mSecondaryOrientation.getMode() == View.MeasureSpec.AT_MOST) {
desired = Math.min(desired, mSecondaryOrientation.getTotalSpace());
}
updateMeasureSpecs(desired);
if (mSizePerSpan == before) {
return; // nothing has changed
}
for (int i = 0; i < childCount; i++) {
View child = getChildAt(i);
final LayoutParams lp = (LayoutParams) child.getLayoutParams();
if (lp.mFullSpan) {
continue;
}
if (isLayoutRTL() && mOrientation == VERTICAL) {
int newOffset = -(mSpanCount - 1 - lp.mSpan.mIndex) * mSizePerSpan;
int prevOffset = -(mSpanCount - 1 - lp.mSpan.mIndex) * before;
child.offsetLeftAndRight(newOffset - prevOffset);
} else {
int newOffset = lp.mSpan.mIndex * mSizePerSpan;
int prevOffset = lp.mSpan.mIndex * before;
if (mOrientation == VERTICAL) {
child.offsetLeftAndRight(newOffset - prevOffset);
} else {
child.offsetTopAndBottom(newOffset - prevOffset);
}
}
}
}
private void applyPendingSavedState(AnchorInfo anchorInfo) {
if (DEBUG) {
Log.d(TAG, "found saved state: " + mPendingSavedState);
}
if (mPendingSavedState.mSpanOffsetsSize > 0) {
if (mPendingSavedState.mSpanOffsetsSize == mSpanCount) {
for (int i = 0; i < mSpanCount; i++) {
mSpans[i].clear();
int line = mPendingSavedState.mSpanOffsets[i];
if (line != Span.INVALID_LINE) {
if (mPendingSavedState.mAnchorLayoutFromEnd) {
line += mPrimaryOrientation.getEndAfterPadding();
} else {
line += mPrimaryOrientation.getStartAfterPadding();
}
}
mSpans[i].setLine(line);
}
} else {
mPendingSavedState.invalidateSpanInfo();
mPendingSavedState.mAnchorPosition = mPendingSavedState.mVisibleAnchorPosition;
}
}
mLastLayoutRTL = mPendingSavedState.mLastLayoutRTL;
setReverseLayout(mPendingSavedState.mReverseLayout);
resolveShouldLayoutReverse();
if (mPendingSavedState.mAnchorPosition != RecyclerView.NO_POSITION) {
mPendingScrollPosition = mPendingSavedState.mAnchorPosition;
anchorInfo.mLayoutFromEnd = mPendingSavedState.mAnchorLayoutFromEnd;
} else {
anchorInfo.mLayoutFromEnd = mShouldReverseLayout;
}
if (mPendingSavedState.mSpanLookupSize > 1) {
mLazySpanLookup.mData = mPendingSavedState.mSpanLookup;
mLazySpanLookup.mFullSpanItems = mPendingSavedState.mFullSpanItems;
}
}
void updateAnchorInfoForLayout(RecyclerView.State state, AnchorInfo anchorInfo) {
if (updateAnchorFromPendingData(state, anchorInfo)) {
return;
}
if (updateAnchorFromChildren(state, anchorInfo)) {
return;
}
if (DEBUG) {
Log.d(TAG, "Deciding anchor info from fresh state");
}
anchorInfo.assignCoordinateFromPadding();
anchorInfo.mPosition = 0;
}
private boolean updateAnchorFromChildren(RecyclerView.State state, AnchorInfo anchorInfo) {
// We don't recycle views out of adapter order. This way, we can rely on the first or
// last child as the anchor position.
// Layout direction may change but we should select the child depending on the latest
// layout direction. Otherwise, we'll choose the wrong child.
anchorInfo.mPosition = mLastLayoutFromEnd
? findLastReferenceChildPosition(state.getItemCount())
: findFirstReferenceChildPosition(state.getItemCount());
anchorInfo.mOffset = INVALID_OFFSET;
return true;
}
boolean updateAnchorFromPendingData(RecyclerView.State state, AnchorInfo anchorInfo) {
// Validate scroll position if exists.
if (state.isPreLayout() || mPendingScrollPosition == RecyclerView.NO_POSITION) {
return false;
}
// Validate it.
if (mPendingScrollPosition < 0 || mPendingScrollPosition >= state.getItemCount()) {
mPendingScrollPosition = RecyclerView.NO_POSITION;
mPendingScrollPositionOffset = INVALID_OFFSET;
return false;
}
if (mPendingSavedState == null || mPendingSavedState.mAnchorPosition == RecyclerView.NO_POSITION
|| mPendingSavedState.mSpanOffsetsSize < 1) {
// If item is visible, make it fully visible.
final View child = findViewByPosition(mPendingScrollPosition);
if (child != null) {
// Use regular anchor position, offset according to pending offset and target
// child
anchorInfo.mPosition = mShouldReverseLayout ? getLastChildPosition()
: getFirstChildPosition();
if (mPendingScrollPositionOffset != INVALID_OFFSET) {
if (anchorInfo.mLayoutFromEnd) {
final int target = mPrimaryOrientation.getEndAfterPadding()
- mPendingScrollPositionOffset;
anchorInfo.mOffset = target - mPrimaryOrientation.getDecoratedEnd(child);
} else {
final int target = mPrimaryOrientation.getStartAfterPadding()
+ mPendingScrollPositionOffset;
anchorInfo.mOffset = target - mPrimaryOrientation.getDecoratedStart(child);
}
return true;
}
// no offset provided. Decide according to the child location
final int childSize = mPrimaryOrientation.getDecoratedMeasurement(child);
if (childSize > mPrimaryOrientation.getTotalSpace()) {
// Item does not fit. Fix depending on layout direction.
anchorInfo.mOffset = anchorInfo.mLayoutFromEnd
? mPrimaryOrientation.getEndAfterPadding()
: mPrimaryOrientation.getStartAfterPadding();
return true;
}
final int startGap = mPrimaryOrientation.getDecoratedStart(child)
- mPrimaryOrientation.getStartAfterPadding();
if (startGap < 0) {
anchorInfo.mOffset = -startGap;
return true;
}
final int endGap = mPrimaryOrientation.getEndAfterPadding()
- mPrimaryOrientation.getDecoratedEnd(child);
if (endGap < 0) {
anchorInfo.mOffset = endGap;
return true;
}
// child already visible. just layout as usual
anchorInfo.mOffset = INVALID_OFFSET;
} else {
// Child is not visible. Set anchor coordinate depending on in which direction
// child will be visible.
anchorInfo.mPosition = mPendingScrollPosition;
if (mPendingScrollPositionOffset == INVALID_OFFSET) {
final int position = calculateScrollDirectionForPosition(
anchorInfo.mPosition);
anchorInfo.mLayoutFromEnd = position == LayoutState.LAYOUT_END;
anchorInfo.assignCoordinateFromPadding();
} else {
anchorInfo.assignCoordinateFromPadding(mPendingScrollPositionOffset);
}
anchorInfo.mInvalidateOffsets = true;
}
} else {
anchorInfo.mOffset = INVALID_OFFSET;
anchorInfo.mPosition = mPendingScrollPosition;
}
return true;
}
void updateMeasureSpecs(int totalSpace) {
mSizePerSpan = totalSpace / mSpanCount;
//noinspection ResourceType
mFullSizeSpec = View.MeasureSpec.makeMeasureSpec(
totalSpace, mSecondaryOrientation.getMode());
}
@Override
public boolean supportsPredictiveItemAnimations() {
return mPendingSavedState == null;
}
/**
* Returns the adapter position of the first visible view for each span.
* <p>
* Note that, this value is not affected by layout orientation or item order traversal.
* ({@link #setReverseLayout(boolean)}). Views are sorted by their positions in the adapter,
* not in the layout.
* <p>
* If RecyclerView has item decorators, they will be considered in calculations as well.
* <p>
* StaggeredGridLayoutManager may pre-cache some views that are not necessarily visible. Those
* views are ignored in this method.
*
* @param into An array to put the results into. If you don't provide any, LayoutManager will
* create a new one.
* @return The adapter position of the first visible item in each span. If a span does not have
* any items, {@link RecyclerView#NO_POSITION} is returned for that span.
* @see #findFirstCompletelyVisibleItemPositions(int[])
* @see #findLastVisibleItemPositions(int[])
*/
public int[] findFirstVisibleItemPositions(int[] into) {
if (into == null) {
into = new int[mSpanCount];
} else if (into.length < mSpanCount) {
throw new IllegalArgumentException("Provided int[]'s size must be more than or equal"
+ " to span count. Expected:" + mSpanCount + ", array size:" + into.length);
}
for (int i = 0; i < mSpanCount; i++) {
into[i] = mSpans[i].findFirstVisibleItemPosition();
}
return into;
}
/**
* Returns the adapter position of the first completely visible view for each span.
* <p>
* Note that, this value is not affected by layout orientation or item order traversal.
* ({@link #setReverseLayout(boolean)}). Views are sorted by their positions in the adapter,
* not in the layout.
* <p>
* If RecyclerView has item decorators, they will be considered in calculations as well.
* <p>
* StaggeredGridLayoutManager may pre-cache some views that are not necessarily visible. Those
* views are ignored in this method.
*
* @param into An array to put the results into. If you don't provide any, LayoutManager will
* create a new one.
* @return The adapter position of the first fully visible item in each span. If a span does
* not have any items, {@link RecyclerView#NO_POSITION} is returned for that span.
* @see #findFirstVisibleItemPositions(int[])
* @see #findLastCompletelyVisibleItemPositions(int[])
*/
public int[] findFirstCompletelyVisibleItemPositions(int[] into) {
if (into == null) {
into = new int[mSpanCount];
} else if (into.length < mSpanCount) {
throw new IllegalArgumentException("Provided int[]'s size must be more than or equal"
+ " to span count. Expected:" + mSpanCount + ", array size:" + into.length);
}
for (int i = 0; i < mSpanCount; i++) {
into[i] = mSpans[i].findFirstCompletelyVisibleItemPosition();
}
return into;
}
/**
* Returns the adapter position of the last visible view for each span.
* <p>
* Note that, this value is not affected by layout orientation or item order traversal.
* ({@link #setReverseLayout(boolean)}). Views are sorted by their positions in the adapter,
* not in the layout.
* <p>
* If RecyclerView has item decorators, they will be considered in calculations as well.
* <p>
* StaggeredGridLayoutManager may pre-cache some views that are not necessarily visible. Those
* views are ignored in this method.
*
* @param into An array to put the results into. If you don't provide any, LayoutManager will
* create a new one.
* @return The adapter position of the last visible item in each span. If a span does not have
* any items, {@link RecyclerView#NO_POSITION} is returned for that span.
* @see #findLastCompletelyVisibleItemPositions(int[])
* @see #findFirstVisibleItemPositions(int[])
*/
public int[] findLastVisibleItemPositions(int[] into) {
if (into == null) {
into = new int[mSpanCount];
} else if (into.length < mSpanCount) {
throw new IllegalArgumentException("Provided int[]'s size must be more than or equal"
+ " to span count. Expected:" + mSpanCount + ", array size:" + into.length);
}
for (int i = 0; i < mSpanCount; i++) {
into[i] = mSpans[i].findLastVisibleItemPosition();
}
return into;
}
/**
* Returns the adapter position of the last completely visible view for each span.
* <p>
* Note that, this value is not affected by layout orientation or item order traversal.
* ({@link #setReverseLayout(boolean)}). Views are sorted by their positions in the adapter,
* not in the layout.
* <p>
* If RecyclerView has item decorators, they will be considered in calculations as well.
* <p>
* StaggeredGridLayoutManager may pre-cache some views that are not necessarily visible. Those
* views are ignored in this method.
*
* @param into An array to put the results into. If you don't provide any, LayoutManager will
* create a new one.
* @return The adapter position of the last fully visible item in each span. If a span does not
* have any items, {@link RecyclerView#NO_POSITION} is returned for that span.
* @see #findFirstCompletelyVisibleItemPositions(int[])
* @see #findLastVisibleItemPositions(int[])
*/
public int[] findLastCompletelyVisibleItemPositions(int[] into) {
if (into == null) {
into = new int[mSpanCount];
} else if (into.length < mSpanCount) {
throw new IllegalArgumentException("Provided int[]'s size must be more than or equal"
+ " to span count. Expected:" + mSpanCount + ", array size:" + into.length);
}
for (int i = 0; i < mSpanCount; i++) {
into[i] = mSpans[i].findLastCompletelyVisibleItemPosition();
}
return into;
}
@Override
public int computeHorizontalScrollOffset(RecyclerView.State state) {
return computeScrollOffset(state);
}
private int computeScrollOffset(RecyclerView.State state) {
if (getChildCount() == 0) {
return 0;
}
return ScrollbarHelper.computeScrollOffset(state, mPrimaryOrientation,
findFirstVisibleItemClosestToStart(!mSmoothScrollbarEnabled),
findFirstVisibleItemClosestToEnd(!mSmoothScrollbarEnabled),
this, mSmoothScrollbarEnabled, mShouldReverseLayout);
}
@Override
public int computeVerticalScrollOffset(RecyclerView.State state) {
return computeScrollOffset(state);
}
@Override
public int computeHorizontalScrollExtent(RecyclerView.State state) {
return computeScrollExtent(state);
}
private int computeScrollExtent(RecyclerView.State state) {
if (getChildCount() == 0) {
return 0;
}
return ScrollbarHelper.computeScrollExtent(state, mPrimaryOrientation,
findFirstVisibleItemClosestToStart(!mSmoothScrollbarEnabled),
findFirstVisibleItemClosestToEnd(!mSmoothScrollbarEnabled),
this, mSmoothScrollbarEnabled);
}
@Override
public int computeVerticalScrollExtent(RecyclerView.State state) {
return computeScrollExtent(state);
}
@Override
public int computeHorizontalScrollRange(RecyclerView.State state) {
return computeScrollRange(state);
}
private int computeScrollRange(RecyclerView.State state) {
if (getChildCount() == 0) {
return 0;
}
return ScrollbarHelper.computeScrollRange(state, mPrimaryOrientation,
findFirstVisibleItemClosestToStart(!mSmoothScrollbarEnabled),
findFirstVisibleItemClosestToEnd(!mSmoothScrollbarEnabled),
this, mSmoothScrollbarEnabled);
}
@Override
public int computeVerticalScrollRange(RecyclerView.State state) {
return computeScrollRange(state);
}
private void measureChildWithDecorationsAndMargin(View child, LayoutParams lp,
boolean alreadyMeasured) {
if (lp.mFullSpan) {
if (mOrientation == VERTICAL) {
measureChildWithDecorationsAndMargin(child, mFullSizeSpec,
getChildMeasureSpec(
getHeight(),
getHeightMode(),
getPaddingTop() + getPaddingBottom(),
lp.height,
true),
alreadyMeasured);
} else {
measureChildWithDecorationsAndMargin(
child,
getChildMeasureSpec(
getWidth(),
getWidthMode(),
getPaddingLeft() + getPaddingRight(),
lp.width,
true),
mFullSizeSpec,
alreadyMeasured);
}
} else {
if (mOrientation == VERTICAL) {
// Padding for width measure spec is 0 because left and right padding were already
// factored into mSizePerSpan.
measureChildWithDecorationsAndMargin(
child,
getChildMeasureSpec(
mSizePerSpan,
getWidthMode(),
0,
lp.width,
false),
getChildMeasureSpec(
getHeight(),
getHeightMode(),
getPaddingTop() + getPaddingBottom(),
lp.height,
true),
alreadyMeasured);
} else {
// Padding for height measure spec is 0 because top and bottom padding were already
// factored into mSizePerSpan.
measureChildWithDecorationsAndMargin(
child,
getChildMeasureSpec(
getWidth(),
getWidthMode(),
getPaddingLeft() + getPaddingRight(),
lp.width,
true),
getChildMeasureSpec(
mSizePerSpan,
getHeightMode(),
0,
lp.height,
false),
alreadyMeasured);
}
}
}
private void measureChildWithDecorationsAndMargin(View child, int widthSpec,
int heightSpec, boolean alreadyMeasured) {
calculateItemDecorationsForChild(child, mTmpRect);
LayoutParams lp = (LayoutParams) child.getLayoutParams();
widthSpec = updateSpecWithExtra(widthSpec, lp.leftMargin + mTmpRect.left,
lp.rightMargin + mTmpRect.right);
heightSpec = updateSpecWithExtra(heightSpec, lp.topMargin + mTmpRect.top,
lp.bottomMargin + mTmpRect.bottom);
final boolean measure = alreadyMeasured
? shouldReMeasureChild(child, widthSpec, heightSpec, lp)
: shouldMeasureChild(child, widthSpec, heightSpec, lp);
if (measure) {
child.measure(widthSpec, heightSpec);
}
}
private int updateSpecWithExtra(int spec, int startInset, int endInset) {
if (startInset == 0 && endInset == 0) {
return spec;
}
final int mode = View.MeasureSpec.getMode(spec);
if (mode == View.MeasureSpec.AT_MOST || mode == View.MeasureSpec.EXACTLY) {
return View.MeasureSpec.makeMeasureSpec(
Math.max(0, View.MeasureSpec.getSize(spec) - startInset - endInset), mode);
}
return spec;
}
@Override
public void onRestoreInstanceState(Parcelable state) {
if (state instanceof SavedState) {
mPendingSavedState = (SavedState) state;
if (mPendingScrollPosition != RecyclerView.NO_POSITION) {
mPendingSavedState.invalidateAnchorPositionInfo();
mPendingSavedState.invalidateSpanInfo();
}
requestLayout();
} else if (DEBUG) {
Log.d(TAG, "invalid saved state class");
}
}
@Override
public Parcelable onSaveInstanceState() {
if (mPendingSavedState != null) {
return new SavedState(mPendingSavedState);
}
SavedState state = new SavedState();
state.mReverseLayout = mReverseLayout;
state.mAnchorLayoutFromEnd = mLastLayoutFromEnd;
state.mLastLayoutRTL = mLastLayoutRTL;
if (mLazySpanLookup != null && mLazySpanLookup.mData != null) {
state.mSpanLookup = mLazySpanLookup.mData;
state.mSpanLookupSize = state.mSpanLookup.length;
state.mFullSpanItems = mLazySpanLookup.mFullSpanItems;
} else {
state.mSpanLookupSize = 0;
}
if (getChildCount() > 0) {
state.mAnchorPosition = mLastLayoutFromEnd ? getLastChildPosition()
: getFirstChildPosition();
state.mVisibleAnchorPosition = findFirstVisibleItemPositionInt();
state.mSpanOffsetsSize = mSpanCount;
state.mSpanOffsets = new int[mSpanCount];
for (int i = 0; i < mSpanCount; i++) {
int line;
if (mLastLayoutFromEnd) {
line = mSpans[i].getEndLine(Span.INVALID_LINE);
if (line != Span.INVALID_LINE) {
line -= mPrimaryOrientation.getEndAfterPadding();
}
} else {
line = mSpans[i].getStartLine(Span.INVALID_LINE);
if (line != Span.INVALID_LINE) {
line -= mPrimaryOrientation.getStartAfterPadding();
}
}
state.mSpanOffsets[i] = line;
}
} else {
state.mAnchorPosition = RecyclerView.NO_POSITION;
state.mVisibleAnchorPosition = RecyclerView.NO_POSITION;
state.mSpanOffsetsSize = 0;
}
if (DEBUG) {
Log.d(TAG, "saved state:\n" + state);
}
return state;
}
@Override
public void onInitializeAccessibilityNodeInfo(@NonNull RecyclerView.Recycler recycler,
@NonNull RecyclerView.State state, @NonNull AccessibilityNodeInfoCompat info) {
super.onInitializeAccessibilityNodeInfo(recycler, state, info);
// Setting the classname allows accessibility services to set a role for staggered grids
// and ensures that they are treated distinctly from canonical grids with clear row/column
// semantics.
info.setClassName("androidx.recyclerview.widget.StaggeredGridLayoutManager");
}
@Override
public void onInitializeAccessibilityNodeInfoForItem(@NonNull RecyclerView.Recycler recycler,
@NonNull RecyclerView.State state, @NonNull View host,
@NonNull AccessibilityNodeInfoCompat info) {
ViewGroup.LayoutParams lp = host.getLayoutParams();
if (!(lp instanceof LayoutParams)) {
super.onInitializeAccessibilityNodeInfoForItem(host, info);
return;
}
LayoutParams sglp = (LayoutParams) lp;
if (mOrientation == HORIZONTAL) {
info.setCollectionItemInfo(AccessibilityNodeInfoCompat.CollectionItemInfoCompat.obtain(
sglp.getSpanIndex(), sglp.mFullSpan ? mSpanCount : 1,
-1, -1, false, false));
} else { // VERTICAL
info.setCollectionItemInfo(AccessibilityNodeInfoCompat.CollectionItemInfoCompat.obtain(
-1, -1,
sglp.getSpanIndex(), sglp.mFullSpan ? mSpanCount : 1, false, false));
}
}
@Override
public void onInitializeAccessibilityEvent(AccessibilityEvent event) {
super.onInitializeAccessibilityEvent(event);
if (getChildCount() > 0) {
final View start = findFirstVisibleItemClosestToStart(false);
final View end = findFirstVisibleItemClosestToEnd(false);
if (start == null || end == null) {
return;
}
final int startPos = getPosition(start);
final int endPos = getPosition(end);
if (startPos < endPos) {
event.setFromIndex(startPos);
event.setToIndex(endPos);
} else {
event.setFromIndex(endPos);
event.setToIndex(startPos);
}
}
}
/**
* Finds the first fully visible child to be used as an anchor child if span count changes when
* state is restored. If no children is fully visible, returns a partially visible child instead
* of returning null.
*/
int findFirstVisibleItemPositionInt() {
final View first = mShouldReverseLayout ? findFirstVisibleItemClosestToEnd(true) :
findFirstVisibleItemClosestToStart(true);
return first == null ? RecyclerView.NO_POSITION : getPosition(first);
}
@Override
public int getRowCountForAccessibility(@NonNull RecyclerView.Recycler recycler,
@NonNull RecyclerView.State state) {
if (mOrientation == HORIZONTAL) {
return Math.min(mSpanCount, state.getItemCount());
}
return -1;
}
@Override
public int getColumnCountForAccessibility(@NonNull RecyclerView.Recycler recycler,
@NonNull RecyclerView.State state) {
if (mOrientation == VERTICAL) {
return Math.min(mSpanCount, state.getItemCount());
}
return -1;
}
/**
* This is for internal use. Not necessarily the child closest to start but the first child
* we find that matches the criteria.
* This method does not do any sorting based on child's start coordinate, instead, it uses
* children order.
*/
View findFirstVisibleItemClosestToStart(boolean fullyVisible) {
final int boundsStart = mPrimaryOrientation.getStartAfterPadding();
final int boundsEnd = mPrimaryOrientation.getEndAfterPadding();
final int limit = getChildCount();
View partiallyVisible = null;
for (int i = 0; i < limit; i++) {
final View child = getChildAt(i);
final int childStart = mPrimaryOrientation.getDecoratedStart(child);
final int childEnd = mPrimaryOrientation.getDecoratedEnd(child);
if (childEnd <= boundsStart || childStart >= boundsEnd) {
continue; // not visible at all
}
if (childStart >= boundsStart || !fullyVisible) {
// when checking for start, it is enough even if part of the child's top is visible
// as long as fully visible is not requested.
return child;
}
if (partiallyVisible == null) {
partiallyVisible = child;
}
}
return partiallyVisible;
}
/**
* This is for internal use. Not necessarily the child closest to bottom but the first child
* we find that matches the criteria.
* This method does not do any sorting based on child's end coordinate, instead, it uses
* children order.
*/
View findFirstVisibleItemClosestToEnd(boolean fullyVisible) {
final int boundsStart = mPrimaryOrientation.getStartAfterPadding();
final int boundsEnd = mPrimaryOrientation.getEndAfterPadding();
View partiallyVisible = null;
for (int i = getChildCount() - 1; i >= 0; i--) {
final View child = getChildAt(i);
final int childStart = mPrimaryOrientation.getDecoratedStart(child);
final int childEnd = mPrimaryOrientation.getDecoratedEnd(child);
if (childEnd <= boundsStart || childStart >= boundsEnd) {
continue; // not visible at all
}
if (childEnd <= boundsEnd || !fullyVisible) {
// when checking for end, it is enough even if part of the child's bottom is visible
// as long as fully visible is not requested.
return child;
}
if (partiallyVisible == null) {
partiallyVisible = child;
}
}
return partiallyVisible;
}
private void fixEndGap(RecyclerView.Recycler recycler, RecyclerView.State state,
boolean canOffsetChildren) {
final int maxEndLine = getMaxEnd(Integer.MIN_VALUE);
if (maxEndLine == Integer.MIN_VALUE) {
return;
}
int gap = mPrimaryOrientation.getEndAfterPadding() - maxEndLine;
int fixOffset;
if (gap > 0) {
fixOffset = -scrollBy(-gap, recycler, state);
} else {
return; // nothing to fix
}
gap -= fixOffset;
if (canOffsetChildren && gap > 0) {
mPrimaryOrientation.offsetChildren(gap);
}
}
private void fixStartGap(RecyclerView.Recycler recycler, RecyclerView.State state,
boolean canOffsetChildren) {
final int minStartLine = getMinStart(Integer.MAX_VALUE);
if (minStartLine == Integer.MAX_VALUE) {
return;
}
int gap = minStartLine - mPrimaryOrientation.getStartAfterPadding();
int fixOffset;
if (gap > 0) {
fixOffset = scrollBy(gap, recycler, state);
} else {
return; // nothing to fix
}
gap -= fixOffset;
if (canOffsetChildren && gap > 0) {
mPrimaryOrientation.offsetChildren(-gap);
}
}
private void updateLayoutState(int anchorPosition, RecyclerView.State state) {
mLayoutState.mAvailable = 0;
mLayoutState.mCurrentPosition = anchorPosition;
int startExtra = 0;
int endExtra = 0;
if (isSmoothScrolling()) {
final int targetPos = state.getTargetScrollPosition();
if (targetPos != RecyclerView.NO_POSITION) {
if (mShouldReverseLayout == targetPos < anchorPosition) {
endExtra = mPrimaryOrientation.getTotalSpace();
} else {
startExtra = mPrimaryOrientation.getTotalSpace();
}
}
}
// Line of the furthest row.
final boolean clipToPadding = getClipToPadding();
if (clipToPadding) {
mLayoutState.mStartLine = mPrimaryOrientation.getStartAfterPadding() - startExtra;
mLayoutState.mEndLine = mPrimaryOrientation.getEndAfterPadding() + endExtra;
} else {
mLayoutState.mEndLine = mPrimaryOrientation.getEnd() + endExtra;
mLayoutState.mStartLine = -startExtra;
}
mLayoutState.mStopInFocusable = false;
mLayoutState.mRecycle = true;
mLayoutState.mInfinite = mPrimaryOrientation.getMode() == View.MeasureSpec.UNSPECIFIED
&& mPrimaryOrientation.getEnd() == 0;
}
private void setLayoutStateDirection(int direction) {
mLayoutState.mLayoutDirection = direction;
mLayoutState.mItemDirection = (mShouldReverseLayout == (direction == LayoutState.LAYOUT_START))
? LayoutState.ITEM_DIRECTION_TAIL : LayoutState.ITEM_DIRECTION_HEAD;
}
@Override
public void offsetChildrenHorizontal(int dx) {
super.offsetChildrenHorizontal(dx);
for (int i = 0; i < mSpanCount; i++) {
mSpans[i].onOffset(dx);
}
}
@Override
public void offsetChildrenVertical(int dy) {
super.offsetChildrenVertical(dy);
for (int i = 0; i < mSpanCount; i++) {
mSpans[i].onOffset(dy);
}
}
@Override
public void onItemsRemoved(RecyclerView recyclerView, int positionStart, int itemCount) {
handleUpdate(positionStart, itemCount, AdapterHelper.UpdateOp.REMOVE);
}
@Override
public void onItemsAdded(RecyclerView recyclerView, int positionStart, int itemCount) {
handleUpdate(positionStart, itemCount, AdapterHelper.UpdateOp.ADD);
}
@Override
public void onItemsChanged(RecyclerView recyclerView) {
mLazySpanLookup.clear();
requestLayout();
}
@Override
public void onItemsMoved(RecyclerView recyclerView, int from, int to, int itemCount) {
handleUpdate(from, to, AdapterHelper.UpdateOp.MOVE);
}
@Override
public void onItemsUpdated(RecyclerView recyclerView, int positionStart, int itemCount,
Object payload) {
handleUpdate(positionStart, itemCount, AdapterHelper.UpdateOp.UPDATE);
}
/**
* Checks whether it should invalidate span assignments in response to an adapter change.
*/
private void handleUpdate(int positionStart, int itemCountOrToPosition, int cmd) {
int minPosition = mShouldReverseLayout ? getLastChildPosition() : getFirstChildPosition();
final int affectedRangeEnd; // exclusive
final int affectedRangeStart; // inclusive
if (cmd == AdapterHelper.UpdateOp.MOVE) {
if (positionStart < itemCountOrToPosition) {
affectedRangeEnd = itemCountOrToPosition + 1;
affectedRangeStart = positionStart;
} else {
affectedRangeEnd = positionStart + 1;
affectedRangeStart = itemCountOrToPosition;
}
} else {
affectedRangeStart = positionStart;
affectedRangeEnd = positionStart + itemCountOrToPosition;
}
mLazySpanLookup.invalidateAfter(affectedRangeStart);
switch (cmd) {
case AdapterHelper.UpdateOp.ADD:
mLazySpanLookup.offsetForAddition(positionStart, itemCountOrToPosition);
break;
case AdapterHelper.UpdateOp.REMOVE:
mLazySpanLookup.offsetForRemoval(positionStart, itemCountOrToPosition);
break;
case AdapterHelper.UpdateOp.MOVE:
// TODO optimize
mLazySpanLookup.offsetForRemoval(positionStart, 1);
mLazySpanLookup.offsetForAddition(itemCountOrToPosition, 1);
break;
}
if (affectedRangeEnd <= minPosition) {
return;
}
int maxPosition = mShouldReverseLayout ? getFirstChildPosition() : getLastChildPosition();
if (affectedRangeStart <= maxPosition) {
requestLayout();
}
}
private int fill(RecyclerView.Recycler recycler, LayoutState layoutState,
RecyclerView.State state) {
mRemainingSpans.set(0, mSpanCount, true);
// The target position we are trying to reach.
final int targetLine;
// Line of the furthest row.
if (mLayoutState.mInfinite) {
if (layoutState.mLayoutDirection == LayoutState.LAYOUT_END) {
targetLine = Integer.MAX_VALUE;
} else { // LAYOUT_START
targetLine = Integer.MIN_VALUE;
}
} else {
if (layoutState.mLayoutDirection == LayoutState.LAYOUT_END) {
targetLine = layoutState.mEndLine + layoutState.mAvailable;
} else { // LAYOUT_START
targetLine = layoutState.mStartLine - layoutState.mAvailable;
}
}
updateAllRemainingSpans(layoutState.mLayoutDirection, targetLine);
if (DEBUG) {
Log.d(TAG, "FILLING targetLine: " + targetLine + ","
+ "remaining spans:" + mRemainingSpans + ", state: " + layoutState);
}
// the default coordinate to add new view.
final int defaultNewViewLine = mShouldReverseLayout
? mPrimaryOrientation.getEndAfterPadding()
: mPrimaryOrientation.getStartAfterPadding();
boolean added = false;
while (layoutState.hasMore(state)
&& (mLayoutState.mInfinite || !mRemainingSpans.isEmpty())) {
View view = layoutState.next(recycler);
LayoutParams lp = ((LayoutParams) view.getLayoutParams());
final int position = lp.getViewLayoutPosition();
final int spanIndex = mLazySpanLookup.getSpan(position);
Span currentSpan;
final boolean assignSpan = spanIndex == LayoutParams.INVALID_SPAN_ID;
if (assignSpan) {
currentSpan = lp.mFullSpan ? mSpans[0] : getNextSpan(layoutState);
mLazySpanLookup.setSpan(position, currentSpan);
if (DEBUG) {
Log.d(TAG, "assigned " + currentSpan.mIndex + " for " + position);
}
} else {
if (DEBUG) {
Log.d(TAG, "using " + spanIndex + " for pos " + position);
}
currentSpan = mSpans[spanIndex];
}
// assign span before measuring so that item decorators can get updated span index
lp.mSpan = currentSpan;
if (layoutState.mLayoutDirection == LayoutState.LAYOUT_END) {
addView(view);
} else {
addView(view, 0);
}
measureChildWithDecorationsAndMargin(view, lp, false);
final int start;
final int end;
if (layoutState.mLayoutDirection == LayoutState.LAYOUT_END) {
start = lp.mFullSpan ? getMaxEnd(defaultNewViewLine)
: currentSpan.getEndLine(defaultNewViewLine);
end = start + mPrimaryOrientation.getDecoratedMeasurement(view);
if (assignSpan && lp.mFullSpan) {
LazySpanLookup.FullSpanItem fullSpanItem;
fullSpanItem = createFullSpanItemFromEnd(start);
fullSpanItem.mGapDir = LayoutState.LAYOUT_START;
fullSpanItem.mPosition = position;
mLazySpanLookup.addFullSpanItem(fullSpanItem);
}
} else {
end = lp.mFullSpan ? getMinStart(defaultNewViewLine)
: currentSpan.getStartLine(defaultNewViewLine);
start = end - mPrimaryOrientation.getDecoratedMeasurement(view);
if (assignSpan && lp.mFullSpan) {
LazySpanLookup.FullSpanItem fullSpanItem;
fullSpanItem = createFullSpanItemFromStart(end);
fullSpanItem.mGapDir = LayoutState.LAYOUT_END;
fullSpanItem.mPosition = position;
mLazySpanLookup.addFullSpanItem(fullSpanItem);
}
}
// check if this item may create gaps in the future
if (lp.mFullSpan && layoutState.mItemDirection == LayoutState.ITEM_DIRECTION_HEAD) {
if (assignSpan) {
mLaidOutInvalidFullSpan = true;
} else {
final boolean hasInvalidGap;
if (layoutState.mLayoutDirection == LayoutState.LAYOUT_END) {
hasInvalidGap = !areAllEndsEqual();
} else { // layoutState.mLayoutDirection == LAYOUT_START
hasInvalidGap = !areAllStartsEqual();
}
if (hasInvalidGap) {
final LazySpanLookup.FullSpanItem fullSpanItem = mLazySpanLookup
.getFullSpanItem(position);
if (fullSpanItem != null) {
fullSpanItem.mHasUnwantedGapAfter = true;
}
mLaidOutInvalidFullSpan = true;
}
}
}
attachViewToSpans(view, lp, layoutState);
final int otherStart;
final int otherEnd;
if (isLayoutRTL() && mOrientation == VERTICAL) {
otherEnd = lp.mFullSpan ? mSecondaryOrientation.getEndAfterPadding() :
mSecondaryOrientation.getEndAfterPadding()
- (mSpanCount - 1 - currentSpan.mIndex) * mSizePerSpan;
otherStart = otherEnd - mSecondaryOrientation.getDecoratedMeasurement(view);
} else {
otherStart = lp.mFullSpan ? mSecondaryOrientation.getStartAfterPadding()
: currentSpan.mIndex * mSizePerSpan
+ mSecondaryOrientation.getStartAfterPadding();
otherEnd = otherStart + mSecondaryOrientation.getDecoratedMeasurement(view);
}
if (mOrientation == VERTICAL) {
layoutDecoratedWithMargins(view, otherStart, start, otherEnd, end);
} else {
layoutDecoratedWithMargins(view, start, otherStart, end, otherEnd);
}
if (lp.mFullSpan) {
updateAllRemainingSpans(mLayoutState.mLayoutDirection, targetLine);
} else {
updateRemainingSpans(currentSpan, mLayoutState.mLayoutDirection, targetLine);
}
recycle(recycler, mLayoutState);
if (mLayoutState.mStopInFocusable && view.hasFocusable()) {
if (lp.mFullSpan) {
mRemainingSpans.clear();
} else {
mRemainingSpans.set(currentSpan.mIndex, false);
}
}
added = true;
}
if (!added) {
recycle(recycler, mLayoutState);
}
final int diff;
if (mLayoutState.mLayoutDirection == LayoutState.LAYOUT_START) {
final int minStart = getMinStart(mPrimaryOrientation.getStartAfterPadding());
diff = mPrimaryOrientation.getStartAfterPadding() - minStart;
} else {
final int maxEnd = getMaxEnd(mPrimaryOrientation.getEndAfterPadding());
diff = maxEnd - mPrimaryOrientation.getEndAfterPadding();
}
return diff > 0 ? Math.min(layoutState.mAvailable, diff) : 0;
}
private LazySpanLookup.FullSpanItem createFullSpanItemFromEnd(int newItemTop) {
LazySpanLookup.FullSpanItem fsi = new LazySpanLookup.FullSpanItem();
fsi.mGapPerSpan = new int[mSpanCount];
for (int i = 0; i < mSpanCount; i++) {
fsi.mGapPerSpan[i] = newItemTop - mSpans[i].getEndLine(newItemTop);
}
return fsi;
}
private LazySpanLookup.FullSpanItem createFullSpanItemFromStart(int newItemBottom) {
LazySpanLookup.FullSpanItem fsi = new LazySpanLookup.FullSpanItem();
fsi.mGapPerSpan = new int[mSpanCount];
for (int i = 0; i < mSpanCount; i++) {
fsi.mGapPerSpan[i] = mSpans[i].getStartLine(newItemBottom) - newItemBottom;
}
return fsi;
}
private void attachViewToSpans(View view, LayoutParams lp, LayoutState layoutState) {
if (layoutState.mLayoutDirection == LayoutState.LAYOUT_END) {
if (lp.mFullSpan) {
appendViewToAllSpans(view);
} else {
lp.mSpan.appendToSpan(view);
}
} else {
if (lp.mFullSpan) {
prependViewToAllSpans(view);
} else {
lp.mSpan.prependToSpan(view);
}
}
}
private void recycle(RecyclerView.Recycler recycler, LayoutState layoutState) {
if (!layoutState.mRecycle || layoutState.mInfinite) {
return;
}
if (layoutState.mAvailable == 0) {
// easy, recycle line is still valid
if (layoutState.mLayoutDirection == LayoutState.LAYOUT_START) {
recycleFromEnd(recycler, layoutState.mEndLine);
} else {
recycleFromStart(recycler, layoutState.mStartLine);
}
} else {
// scrolling case, recycle line can be shifted by how much space we could cover
// by adding new views
if (layoutState.mLayoutDirection == LayoutState.LAYOUT_START) {
// calculate recycle line
int scrolled = layoutState.mStartLine - getMaxStart(layoutState.mStartLine);
final int line;
if (scrolled < 0) {
line = layoutState.mEndLine;
} else {
line = layoutState.mEndLine - Math.min(scrolled, layoutState.mAvailable);
}
recycleFromEnd(recycler, line);
} else {
// calculate recycle line
int scrolled = getMinEnd(layoutState.mEndLine) - layoutState.mEndLine;
final int line;
if (scrolled < 0) {
line = layoutState.mStartLine;
} else {
line = layoutState.mStartLine + Math.min(scrolled, layoutState.mAvailable);
}
recycleFromStart(recycler, line);
}
}
}
private void appendViewToAllSpans(View view) {
// traverse in reverse so that we end up assigning full span items to 0
for (int i = mSpanCount - 1; i >= 0; i--) {
mSpans[i].appendToSpan(view);
}
}
private void prependViewToAllSpans(View view) {
// traverse in reverse so that we end up assigning full span items to 0
for (int i = mSpanCount - 1; i >= 0; i--) {
mSpans[i].prependToSpan(view);
}
}
private void updateAllRemainingSpans(int layoutDir, int targetLine) {
for (int i = 0; i < mSpanCount; i++) {
if (mSpans[i].mViews.isEmpty()) {
continue;
}
updateRemainingSpans(mSpans[i], layoutDir, targetLine);
}
}
private void updateRemainingSpans(Span span, int layoutDir, int targetLine) {
final int deletedSize = span.getDeletedSize();
if (layoutDir == LayoutState.LAYOUT_START) {
final int line = span.getStartLine();
if (line + deletedSize <= targetLine) {
mRemainingSpans.set(span.mIndex, false);
}
} else {
final int line = span.getEndLine();
if (line - deletedSize >= targetLine) {
mRemainingSpans.set(span.mIndex, false);
}
}
}
private int getMaxStart(int def) {
int maxStart = mSpans[0].getStartLine(def);
for (int i = 1; i < mSpanCount; i++) {
final int spanStart = mSpans[i].getStartLine(def);
if (spanStart > maxStart) {
maxStart = spanStart;
}
}
return maxStart;
}
private int getMinStart(int def) {
int minStart = mSpans[0].getStartLine(def);
for (int i = 1; i < mSpanCount; i++) {
final int spanStart = mSpans[i].getStartLine(def);
if (spanStart < minStart) {
minStart = spanStart;
}
}
return minStart;
}
boolean areAllEndsEqual() {
int end = mSpans[0].getEndLine(Span.INVALID_LINE);
for (int i = 1; i < mSpanCount; i++) {
if (mSpans[i].getEndLine(Span.INVALID_LINE) != end) {
return false;
}
}
return true;
}
boolean areAllStartsEqual() {
int start = mSpans[0].getStartLine(Span.INVALID_LINE);
for (int i = 1; i < mSpanCount; i++) {
if (mSpans[i].getStartLine(Span.INVALID_LINE) != start) {
return false;
}
}
return true;
}
private int getMaxEnd(int def) {
int maxEnd = mSpans[0].getEndLine(def);
for (int i = 1; i < mSpanCount; i++) {
final int spanEnd = mSpans[i].getEndLine(def);
if (spanEnd > maxEnd) {
maxEnd = spanEnd;
}
}
return maxEnd;
}
private int getMinEnd(int def) {
int minEnd = mSpans[0].getEndLine(def);
for (int i = 1; i < mSpanCount; i++) {
final int spanEnd = mSpans[i].getEndLine(def);
if (spanEnd < minEnd) {
minEnd = spanEnd;
}
}
return minEnd;
}
private void recycleFromStart(RecyclerView.Recycler recycler, int line) {
while (getChildCount() > 0) {
View child = getChildAt(0);
if (mPrimaryOrientation.getDecoratedEnd(child) <= line
&& mPrimaryOrientation.getTransformedEndWithDecoration(child) <= line) {
LayoutParams lp = (LayoutParams) child.getLayoutParams();
// Don't recycle the last View in a span not to lose span's start/end lines
if (lp.mFullSpan) {
for (int j = 0; j < mSpanCount; j++) {
if (mSpans[j].mViews.size() == 1) {
return;
}
}
for (int j = 0; j < mSpanCount; j++) {
mSpans[j].popStart();
}
} else {
if (lp.mSpan.mViews.size() == 1) {
return;
}
lp.mSpan.popStart();
}
removeAndRecycleView(child, recycler);
} else {
return; // done
}
}
}
private void recycleFromEnd(RecyclerView.Recycler recycler, int line) {
final int childCount = getChildCount();
int i;
for (i = childCount - 1; i >= 0; i--) {
View child = getChildAt(i);
if (mPrimaryOrientation.getDecoratedStart(child) >= line
&& mPrimaryOrientation.getTransformedStartWithDecoration(child) >= line) {
LayoutParams lp = (LayoutParams) child.getLayoutParams();
// Don't recycle the last View in a span not to lose span's start/end lines
if (lp.mFullSpan) {
for (int j = 0; j < mSpanCount; j++) {
if (mSpans[j].mViews.size() == 1) {
return;
}
}
for (int j = 0; j < mSpanCount; j++) {
mSpans[j].popEnd();
}
} else {
if (lp.mSpan.mViews.size() == 1) {
return;
}
lp.mSpan.popEnd();
}
removeAndRecycleView(child, recycler);
} else {
return; // done
}
}
}
/**
* @return True if last span is the first one we want to fill
*/
private boolean preferLastSpan(int layoutDir) {
if (mOrientation == HORIZONTAL) {
return (layoutDir == LayoutState.LAYOUT_START) != mShouldReverseLayout;
}
return ((layoutDir == LayoutState.LAYOUT_START) == mShouldReverseLayout) == isLayoutRTL();
}
/**
* Finds the span for the next view.
*/
private Span getNextSpan(LayoutState layoutState) {
final boolean preferLastSpan = preferLastSpan(layoutState.mLayoutDirection);
final int startIndex, endIndex, diff;
if (preferLastSpan) {
startIndex = mSpanCount - 1;
endIndex = -1;
diff = -1;
} else {
startIndex = 0;
endIndex = mSpanCount;
diff = 1;
}
if (layoutState.mLayoutDirection == LayoutState.LAYOUT_END) {
Span min = null;
int minLine = Integer.MAX_VALUE;
final int defaultLine = mPrimaryOrientation.getStartAfterPadding();
for (int i = startIndex; i != endIndex; i += diff) {
final Span other = mSpans[i];
int otherLine = other.getEndLine(defaultLine);
if (otherLine < minLine) {
min = other;
minLine = otherLine;
}
}
return min;
} else {
Span max = null;
int maxLine = Integer.MIN_VALUE;
final int defaultLine = mPrimaryOrientation.getEndAfterPadding();
for (int i = startIndex; i != endIndex; i += diff) {
final Span other = mSpans[i];
int otherLine = other.getStartLine(defaultLine);
if (otherLine > maxLine) {
max = other;
maxLine = otherLine;
}
}
return max;
}
}
@Override
public boolean canScrollVertically() {
return mOrientation == VERTICAL;
}
@Override
public boolean canScrollHorizontally() {
return mOrientation == HORIZONTAL;
}
@Override
public boolean isLayoutReversed() {
return mReverseLayout;
}
@Override
public int scrollHorizontallyBy(int dx, RecyclerView.Recycler recycler,
RecyclerView.State state) {
return scrollBy(dx, recycler, state);
}
@Override
public int scrollVerticallyBy(int dy, RecyclerView.Recycler recycler,
RecyclerView.State state) {
return scrollBy(dy, recycler, state);
}
private int calculateScrollDirectionForPosition(int position) {
if (getChildCount() == 0) {
return mShouldReverseLayout ? LayoutState.LAYOUT_END : LayoutState.LAYOUT_START;
}
final int firstChildPos = getFirstChildPosition();
return position < firstChildPos != mShouldReverseLayout ? LayoutState.LAYOUT_START : LayoutState.LAYOUT_END;
}
@Override
public PointF computeScrollVectorForPosition(int targetPosition) {
final int direction = calculateScrollDirectionForPosition(targetPosition);
PointF outVector = new PointF();
if (direction == 0) {
return null;
}
if (mOrientation == HORIZONTAL) {
outVector.x = direction;
outVector.y = 0;
} else {
outVector.x = 0;
outVector.y = direction;
}
return outVector;
}
@Override
public void smoothScrollToPosition(RecyclerView recyclerView, RecyclerView.State state,
int position) {
LinearSmoothScroller scroller = new LinearSmoothScroller(recyclerView.getContext());
scroller.setTargetPosition(position);
startSmoothScroll(scroller);
}
@Override
public void scrollToPosition(int position) {
if (mPendingSavedState != null && mPendingSavedState.mAnchorPosition != position) {
mPendingSavedState.invalidateAnchorPositionInfo();
}
mPendingScrollPosition = position;
mPendingScrollPositionOffset = INVALID_OFFSET;
requestLayout();
}
/**
* Scroll to the specified adapter position with the given offset from layout start.
* <p>
* Note that scroll position change will not be reflected until the next layout call.
* <p>
* If you are just trying to make a position visible, use {@link #scrollToPosition(int)}.
*
* @param position Index (starting at 0) of the reference item.
* @param offset The distance (in pixels) between the start edge of the item view and
* start edge of the RecyclerView.
* @see #setReverseLayout(boolean)
* @see #scrollToPosition(int)
*/
public void scrollToPositionWithOffset(int position, int offset) {
if (mPendingSavedState != null) {
mPendingSavedState.invalidateAnchorPositionInfo();
}
mPendingScrollPosition = position;
mPendingScrollPositionOffset = offset;
requestLayout();
}
@Override
@RestrictTo(LIBRARY)
public void collectAdjacentPrefetchPositions(int dx, int dy, RecyclerView.State state,
LayoutPrefetchRegistry layoutPrefetchRegistry) {
/* This method uses the simplifying assumption that the next N items (where N = span count)
* will be assigned, one-to-one, to spans, where ordering is based on which span extends
* least beyond the viewport.
*
* While this simplified model will be incorrect in some cases, it's difficult to know
* item heights, or whether individual items will be full span prior to construction.
*
* While this greedy estimation approach may underestimate the distance to prefetch items,
* it's very unlikely to overestimate them, so distances can be conservatively used to know
* the soonest (in terms of scroll distance) a prefetched view may come on screen.
*/
int delta = (mOrientation == HORIZONTAL) ? dx : dy;
if (getChildCount() == 0 || delta == 0) {
// can't support this scroll, so don't bother prefetching
return;
}
prepareLayoutStateForDelta(delta, state);
// build sorted list of distances to end of each span (though we don't care which is which)
if (mPrefetchDistances == null || mPrefetchDistances.length < mSpanCount) {
mPrefetchDistances = new int[mSpanCount];
}
int itemPrefetchCount = 0;
for (int i = 0; i < mSpanCount; i++) {
// compute number of pixels past the edge of the viewport that the current span extends
int distance = mLayoutState.mItemDirection == LayoutState.LAYOUT_START
? mLayoutState.mStartLine - mSpans[i].getStartLine(mLayoutState.mStartLine)
: mSpans[i].getEndLine(mLayoutState.mEndLine) - mLayoutState.mEndLine;
if (distance >= 0) {
// span extends to the edge, so prefetch next item
mPrefetchDistances[itemPrefetchCount] = distance;
itemPrefetchCount++;
}
}
Arrays.sort(mPrefetchDistances, 0, itemPrefetchCount);
// then assign them in order to the next N views (where N = span count)
for (int i = 0; i < itemPrefetchCount && mLayoutState.hasMore(state); i++) {
layoutPrefetchRegistry.addPosition(mLayoutState.mCurrentPosition,
mPrefetchDistances[i]);
mLayoutState.mCurrentPosition += mLayoutState.mItemDirection;
}
}
void prepareLayoutStateForDelta(int delta, RecyclerView.State state) {
final int referenceChildPosition;
final int layoutDir;
if (delta > 0) { // layout towards end
layoutDir = LayoutState.LAYOUT_END;
referenceChildPosition = getLastChildPosition();
} else {
layoutDir = LayoutState.LAYOUT_START;
referenceChildPosition = getFirstChildPosition();
}
mLayoutState.mRecycle = true;
updateLayoutState(referenceChildPosition, state);
setLayoutStateDirection(layoutDir);
mLayoutState.mCurrentPosition = referenceChildPosition + mLayoutState.mItemDirection;
mLayoutState.mAvailable = Math.abs(delta);
}
int scrollBy(int dt, RecyclerView.Recycler recycler, RecyclerView.State state) {
if (getChildCount() == 0 || dt == 0) {
return 0;
}
prepareLayoutStateForDelta(dt, state);
int consumed = fill(recycler, mLayoutState, state);
final int available = mLayoutState.mAvailable;
final int totalScroll;
if (available < consumed) {
totalScroll = dt;
} else if (dt < 0) {
totalScroll = -consumed;
} else { // dt > 0
totalScroll = consumed;
}
if (DEBUG) {
Log.d(TAG, "asked " + dt + " scrolled" + totalScroll);
}
mPrimaryOrientation.offsetChildren(-totalScroll);
// always reset this if we scroll for a proper save instance state
mLastLayoutFromEnd = mShouldReverseLayout;
mLayoutState.mAvailable = 0;
recycle(recycler, mLayoutState);
return totalScroll;
}
int getLastChildPosition() {
final int childCount = getChildCount();
return childCount == 0 ? 0 : getPosition(getChildAt(childCount - 1));
}
int getFirstChildPosition() {
final int childCount = getChildCount();
return childCount == 0 ? 0 : getPosition(getChildAt(0));
}
/**
* Finds the first View that can be used as an anchor View.
*
* @return Position of the View or 0 if it cannot find any such View.
*/
private int findFirstReferenceChildPosition(int itemCount) {
final int limit = getChildCount();
for (int i = 0; i < limit; i++) {
final View view = getChildAt(i);
final int position = getPosition(view);
if (position >= 0 && position < itemCount) {
return position;
}
}
return 0;
}
/**
* Finds the last View that can be used as an anchor View.
*
* @return Position of the View or 0 if it cannot find any such View.
*/
private int findLastReferenceChildPosition(int itemCount) {
for (int i = getChildCount() - 1; i >= 0; i--) {
final View view = getChildAt(i);
final int position = getPosition(view);
if (position >= 0 && position < itemCount) {
return position;
}
}
return 0;
}
@SuppressWarnings("deprecation")
@Override
public RecyclerView.LayoutParams generateDefaultLayoutParams() {
if (mOrientation == HORIZONTAL) {
return new LayoutParams(ViewGroup.LayoutParams.WRAP_CONTENT,
ViewGroup.LayoutParams.MATCH_PARENT);
} else {
return new LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT,
ViewGroup.LayoutParams.WRAP_CONTENT);
}
}
@Override
public RecyclerView.LayoutParams generateLayoutParams(Context c, AttributeSet attrs) {
return new LayoutParams(c, attrs);
}
@Override
public RecyclerView.LayoutParams generateLayoutParams(ViewGroup.LayoutParams lp) {
if (lp instanceof ViewGroup.MarginLayoutParams) {
return new LayoutParams((ViewGroup.MarginLayoutParams) lp);
} else {
return new LayoutParams(lp);
}
}
@Override
public boolean checkLayoutParams(RecyclerView.LayoutParams lp) {
return lp instanceof LayoutParams;
}
public int getOrientation() {
return mOrientation;
}
@Nullable
@Override
public View onFocusSearchFailed(View focused, int direction, RecyclerView.Recycler recycler,
RecyclerView.State state) {
if (getChildCount() == 0) {
return null;
}
final View directChild = findContainingItemView(focused);
if (directChild == null) {
return null;
}
resolveShouldLayoutReverse();
final int layoutDir = convertFocusDirectionToLayoutDirection(direction);
if (layoutDir == LayoutState.INVALID_LAYOUT) {
return null;
}
LayoutParams prevFocusLayoutParams = (LayoutParams) directChild.getLayoutParams();
boolean prevFocusFullSpan = prevFocusLayoutParams.mFullSpan;
final Span prevFocusSpan = prevFocusLayoutParams.mSpan;
final int referenceChildPosition;
if (layoutDir == LayoutState.LAYOUT_END) { // layout towards end
referenceChildPosition = getLastChildPosition();
} else {
referenceChildPosition = getFirstChildPosition();
}
updateLayoutState(referenceChildPosition, state);
setLayoutStateDirection(layoutDir);
mLayoutState.mCurrentPosition = referenceChildPosition + mLayoutState.mItemDirection;
mLayoutState.mAvailable = (int) (MAX_SCROLL_FACTOR * mPrimaryOrientation.getTotalSpace());
mLayoutState.mStopInFocusable = true;
mLayoutState.mRecycle = false;
fill(recycler, mLayoutState, state);
mLastLayoutFromEnd = mShouldReverseLayout;
if (!prevFocusFullSpan) {
View view = prevFocusSpan.getFocusableViewAfter(referenceChildPosition, layoutDir);
if (view != null && view != directChild) {
return view;
}
}
// either could not find from the desired span or prev view is full span.
// traverse all spans
if (preferLastSpan(layoutDir)) {
for (int i = mSpanCount - 1; i >= 0; i--) {
View view = mSpans[i].getFocusableViewAfter(referenceChildPosition, layoutDir);
if (view != null && view != directChild) {
return view;
}
}
} else {
for (int i = 0; i < mSpanCount; i++) {
View view = mSpans[i].getFocusableViewAfter(referenceChildPosition, layoutDir);
if (view != null && view != directChild) {
return view;
}
}
}
// Could not find any focusable views from any of the existing spans. Now start the search
// to find the best unfocusable candidate to become visible on the screen next. The search
// is done in the same fashion: first, check the views in the desired span and if no
// candidate is found, traverse the views in all the remaining spans.
boolean shouldSearchFromStart = !mReverseLayout == (layoutDir == LayoutState.LAYOUT_START);
View unfocusableCandidate = null;
if (!prevFocusFullSpan) {
unfocusableCandidate = findViewByPosition(shouldSearchFromStart
? prevFocusSpan.findFirstPartiallyVisibleItemPosition() :
prevFocusSpan.findLastPartiallyVisibleItemPosition());
if (unfocusableCandidate != null && unfocusableCandidate != directChild) {
return unfocusableCandidate;
}
}
if (preferLastSpan(layoutDir)) {
for (int i = mSpanCount - 1; i >= 0; i--) {
if (i == prevFocusSpan.mIndex) {
continue;
}
unfocusableCandidate = findViewByPosition(shouldSearchFromStart
? mSpans[i].findFirstPartiallyVisibleItemPosition() :
mSpans[i].findLastPartiallyVisibleItemPosition());
if (unfocusableCandidate != null && unfocusableCandidate != directChild) {
return unfocusableCandidate;
}
}
} else {
for (int i = 0; i < mSpanCount; i++) {
unfocusableCandidate = findViewByPosition(shouldSearchFromStart
? mSpans[i].findFirstPartiallyVisibleItemPosition() :
mSpans[i].findLastPartiallyVisibleItemPosition());
if (unfocusableCandidate != null && unfocusableCandidate != directChild) {
return unfocusableCandidate;
}
}
}
return null;
}
/**
* Converts a focusDirection to orientation.
*
* @param focusDirection One of {@link View#FOCUS_UP}, {@link View#FOCUS_DOWN},
* {@link View#FOCUS_LEFT}, {@link View#FOCUS_RIGHT},
* {@link View#FOCUS_BACKWARD}, {@link View#FOCUS_FORWARD}
* or 0 for not applicable
* @return {@link LayoutState#LAYOUT_START} or {@link LayoutState#LAYOUT_END} if focus direction
* is applicable to current state, {@link LayoutState#INVALID_LAYOUT} otherwise.
*/
private int convertFocusDirectionToLayoutDirection(int focusDirection) {
switch (focusDirection) {
case View.FOCUS_BACKWARD:
if (mOrientation == VERTICAL) {
return LayoutState.LAYOUT_START;
} else if (isLayoutRTL()) {
return LayoutState.LAYOUT_END;
} else {
return LayoutState.LAYOUT_START;
}
case View.FOCUS_FORWARD:
if (mOrientation == VERTICAL) {
return LayoutState.LAYOUT_END;
} else if (isLayoutRTL()) {
return LayoutState.LAYOUT_START;
} else {
return LayoutState.LAYOUT_END;
}
case View.FOCUS_UP:
return mOrientation == VERTICAL ? LayoutState.LAYOUT_START
: LayoutState.INVALID_LAYOUT;
case View.FOCUS_DOWN:
return mOrientation == VERTICAL ? LayoutState.LAYOUT_END
: LayoutState.INVALID_LAYOUT;
case View.FOCUS_LEFT:
return mOrientation == HORIZONTAL ? LayoutState.LAYOUT_START
: LayoutState.INVALID_LAYOUT;
case View.FOCUS_RIGHT:
return mOrientation == HORIZONTAL ? LayoutState.LAYOUT_END
: LayoutState.INVALID_LAYOUT;
default:
if (DEBUG) {
Log.d(TAG, "Unknown focus request:" + focusDirection);
}
return LayoutState.INVALID_LAYOUT;
}
}
/**
* LayoutParams used by StaggeredGridLayoutManager.
* <p>
* Note that if the orientation is {@link #VERTICAL}, the width parameter is ignored and if the
* orientation is {@link #HORIZONTAL} the height parameter is ignored because child view is
* expected to fill all of the space given to it.
*/
public static class LayoutParams extends RecyclerView.LayoutParams {
/**
* Span Id for Views that are not laid out yet.
*/
public static final int INVALID_SPAN_ID = -1;
// Package scope to be able to access from tests.
Span mSpan;
boolean mFullSpan;
public LayoutParams(Context c, AttributeSet attrs) {
super(c, attrs);
}
public LayoutParams(int width, int height) {
super(width, height);
}
public LayoutParams(ViewGroup.MarginLayoutParams source) {
super(source);
}
public LayoutParams(ViewGroup.LayoutParams source) {
super(source);
}
public LayoutParams(RecyclerView.LayoutParams source) {
super(source);
}
/**
* When set to true, the item will layout using all span area. That means, if orientation
* is vertical, the view will have full width; if orientation is horizontal, the view will
* have full height.
*
* @param fullSpan True if this item should traverse all spans.
* @see #isFullSpan()
*/
public void setFullSpan(boolean fullSpan) {
mFullSpan = fullSpan;
}
/**
* Returns whether this View occupies all available spans or just one.
*
* @return True if the View occupies all spans or false otherwise.
* @see #setFullSpan(boolean)
*/
public boolean isFullSpan() {
return mFullSpan;
}
/**
* Returns the Span index to which this View is assigned.
*
* @return The Span index of the View. If View is not yet assigned to any span, returns
* {@link #INVALID_SPAN_ID}.
*/
public final int getSpanIndex() {
if (mSpan == null) {
return INVALID_SPAN_ID;
}
return mSpan.mIndex;
}
}
// Package scoped to access from tests.
class Span {
static final int INVALID_LINE = Integer.MIN_VALUE;
ArrayList<View> mViews = new ArrayList<>();
int mCachedStart = INVALID_LINE;
int mCachedEnd = INVALID_LINE;
int mDeletedSize = 0;
final int mIndex;
Span(int index) {
mIndex = index;
}
int getStartLine(int def) {
if (mCachedStart != INVALID_LINE) {
return mCachedStart;
}
if (mViews.size() == 0) {
return def;
}
calculateCachedStart();
return mCachedStart;
}
void calculateCachedStart() {
final View startView = mViews.get(0);
final LayoutParams lp = getLayoutParams(startView);
mCachedStart = mPrimaryOrientation.getDecoratedStart(startView);
if (lp.mFullSpan) {
LazySpanLookup.FullSpanItem fsi = mLazySpanLookup
.getFullSpanItem(lp.getViewLayoutPosition());
if (fsi != null && fsi.mGapDir == LayoutState.LAYOUT_START) {
mCachedStart -= fsi.getGapForSpan(mIndex);
}
}
}
// Use this one when default value does not make sense and not having a value means a bug.
int getStartLine() {
if (mCachedStart != INVALID_LINE) {
return mCachedStart;
}
calculateCachedStart();
return mCachedStart;
}
int getEndLine(int def) {
if (mCachedEnd != INVALID_LINE) {
return mCachedEnd;
}
final int size = mViews.size();
if (size == 0) {
return def;
}
calculateCachedEnd();
return mCachedEnd;
}
void calculateCachedEnd() {
final View endView = mViews.get(mViews.size() - 1);
final LayoutParams lp = getLayoutParams(endView);
mCachedEnd = mPrimaryOrientation.getDecoratedEnd(endView);
if (lp.mFullSpan) {
LazySpanLookup.FullSpanItem fsi = mLazySpanLookup
.getFullSpanItem(lp.getViewLayoutPosition());
if (fsi != null && fsi.mGapDir == LayoutState.LAYOUT_END) {
mCachedEnd += fsi.getGapForSpan(mIndex);
}
}
}
// Use this one when default value does not make sense and not having a value means a bug.
int getEndLine() {
if (mCachedEnd != INVALID_LINE) {
return mCachedEnd;
}
calculateCachedEnd();
return mCachedEnd;
}
void prependToSpan(View view) {
LayoutParams lp = getLayoutParams(view);
lp.mSpan = this;
mViews.add(0, view);
mCachedStart = INVALID_LINE;
if (mViews.size() == 1) {
mCachedEnd = INVALID_LINE;
}
if (lp.isItemRemoved() || lp.isItemChanged()) {
mDeletedSize += mPrimaryOrientation.getDecoratedMeasurement(view);
}
}
void appendToSpan(View view) {
LayoutParams lp = getLayoutParams(view);
lp.mSpan = this;
mViews.add(view);
mCachedEnd = INVALID_LINE;
if (mViews.size() == 1) {
mCachedStart = INVALID_LINE;
}
if (lp.isItemRemoved() || lp.isItemChanged()) {
mDeletedSize += mPrimaryOrientation.getDecoratedMeasurement(view);
}
}
// Useful method to preserve positions on a re-layout.
void cacheReferenceLineAndClear(boolean reverseLayout, int offset) {
int reference;
if (reverseLayout) {
reference = getEndLine(INVALID_LINE);
} else {
reference = getStartLine(INVALID_LINE);
}
clear();
if (reference == INVALID_LINE) {
return;
}
if ((reverseLayout && reference < mPrimaryOrientation.getEndAfterPadding())
|| (!reverseLayout && reference > mPrimaryOrientation.getStartAfterPadding())) {
return;
}
if (offset != INVALID_OFFSET) {
reference += offset;
}
mCachedStart = mCachedEnd = reference;
}
void clear() {
mViews.clear();
invalidateCache();
mDeletedSize = 0;
}
void invalidateCache() {
mCachedStart = INVALID_LINE;
mCachedEnd = INVALID_LINE;
}
void setLine(int line) {
mCachedEnd = mCachedStart = line;
}
void popEnd() {
final int size = mViews.size();
View end = mViews.remove(size - 1);
final LayoutParams lp = getLayoutParams(end);
lp.mSpan = null;
if (lp.isItemRemoved() || lp.isItemChanged()) {
mDeletedSize -= mPrimaryOrientation.getDecoratedMeasurement(end);
}
if (size == 1) {
mCachedStart = INVALID_LINE;
}
mCachedEnd = INVALID_LINE;
}
void popStart() {
View start = mViews.remove(0);
final LayoutParams lp = getLayoutParams(start);
lp.mSpan = null;
if (mViews.size() == 0) {
mCachedEnd = INVALID_LINE;
}
if (lp.isItemRemoved() || lp.isItemChanged()) {
mDeletedSize -= mPrimaryOrientation.getDecoratedMeasurement(start);
}
mCachedStart = INVALID_LINE;
}
public int getDeletedSize() {
return mDeletedSize;
}
LayoutParams getLayoutParams(View view) {
return (LayoutParams) view.getLayoutParams();
}
void onOffset(int dt) {
if (mCachedStart != INVALID_LINE) {
mCachedStart += dt;
}
if (mCachedEnd != INVALID_LINE) {
mCachedEnd += dt;
}
}
public int findFirstVisibleItemPosition() {
return mReverseLayout
? findOneVisibleChild(mViews.size() - 1, -1, false)
: findOneVisibleChild(0, mViews.size(), false);
}
public int findFirstPartiallyVisibleItemPosition() {
return mReverseLayout
? findOnePartiallyVisibleChild(mViews.size() - 1, -1, true)
: findOnePartiallyVisibleChild(0, mViews.size(), true);
}
public int findFirstCompletelyVisibleItemPosition() {
return mReverseLayout
? findOneVisibleChild(mViews.size() - 1, -1, true)
: findOneVisibleChild(0, mViews.size(), true);
}
public int findLastVisibleItemPosition() {
return mReverseLayout
? findOneVisibleChild(0, mViews.size(), false)
: findOneVisibleChild(mViews.size() - 1, -1, false);
}
public int findLastPartiallyVisibleItemPosition() {
return mReverseLayout
? findOnePartiallyVisibleChild(0, mViews.size(), true)
: findOnePartiallyVisibleChild(mViews.size() - 1, -1, true);
}
public int findLastCompletelyVisibleItemPosition() {
return mReverseLayout
? findOneVisibleChild(0, mViews.size(), true)
: findOneVisibleChild(mViews.size() - 1, -1, true);
}
/**
* Returns the first view within this span that is partially or fully visible. Partially
* visible refers to a view that overlaps but is not fully contained within RV's padded
* bounded area. This view returned can be defined to have an area of overlap strictly
* greater than zero if acceptEndPointInclusion is false. If true, the view's endpoint
* inclusion is enough to consider it partially visible. The latter case can then refer to
* an out-of-bounds view positioned right at the top (or bottom) boundaries of RV's padded
* area. This is used e.g. inside
* {@link #onFocusSearchFailed(View, int, RecyclerView.Recycler, RecyclerView.State)} for
* calculating the next unfocusable child to become visible on the screen.
* @param fromIndex The child position index to start the search from.
* @param toIndex The child position index to end the search at.
* @param completelyVisible True if we have to only consider completely visible views,
* false otherwise.
* @param acceptCompletelyVisible True if we can consider both partially or fully visible
* views, false, if only a partially visible child should be
* returned.
* @param acceptEndPointInclusion If the view's endpoint intersection with RV's padded
* bounded area is enough to consider it partially visible,
* false otherwise
* @return The adapter position of the first view that's either partially or fully visible.
* {@link RecyclerView#NO_POSITION} if no such view is found.
*/
int findOnePartiallyOrCompletelyVisibleChild(int fromIndex, int toIndex,
boolean completelyVisible,
boolean acceptCompletelyVisible,
boolean acceptEndPointInclusion) {
final int start = mPrimaryOrientation.getStartAfterPadding();
final int end = mPrimaryOrientation.getEndAfterPadding();
final int next = toIndex > fromIndex ? 1 : -1;
for (int i = fromIndex; i != toIndex; i += next) {
final View child = mViews.get(i);
final int childStart = mPrimaryOrientation.getDecoratedStart(child);
final int childEnd = mPrimaryOrientation.getDecoratedEnd(child);
boolean childStartInclusion = acceptEndPointInclusion ? (childStart <= end)
: (childStart < end);
boolean childEndInclusion = acceptEndPointInclusion ? (childEnd >= start)
: (childEnd > start);
if (childStartInclusion && childEndInclusion) {
if (completelyVisible && acceptCompletelyVisible) {
// the child has to be completely visible to be returned.
if (childStart >= start && childEnd <= end) {
return getPosition(child);
}
} else if (acceptCompletelyVisible) {
// can return either a partially or completely visible child.
return getPosition(child);
} else if (childStart < start || childEnd > end) {
// should return a partially visible child if exists and a completely
// visible child is not acceptable in this case.
return getPosition(child);
}
}
}
return RecyclerView.NO_POSITION;
}
int findOneVisibleChild(int fromIndex, int toIndex, boolean completelyVisible) {
return findOnePartiallyOrCompletelyVisibleChild(fromIndex, toIndex, completelyVisible,
true, false);
}
int findOnePartiallyVisibleChild(int fromIndex, int toIndex,
boolean acceptEndPointInclusion) {
return findOnePartiallyOrCompletelyVisibleChild(fromIndex, toIndex, false, false,
acceptEndPointInclusion);
}
/**
* Depending on the layout direction, returns the View that is after the given position.
*/
public View getFocusableViewAfter(int referenceChildPosition, int layoutDir) {
View candidate = null;
if (layoutDir == LayoutState.LAYOUT_START) {
final int limit = mViews.size();
for (int i = 0; i < limit; i++) {
final View view = mViews.get(i);
if ((mReverseLayout && getPosition(view) <= referenceChildPosition)
|| (!mReverseLayout && getPosition(view) >= referenceChildPosition)) {
break;
}
if (view.hasFocusable()) {
candidate = view;
} else {
break;
}
}
} else {
for (int i = mViews.size() - 1; i >= 0; i--) {
final View view = mViews.get(i);
if ((mReverseLayout && getPosition(view) >= referenceChildPosition)
|| (!mReverseLayout && getPosition(view) <= referenceChildPosition)) {
break;
}
if (view.hasFocusable()) {
candidate = view;
} else {
break;
}
}
}
return candidate;
}
}
/**
* An array of mappings from adapter position to span.
* This only grows when a write happens and it grows up to the size of the adapter.
*/
static class LazySpanLookup {
private static final int MIN_SIZE = 10;
int[] mData;
List<FullSpanItem> mFullSpanItems;
/**
* Invalidates everything after this position, including full span information
*/
int forceInvalidateAfter(int position) {
if (mFullSpanItems != null) {
for (int i = mFullSpanItems.size() - 1; i >= 0; i--) {
FullSpanItem fsi = mFullSpanItems.get(i);
if (fsi.mPosition >= position) {
mFullSpanItems.remove(i);
}
}
}
return invalidateAfter(position);
}
/**
* returns end position for invalidation.
*/
int invalidateAfter(int position) {
if (mData == null) {
return RecyclerView.NO_POSITION;
}
if (position >= mData.length) {
return RecyclerView.NO_POSITION;
}
int endPosition = invalidateFullSpansAfter(position);
if (endPosition == RecyclerView.NO_POSITION) {
Arrays.fill(mData, position, mData.length, LayoutParams.INVALID_SPAN_ID);
return mData.length;
} else {
// Just invalidate items in between `position` and the next full span item, or the
// end of the tracked spans in mData if it's not been lengthened yet.
final int invalidateToIndex = Math.min(endPosition + 1, mData.length);
Arrays.fill(mData, position, invalidateToIndex, LayoutParams.INVALID_SPAN_ID);
return invalidateToIndex;
}
}
int getSpan(int position) {
if (mData == null || position >= mData.length) {
return LayoutParams.INVALID_SPAN_ID;
} else {
return mData[position];
}
}
void setSpan(int position, Span span) {
ensureSize(position);
mData[position] = span.mIndex;
}
int sizeForPosition(int position) {
int len = mData.length;
while (len <= position) {
len *= 2;
}
return len;
}
void ensureSize(int position) {
if (mData == null) {
mData = new int[Math.max(position, MIN_SIZE) + 1];
Arrays.fill(mData, LayoutParams.INVALID_SPAN_ID);
} else if (position >= mData.length) {
int[] old = mData;
mData = new int[sizeForPosition(position)];
System.arraycopy(old, 0, mData, 0, old.length);
Arrays.fill(mData, old.length, mData.length, LayoutParams.INVALID_SPAN_ID);
}
}
void clear() {
if (mData != null) {
Arrays.fill(mData, LayoutParams.INVALID_SPAN_ID);
}
mFullSpanItems = null;
}
void offsetForRemoval(int positionStart, int itemCount) {
if (mData == null || positionStart >= mData.length) {
return;
}
ensureSize(positionStart + itemCount);
System.arraycopy(mData, positionStart + itemCount, mData, positionStart,
mData.length - positionStart - itemCount);
Arrays.fill(mData, mData.length - itemCount, mData.length,
LayoutParams.INVALID_SPAN_ID);
offsetFullSpansForRemoval(positionStart, itemCount);
}
private void offsetFullSpansForRemoval(int positionStart, int itemCount) {
if (mFullSpanItems == null) {
return;
}
final int end = positionStart + itemCount;
for (int i = mFullSpanItems.size() - 1; i >= 0; i--) {
FullSpanItem fsi = mFullSpanItems.get(i);
if (fsi.mPosition < positionStart) {
continue;
}
if (fsi.mPosition < end) {
mFullSpanItems.remove(i);
} else {
fsi.mPosition -= itemCount;
}
}
}
void offsetForAddition(int positionStart, int itemCount) {
if (mData == null || positionStart >= mData.length) {
return;
}
ensureSize(positionStart + itemCount);
System.arraycopy(mData, positionStart, mData, positionStart + itemCount,
mData.length - positionStart - itemCount);
Arrays.fill(mData, positionStart, positionStart + itemCount,
LayoutParams.INVALID_SPAN_ID);
offsetFullSpansForAddition(positionStart, itemCount);
}
private void offsetFullSpansForAddition(int positionStart, int itemCount) {
if (mFullSpanItems == null) {
return;
}
for (int i = mFullSpanItems.size() - 1; i >= 0; i--) {
FullSpanItem fsi = mFullSpanItems.get(i);
if (fsi.mPosition < positionStart) {
continue;
}
fsi.mPosition += itemCount;
}
}
/**
* Returns when invalidation should end. e.g. hitting a full span position.
* Returned position SHOULD BE invalidated.
*/
private int invalidateFullSpansAfter(int position) {
if (mFullSpanItems == null) {
return RecyclerView.NO_POSITION;
}
final FullSpanItem item = getFullSpanItem(position);
// if there is an fsi at this position, get rid of it.
if (item != null) {
mFullSpanItems.remove(item);
}
int nextFsiIndex = -1;
final int count = mFullSpanItems.size();
for (int i = 0; i < count; i++) {
FullSpanItem fsi = mFullSpanItems.get(i);
if (fsi.mPosition >= position) {
nextFsiIndex = i;
break;
}
}
if (nextFsiIndex != -1) {
FullSpanItem fsi = mFullSpanItems.get(nextFsiIndex);
mFullSpanItems.remove(nextFsiIndex);
return fsi.mPosition;
}
return RecyclerView.NO_POSITION;
}
public void addFullSpanItem(FullSpanItem fullSpanItem) {
if (mFullSpanItems == null) {
mFullSpanItems = new ArrayList<>();
}
final int size = mFullSpanItems.size();
for (int i = 0; i < size; i++) {
FullSpanItem other = mFullSpanItems.get(i);
if (other.mPosition == fullSpanItem.mPosition) {
if (DEBUG) {
throw new IllegalStateException("two fsis for same position");
} else {
mFullSpanItems.remove(i);
}
}
if (other.mPosition >= fullSpanItem.mPosition) {
mFullSpanItems.add(i, fullSpanItem);
return;
}
}
// if it is not added to a position.
mFullSpanItems.add(fullSpanItem);
}
public FullSpanItem getFullSpanItem(int position) {
if (mFullSpanItems == null) {
return null;
}
for (int i = mFullSpanItems.size() - 1; i >= 0; i--) {
final FullSpanItem fsi = mFullSpanItems.get(i);
if (fsi.mPosition == position) {
return fsi;
}
}
return null;
}
/**
* @param minPos inclusive
* @param maxPos exclusive
* @param gapDir if not 0, returns FSIs on in that direction
* @param hasUnwantedGapAfter If true, when full span item has unwanted gaps, it will be
* returned even if its gap direction does not match.
*/
public FullSpanItem getFirstFullSpanItemInRange(int minPos, int maxPos, int gapDir,
boolean hasUnwantedGapAfter) {
if (mFullSpanItems == null) {
return null;
}
final int limit = mFullSpanItems.size();
for (int i = 0; i < limit; i++) {
FullSpanItem fsi = mFullSpanItems.get(i);
if (fsi.mPosition >= maxPos) {
return null;
}
if (fsi.mPosition >= minPos
&& (gapDir == 0 || fsi.mGapDir == gapDir
|| (hasUnwantedGapAfter && fsi.mHasUnwantedGapAfter))) {
return fsi;
}
}
return null;
}
/**
* We keep information about full span items because they may create gaps in the UI.
*/
@SuppressLint("BanParcelableUsage")
static class FullSpanItem implements Parcelable {
int mPosition;
int mGapDir;
int[] mGapPerSpan;
// A full span may be laid out in primary direction but may have gaps due to
// invalidation of views after it. This is recorded during a reverse scroll and if
// view is still on the screen after scroll stops, we have to recalculate layout
boolean mHasUnwantedGapAfter;
FullSpanItem(Parcel in) {
mPosition = in.readInt();
mGapDir = in.readInt();
mHasUnwantedGapAfter = in.readInt() == 1;
int spanCount = in.readInt();
if (spanCount > 0) {
mGapPerSpan = new int[spanCount];
in.readIntArray(mGapPerSpan);
}
}
FullSpanItem() {
}
int getGapForSpan(int spanIndex) {
return mGapPerSpan == null ? 0 : mGapPerSpan[spanIndex];
}
@Override
public int describeContents() {
return 0;
}
@Override
public void writeToParcel(Parcel dest, int flags) {
dest.writeInt(mPosition);
dest.writeInt(mGapDir);
dest.writeInt(mHasUnwantedGapAfter ? 1 : 0);
if (mGapPerSpan != null && mGapPerSpan.length > 0) {
dest.writeInt(mGapPerSpan.length);
dest.writeIntArray(mGapPerSpan);
} else {
dest.writeInt(0);
}
}
@Override
public String toString() {
return "FullSpanItem{"
+ "mPosition=" + mPosition
+ ", mGapDir=" + mGapDir
+ ", mHasUnwantedGapAfter=" + mHasUnwantedGapAfter
+ ", mGapPerSpan=" + Arrays.toString(mGapPerSpan)
+ '}';
}
public static final Parcelable.Creator<FullSpanItem> CREATOR =
new Parcelable.Creator<FullSpanItem>() {
@Override
public FullSpanItem createFromParcel(Parcel in) {
return new FullSpanItem(in);
}
@Override
public FullSpanItem[] newArray(int size) {
return new FullSpanItem[size];
}
};
}
}
/**
*/
@RestrictTo(LIBRARY)
@SuppressLint("BanParcelableUsage")
public static class SavedState implements Parcelable {
int mAnchorPosition;
int mVisibleAnchorPosition; // Replacement for span info when spans are invalidated
int mSpanOffsetsSize;
int[] mSpanOffsets;
int mSpanLookupSize;
int[] mSpanLookup;
List<LazySpanLookup.FullSpanItem> mFullSpanItems;
boolean mReverseLayout;
boolean mAnchorLayoutFromEnd;
boolean mLastLayoutRTL;
public SavedState() {
}
SavedState(Parcel in) {
mAnchorPosition = in.readInt();
mVisibleAnchorPosition = in.readInt();
mSpanOffsetsSize = in.readInt();
if (mSpanOffsetsSize > 0) {
mSpanOffsets = new int[mSpanOffsetsSize];
in.readIntArray(mSpanOffsets);
}
mSpanLookupSize = in.readInt();
if (mSpanLookupSize > 0) {
mSpanLookup = new int[mSpanLookupSize];
in.readIntArray(mSpanLookup);
}
mReverseLayout = in.readInt() == 1;
mAnchorLayoutFromEnd = in.readInt() == 1;
mLastLayoutRTL = in.readInt() == 1;
@SuppressWarnings("unchecked")
List<LazySpanLookup.FullSpanItem> fullSpanItems =
in.readArrayList(LazySpanLookup.FullSpanItem.class.getClassLoader());
mFullSpanItems = fullSpanItems;
}
public SavedState(SavedState other) {
mSpanOffsetsSize = other.mSpanOffsetsSize;
mAnchorPosition = other.mAnchorPosition;
mVisibleAnchorPosition = other.mVisibleAnchorPosition;
mSpanOffsets = other.mSpanOffsets;
mSpanLookupSize = other.mSpanLookupSize;
mSpanLookup = other.mSpanLookup;
mReverseLayout = other.mReverseLayout;
mAnchorLayoutFromEnd = other.mAnchorLayoutFromEnd;
mLastLayoutRTL = other.mLastLayoutRTL;
mFullSpanItems = other.mFullSpanItems;
}
void invalidateSpanInfo() {
mSpanOffsets = null;
mSpanOffsetsSize = 0;
mSpanLookupSize = 0;
mSpanLookup = null;
mFullSpanItems = null;
}
void invalidateAnchorPositionInfo() {
mSpanOffsets = null;
mSpanOffsetsSize = 0;
mAnchorPosition = RecyclerView.NO_POSITION;
mVisibleAnchorPosition = RecyclerView.NO_POSITION;
}
@Override
public int describeContents() {
return 0;
}
@Override
public void writeToParcel(Parcel dest, int flags) {
dest.writeInt(mAnchorPosition);
dest.writeInt(mVisibleAnchorPosition);
dest.writeInt(mSpanOffsetsSize);
if (mSpanOffsetsSize > 0) {
dest.writeIntArray(mSpanOffsets);
}
dest.writeInt(mSpanLookupSize);
if (mSpanLookupSize > 0) {
dest.writeIntArray(mSpanLookup);
}
dest.writeInt(mReverseLayout ? 1 : 0);
dest.writeInt(mAnchorLayoutFromEnd ? 1 : 0);
dest.writeInt(mLastLayoutRTL ? 1 : 0);
dest.writeList(mFullSpanItems);
}
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];
}
};
}
/**
* Data class to hold the information about an anchor position which is used in onLayout call.
*/
class AnchorInfo {
int mPosition;
int mOffset;
boolean mLayoutFromEnd;
boolean mInvalidateOffsets;
boolean mValid;
// this is where we save span reference lines in case we need to re-use them for multi-pass
// measure steps
int[] mSpanReferenceLines;
AnchorInfo() {
reset();
}
void reset() {
mPosition = RecyclerView.NO_POSITION;
mOffset = INVALID_OFFSET;
mLayoutFromEnd = false;
mInvalidateOffsets = false;
mValid = false;
if (mSpanReferenceLines != null) {
Arrays.fill(mSpanReferenceLines, -1);
}
}
void saveSpanReferenceLines(Span[] spans) {
int spanCount = spans.length;
if (mSpanReferenceLines == null || mSpanReferenceLines.length < spanCount) {
mSpanReferenceLines = new int[mSpans.length];
}
for (int i = 0; i < spanCount; i++) {
// does not matter start or end since this is only recorded when span is reset
mSpanReferenceLines[i] = spans[i].getStartLine(Span.INVALID_LINE);
}
}
void assignCoordinateFromPadding() {
mOffset = mLayoutFromEnd ? mPrimaryOrientation.getEndAfterPadding()
: mPrimaryOrientation.getStartAfterPadding();
}
void assignCoordinateFromPadding(int addedDistance) {
if (mLayoutFromEnd) {
mOffset = mPrimaryOrientation.getEndAfterPadding() - addedDistance;
} else {
mOffset = mPrimaryOrientation.getStartAfterPadding() + addedDistance;
}
}
}
}