public final class

GridLayoutManager

extends RecyclerView.LayoutManager

 java.lang.Object

androidx.recyclerview.widget.RecyclerView.LayoutManager

↳androidx.leanback.widget.GridLayoutManager

Gradle dependencies

compile group: 'androidx.leanback', name: 'leanback-grid', version: '1.0.0-alpha03'

  • groupId: androidx.leanback
  • artifactId: leanback-grid
  • version: 1.0.0-alpha03

Artifact androidx.leanback:leanback-grid:1.0.0-alpha03 it located at Google repository (https://maven.google.com/)

Androidx class mapping:

androidx.leanback.widget.GridLayoutManager android.support.v17.leanback.widget.GridLayoutManager

Overview

A implementation that lays out items in a grid for leanback VerticalGridView and HorizontalGridView.

Summary

Constructors
publicGridLayoutManager()

Methods
public booleancanScrollHorizontally()

Query if horizontal scrolling is currently supported.

public booleancanScrollVertically()

Query if vertical scrolling is currently supported.

public booleancheckLayoutParams(RecyclerView.LayoutParams lp)

Determines the validity of the supplied LayoutParams object.

public voidcollectAdjacentPrefetchPositions(int dx, int dy, RecyclerView.State state, RecyclerView.LayoutManager.LayoutPrefetchRegistry layoutPrefetchRegistry)

Gather all positions from the LayoutManager to be prefetched, given specified momentum.

public voidcollectInitialPrefetchPositions(int adapterItemCount, RecyclerView.LayoutManager.LayoutPrefetchRegistry layoutPrefetchRegistry)

Gather all positions from the LayoutManager to be prefetched in preperation for its RecyclerView to come on screen, due to the movement of another, containing RecyclerView.

public RecyclerView.LayoutParamsgenerateDefaultLayoutParams()

public RecyclerView.LayoutParamsgenerateLayoutParams(Context context, AttributeSet attrs)

public RecyclerView.LayoutParamsgenerateLayoutParams(ViewGroup.LayoutParams lp)

public intgetColumnCountForAccessibility(RecyclerView.Recycler recycler, RecyclerView.State state)

Returns the number of columns for accessibility.

public intgetDecoratedBottom(View child)

Returns the bottom edge of the given child view within its parent, offset by any applied ItemDecorations.

public voidgetDecoratedBoundsWithMargins(View view, Rect outBounds)

Returns the bounds of the view including its decoration and margins.

public intgetDecoratedLeft(View child)

Returns the left edge of the given child view within its parent, offset by any applied ItemDecorations.

public intgetDecoratedRight(View child)

Returns the right edge of the given child view within its parent, offset by any applied ItemDecorations.

public intgetDecoratedTop(View child)

Returns the top edge of the given child view within its parent, offset by any applied ItemDecorations.

public intgetRowCountForAccessibility(RecyclerView.Recycler recycler, RecyclerView.State state)

Returns the number of rows for accessibility.

public voidonAdapterChanged(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 booleanonAddFocusables(RecyclerView recyclerView, java.util.ArrayList<View> views, int direction, int focusableMode)

Called to populate focusable views within the RecyclerView.

public voidonInitializeAccessibilityNodeInfo(RecyclerView.Recycler recycler, RecyclerView.State state, AccessibilityNodeInfoCompat info)

Called by the AccessibilityDelegate when the information about the current layout should be populated.

public voidonInitializeAccessibilityNodeInfoForItem(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 ViewonInterceptFocusSearch(View focused, int direction)

This method gives a LayoutManager an opportunity to intercept the initial focus search before the default behavior of is used.

public voidonItemsAdded(RecyclerView recyclerView, int positionStart, int itemCount)

Called when items have been added to the adapter.

public voidonItemsChanged(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 voidonItemsMoved(RecyclerView recyclerView, int from, int to, int itemCount)

Called when an item is moved withing the adapter.

public voidonItemsRemoved(RecyclerView recyclerView, int positionStart, int itemCount)

Called when items have been removed from the adapter.

public voidonItemsUpdated(RecyclerView recyclerView, int positionStart, int itemCount)

Called when items have been changed in the adapter.

public voidonLayoutChildren(RecyclerView.Recycler recycler, RecyclerView.State state)

Lay out all relevant child views from the given adapter.

public voidonLayoutCompleted(RecyclerView.State state)

Called after a full layout calculation is finished.

public voidonMeasure(RecyclerView.Recycler recycler, RecyclerView.State state, int widthSpec, int heightSpec)

Measure the attached RecyclerView.

public booleanonRequestChildFocus(RecyclerView parent, RecyclerView.State state, View child, View focused)

Called when a descendant view of the RecyclerView requests focus.

public voidonRestoreInstanceState(Parcelable state)

Called when the RecyclerView is ready to restore the state based on a previous RecyclerView.

public ParcelableonSaveInstanceState()

Called when the LayoutManager should save its state.

public booleanperformAccessibilityAction(RecyclerView.Recycler recycler, RecyclerView.State state, int action, Bundle args)

Called by AccessibilityDelegate when an action is requested from the RecyclerView.

public voidremoveAndRecycleAllViews(RecyclerView.Recycler recycler)

Removes all views and recycles them using the given recycler.

public booleanrequestChildRectangleOnScreen(RecyclerView parent, View child, Rect rect, boolean immediate)

Called when a child of the RecyclerView wants a particular rectangle to be positioned onto the screen.

public intscrollHorizontallyBy(int dx, RecyclerView.Recycler recycler, RecyclerView.State state)

Scroll horizontally by dx pixels in screen coordinates and return the distance traveled.

public voidscrollToPosition(int position)

Scroll to the specified adapter position.

public intscrollVerticallyBy(int dy, RecyclerView.Recycler recycler, RecyclerView.State state)

Scroll vertically by dy pixels in screen coordinates and return the distance traveled.

public voidsetFocusOutAllowed(boolean throughFront, boolean throughBack)

Sets whether focus can move out from the front and/or back of the grid view.

public voidsetOrientation(int orientation)

Sets grid view orientation.

public voidsmoothScrollToPosition(RecyclerView recyclerView, RecyclerView.State state, int position)

Smooth scroll to the specified adapter position.

public voidstartSmoothScroll(RecyclerView.SmoothScroller smoothScroller)

Starts a smooth scroll using the provided RecyclerView.SmoothScroller.

public booleansupportsPredictiveItemAnimations()

Returns whether this LayoutManager supports "predictive item animations".

from RecyclerView.LayoutManageraddDisappearingView, addDisappearingView, addView, addView, assertInLayoutOrScroll, assertNotInLayoutOrScroll, attachView, attachView, attachView, calculateItemDecorationsForChild, chooseSize, computeHorizontalScrollExtent, computeHorizontalScrollOffset, computeHorizontalScrollRange, computeVerticalScrollExtent, computeVerticalScrollOffset, computeVerticalScrollRange, detachAndScrapAttachedViews, detachAndScrapView, detachAndScrapViewAt, detachView, detachViewAt, endAnimation, findContainingItemView, findViewByPosition, getBaseline, getBottomDecorationHeight, getChildAt, getChildCount, getChildMeasureSpec, getChildMeasureSpec, getClipToPadding, getDecoratedMeasuredHeight, getDecoratedMeasuredWidth, 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, isAutoMeasureEnabled, isFocused, isItemPrefetchEnabled, isLayoutHierarchical, isLayoutReversed, isMeasurementCacheEnabled, isSmoothScrolling, isViewPartiallyVisible, layoutDecorated, layoutDecoratedWithMargins, measureChild, measureChildWithMargins, moveView, offsetChildrenHorizontal, offsetChildrenVertical, onAttachedToWindow, onDetachedFromWindow, onDetachedFromWindow, onFocusSearchFailed, onInitializeAccessibilityEvent, onInitializeAccessibilityEvent, onItemsUpdated, onRequestChildFocus, onScrollStateChanged, performAccessibilityActionForItem, postOnAnimation, removeAllViews, removeAndRecycleView, removeAndRecycleViewAt, removeCallbacks, removeDetachedView, removeView, removeViewAt, requestChildRectangleOnScreen, requestLayout, requestSimpleAnimationsInNextLayout, setAutoMeasureEnabled, setItemPrefetchEnabled, setMeasuredDimension, setMeasuredDimension, setMeasurementCacheEnabled, stopIgnoringView
from java.lang.Objectclone, equals, finalize, getClass, hashCode, notify, notifyAll, toString, wait, wait, wait

Constructors

public GridLayoutManager()

Methods

public void setOrientation(int orientation)

Sets grid view orientation.

public void setFocusOutAllowed(boolean throughFront, boolean throughBack)

Sets whether focus can move out from the front and/or back of the grid view.

Parameters:

throughFront: For the vertical orientation, this controls whether focus can move out from the top of the grid. For the horizontal orientation, this controls whether focus can move out the front side of the grid.
throughBack: For the vertical orientation, this controls whether focus can move out from the bottom of the grid. For the horizontal orientation, this controls whether focus can move out the back side of the grid.

public boolean checkLayoutParams(RecyclerView.LayoutParams lp)

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 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 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 RecyclerView.LayoutParams generateDefaultLayoutParams()

public RecyclerView.LayoutParams generateLayoutParams(Context context, AttributeSet attrs)

public RecyclerView.LayoutParams generateLayoutParams(ViewGroup.LayoutParams lp)

public int getDecoratedLeft(View child)

Returns the left edge of the given child view within its parent, offset by any applied ItemDecorations.

Parameters:

child: Child to query

Returns:

Child left edge with offsets applied

See also: RecyclerView.LayoutManager.getLeftDecorationWidth(View)

public int getDecoratedTop(View child)

Returns the top edge of the given child view within its parent, offset by any applied ItemDecorations.

Parameters:

child: Child to query

Returns:

Child top edge with offsets applied

See also: RecyclerView.LayoutManager.getTopDecorationHeight(View)

public int getDecoratedRight(View child)

Returns the right edge of the given child view within its parent, offset by any applied ItemDecorations.

Parameters:

child: Child to query

Returns:

Child right edge with offsets applied

See also: RecyclerView.LayoutManager.getRightDecorationWidth(View)

public int getDecoratedBottom(View child)

Returns the bottom edge of the given child view within its parent, offset by any applied ItemDecorations.

Parameters:

child: Child to query

Returns:

Child bottom edge with offsets applied

See also: RecyclerView.LayoutManager.getBottomDecorationHeight(View)

public void getDecoratedBoundsWithMargins(View view, Rect outBounds)

Returns the bounds of the view including its decoration and margins.

Parameters:

view: The view element to check
outBounds: A rect that will receive the bounds of the element including its decoration and margins.

public void onMeasure(RecyclerView.Recycler recycler, RecyclerView.State state, int widthSpec, int heightSpec)

Measure the attached RecyclerView. Implementations must call RecyclerView.LayoutManager.setMeasuredDimension(int, int) before returning.

It is strongly advised to use the AutoMeasure mechanism by overriding RecyclerView.LayoutManager.isAutoMeasureEnabled() to return true as AutoMeasure handles all the standard measure cases including when the RecyclerView's layout_width or layout_height have been set to wrap_content. If RecyclerView.LayoutManager.isAutoMeasureEnabled() is overridden to return true, this method should not be overridden.

The default implementation will handle EXACTLY measurements and respect the minimum width and height properties of the host RecyclerView if measured as UNSPECIFIED. AT_MOST measurements will be treated as EXACTLY and the RecyclerView will consume all available space.

Parameters:

recycler: Recycler
state: Transient state of RecyclerView
widthSpec: Width
heightSpec: Height

See also: RecyclerView.LayoutManager.isAutoMeasureEnabled(), RecyclerView.LayoutManager.setMeasuredDimension(int, int)

public void removeAndRecycleAllViews(RecyclerView.Recycler recycler)

Removes all views and recycles them using the given recycler.

If you want to clean cached views as well, you should call RecyclerView.Recycler.clear() too.

If a View is marked as "ignored", it is not removed nor recycled.

Parameters:

recycler: Recycler to use to recycle children

See also: RecyclerView.LayoutManager, RecyclerView.LayoutManager, RecyclerView.LayoutManager.ignoreView(View)

public void onLayoutCompleted(RecyclerView.State state)

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 void onLayoutChildren(RecyclerView.Recycler recycler, RecyclerView.State state)

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

public int scrollHorizontallyBy(int dx, RecyclerView.Recycler recycler, RecyclerView.State state)

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.

public int scrollVerticallyBy(int dy, RecyclerView.Recycler recycler, RecyclerView.State state)

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 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.

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)

public void collectInitialPrefetchPositions(int adapterItemCount, RecyclerView.LayoutManager.LayoutPrefetchRegistry layoutPrefetchRegistry)

Gather all positions from the LayoutManager to be prefetched in preperation for its RecyclerView to come on screen, due to the movement of another, containing RecyclerView.

This method is only called when a RecyclerView is nested in another RecyclerView.

If item prefetch is enabled for this LayoutManager, as well in another containing LayoutManager, this method is called in between draw traversals to gather which positions this LayoutManager will first need, once it appears on the screen.

For example, if this LayoutManager represents a horizontally scrolling list within a vertically scrolling LayoutManager, this method would be called when the horizontal list is about to come onscreen.

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:

adapterItemCount: number of items in the associated adapter.
layoutPrefetchRegistry: PrefetchRegistry to add prefetch entries into.

See also: RecyclerView.LayoutManager.isItemPrefetchEnabled(), RecyclerView.LayoutManager

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 smoothScrollToPosition(RecyclerView recyclerView, RecyclerView.State state, int position)

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 startSmoothScroll(RecyclerView.SmoothScroller smoothScroller)

Starts a smooth scroll using the provided RecyclerView.SmoothScroller.

Each instance of SmoothScroller is intended to only be used once. Provide a new SmoothScroller instance each time this method is called.

Calling this method will cancel any previous smooth scroll request.

Parameters:

smoothScroller: Instance which defines how smooth scroll should be animated

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.)

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 onItemsRemoved(RecyclerView recyclerView, int positionStart, int itemCount)

Called when items have been removed from the adapter.

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)

Called when items have been changed in the adapter. To receive payload, override RecyclerView.LayoutManager.onItemsUpdated(RecyclerView, int, int, Object) instead, then this callback will not be invoked.

public boolean onRequestChildFocus(RecyclerView parent, RecyclerView.State state, View child, View focused)

Called when a descendant view of the RecyclerView requests focus.

A LayoutManager wishing to keep focused views aligned in a specific portion of the view may implement that behavior in an override of this method.

If the LayoutManager executes different behavior that should override the default behavior of scrolling the focused child on screen instead of running alongside it, this method should return true.

Parameters:

parent: The RecyclerView hosting this LayoutManager
state: Current state of RecyclerView
child: Direct child of the RecyclerView containing the newly focused view
focused: The newly focused view. This may be the same view as child or it may be null

Returns:

true if the default scroll behavior should be suppressed

public boolean requestChildRectangleOnScreen(RecyclerView parent, View child, Rect rect, boolean immediate)

Called when a child of the RecyclerView wants a particular rectangle to be positioned onto the screen. See for more details.

The base implementation will attempt to perform a standard programmatic scroll to bring the given rect into view, within the padded area of the RecyclerView.

Parameters:

parent: The parent RecyclerView.
child: The direct child making the request.
rect: The rectangle in the child's coordinates the child wishes to be on the screen.
immediate: True to forbid animated or delayed scrolling, false otherwise

Returns:

Whether the group scrolled to handle the operation

public View onInterceptFocusSearch(View focused, int direction)

This method gives a LayoutManager an opportunity to intercept the initial focus search before the default behavior of is used. If this method returns null FocusFinder will attempt to find a focusable child view. If it fails then RecyclerView.LayoutManager.onFocusSearchFailed(View, int, RecyclerView.Recycler, RecyclerView.State) will be called to give the LayoutManager an opportunity to add new views for items that did not have attached views representing them. The LayoutManager should not add or remove views from this method.

Parameters:

focused: The currently focused view
direction: One of View, View, View, View, View, View

Returns:

A descendant view to focus or null to fall back to default behavior. The default implementation returns null.

public boolean onAddFocusables(RecyclerView recyclerView, java.util.ArrayList<View> views, int direction, int focusableMode)

Called to populate focusable views within the RecyclerView.

The LayoutManager implementation should return true if the default behavior of ViewGroup should be suppressed.

The default implementation returns false to trigger RecyclerView to fall back to the default ViewGroup behavior.

Parameters:

recyclerView: The RecyclerView hosting this LayoutManager
views: List of output views. This method should add valid focusable views to this list.
direction: One of View, View, View, View, View, View
focusableMode: The type of focusables to be added.

Returns:

true to suppress the default behavior, false to add default focusables after this method returns.

See also: RecyclerView.LayoutManager, RecyclerView.LayoutManager

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). 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.

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

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 int getRowCountForAccessibility(RecyclerView.Recycler recycler, RecyclerView.State state)

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.

public int getColumnCountForAccessibility(RecyclerView.Recycler recycler, RecyclerView.State state)

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 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.

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 boolean performAccessibilityAction(RecyclerView.Recycler recycler, RecyclerView.State state, int action, Bundle args)

Called by AccessibilityDelegate when an action is requested from the RecyclerView.

Parameters:

recycler: The Recycler that can be used to convert view positions into adapter positions
state: The current state of RecyclerView
action: The action to perform
args: Optional action arguments

See also: View

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.

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)

Source

/*
 * Copyright 2021 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.leanback.widget;

import static androidx.recyclerview.widget.RecyclerView.HORIZONTAL;
import static androidx.recyclerview.widget.RecyclerView.NO_ID;
import static androidx.recyclerview.widget.RecyclerView.NO_POSITION;
import static androidx.recyclerview.widget.RecyclerView.SCROLL_STATE_IDLE;
import static androidx.recyclerview.widget.RecyclerView.VERTICAL;

import android.annotation.SuppressLint;
import android.content.Context;
import android.graphics.PointF;
import android.graphics.Rect;
import android.media.AudioManager;
import android.os.Build;
import android.os.Bundle;
import android.os.Parcel;
import android.os.Parcelable;
import android.util.AttributeSet;
import android.util.DisplayMetrics;
import android.util.Log;
import android.util.SparseIntArray;
import android.view.FocusFinder;
import android.view.Gravity;
import android.view.View;
import android.view.View.MeasureSpec;
import android.view.ViewGroup;
import android.view.ViewGroup.MarginLayoutParams;
import android.view.accessibility.AccessibilityEvent;
import android.view.animation.AccelerateDecelerateInterpolator;
import android.widget.GridView;

import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.annotation.VisibleForTesting;
import androidx.collection.CircularIntArray;
import androidx.core.view.ViewCompat;
import androidx.core.view.accessibility.AccessibilityNodeInfoCompat;
import androidx.recyclerview.widget.LinearSmoothScroller;
import androidx.recyclerview.widget.OrientationHelper;
import androidx.recyclerview.widget.RecyclerView;
import androidx.recyclerview.widget.RecyclerView.Recycler;
import androidx.recyclerview.widget.RecyclerView.State;

import java.io.PrintWriter;
import java.io.StringWriter;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;

/**
 * A {@link RecyclerView.LayoutManager} implementation that lays out items in a grid for leanback
 * {@link VerticalGridView} and {@link HorizontalGridView}.
 */
public final class GridLayoutManager extends RecyclerView.LayoutManager {

    /*
     * LayoutParams for {@link HorizontalGridView} and {@link VerticalGridView}.
     * The class currently does two internal jobs:
     * - Saves optical bounds insets.
     * - Caches focus align view center.
     */
    static final class LayoutParams extends RecyclerView.LayoutParams {

        // For placement
        int mLeftInset;
        int mTopInset;
        int mRightInset;
        int mBottomInset;

        // For alignment
        private int mAlignX;
        private int mAlignY;
        private int[] mAlignMultiple;
        private ItemAlignmentFacet mAlignmentFacet;

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

        LayoutParams(int width, int height) {
            super(width, height);
        }

        LayoutParams(MarginLayoutParams source) {
            super(source);
        }

        LayoutParams(ViewGroup.LayoutParams source) {
            super(source);
        }

        LayoutParams(RecyclerView.LayoutParams source) {
            super(source);
        }

        LayoutParams(LayoutParams source) {
            super(source);
        }

        int getAlignX() {
            return mAlignX;
        }

        int getAlignY() {
            return mAlignY;
        }

        int getOpticalLeft(View view) {
            return view.getLeft() + mLeftInset;
        }

        int getOpticalTop(View view) {
            return view.getTop() + mTopInset;
        }

        int getOpticalRight(View view) {
            return view.getRight() - mRightInset;
        }

        int getOpticalBottom(View view) {
            return view.getBottom() - mBottomInset;
        }

        int getOpticalWidth(View view) {
            return view.getWidth() - mLeftInset - mRightInset;
        }

        int getOpticalHeight(View view) {
            return view.getHeight() - mTopInset - mBottomInset;
        }

        int getOpticalLeftInset() {
            return mLeftInset;
        }

        int getOpticalRightInset() {
            return mRightInset;
        }

        int getOpticalTopInset() {
            return mTopInset;
        }

        int getOpticalBottomInset() {
            return mBottomInset;
        }

        void setAlignX(int alignX) {
            mAlignX = alignX;
        }

        void setAlignY(int alignY) {
            mAlignY = alignY;
        }

        void setItemAlignmentFacet(ItemAlignmentFacet facet) {
            mAlignmentFacet = facet;
        }

        ItemAlignmentFacet getItemAlignmentFacet() {
            return mAlignmentFacet;
        }

        void calculateItemAlignments(int orientation, View view) {
            ItemAlignmentFacet.ItemAlignmentDef[] defs = mAlignmentFacet.getAlignmentDefs();
            if (mAlignMultiple == null || mAlignMultiple.length != defs.length) {
                mAlignMultiple = new int[defs.length];
            }
            for (int i = 0; i < defs.length; i++) {
                mAlignMultiple[i] = ItemAlignmentFacetHelper
                        .getAlignmentPosition(view, defs[i], orientation);
            }
            if (orientation == HORIZONTAL) {
                mAlignX = mAlignMultiple[0];
            } else {
                mAlignY = mAlignMultiple[0];
            }
        }

        int[] getAlignMultiple() {
            return mAlignMultiple;
        }

        void setOpticalInsets(int leftInset, int topInset, int rightInset, int bottomInset) {
            mLeftInset = leftInset;
            mTopInset = topInset;
            mRightInset = rightInset;
            mBottomInset = bottomInset;
        }
    }

    /**
     * Base class which scrolls to selected view in onStop().
     */
    abstract class GridLinearSmoothScroller extends LinearSmoothScroller {
        boolean mSkipOnStopInternal;

        GridLinearSmoothScroller() {
            super(mBaseGridView.getContext());
        }

        @Override
        protected void onStop() {
            super.onStop();
            if (!mSkipOnStopInternal) {
                onStopInternal();
            }
            if (mCurrentSmoothScroller == this) {
                mCurrentSmoothScroller = null;
            }
            if (mPendingMoveSmoothScroller == this) {
                mPendingMoveSmoothScroller = null;
            }
        }

        protected void onStopInternal() {
            // onTargetFound() may not be called if we hit the "wall" first or get cancelled.
            View targetView = findViewByPosition(getTargetPosition());
            if (targetView == null) {
                if (getTargetPosition() >= 0) {
                    // if smooth scroller is stopped without target, immediately jumps
                    // to the target position.
                    scrollToSelection(getTargetPosition(), 0, false, 0);
                }
                return;
            }
            if (mFocusPosition != getTargetPosition()) {
                // This should not happen since we cropped value in startPositionSmoothScroller()
                mFocusPosition = getTargetPosition();
            }
            if (hasFocus()) {
                mFlag |= PF_IN_SELECTION;
                targetView.requestFocus();
                mFlag &= ~PF_IN_SELECTION;
            }
            dispatchChildSelected();
            dispatchChildSelectedAndPositioned();
        }

        @Override
        protected float calculateSpeedPerPixel(DisplayMetrics displayMetrics) {
            return super.calculateSpeedPerPixel(displayMetrics) * mSmoothScrollSpeedFactor;
        }

        @Override
        protected int calculateTimeForScrolling(int dx) {
            int ms = super.calculateTimeForScrolling(dx);
            if (mWindowAlignment.mainAxis().getSize() > 0) {
                float minMs = (float) MIN_MS_SMOOTH_SCROLL_MAIN_SCREEN
                        / mWindowAlignment.mainAxis().getSize() * dx;
                if (ms < minMs) {
                    ms = (int) minMs;
                }
            }
            return ms;
        }

        @Override
        protected void onTargetFound(View targetView,
                RecyclerView.State state, Action action) {
            if (getScrollPosition(targetView, null, sTwoInts)) {
                int dx, dy;
                if (mOrientation == HORIZONTAL) {
                    dx = sTwoInts[0];
                    dy = sTwoInts[1];
                } else {
                    dx = sTwoInts[1];
                    dy = sTwoInts[0];
                }
                final int distance = (int) Math.sqrt(dx * dx + dy * dy);
                final int time = calculateTimeForDeceleration(distance);
                action.update(dx, dy, time, mDecelerateInterpolator);
            }
        }
    }

    /**
     * The SmoothScroller that remembers pending DPAD keys and consume pending keys
     * during scroll.
     */
    final class PendingMoveSmoothScroller extends GridLinearSmoothScroller {
        // -2 is a target position that LinearSmoothScroller can never find until
        // consumePendingMovesXXX() sets real targetPosition.
        static final int TARGET_UNDEFINED = -2;
        // whether the grid is staggered.
        private final boolean mStaggeredGrid;
        // Number of pending movements on primary direction, negative if PREV_ITEM.
        private int mPendingMoves;

        PendingMoveSmoothScroller(int initialPendingMoves, boolean staggeredGrid) {
            mPendingMoves = initialPendingMoves;
            mStaggeredGrid = staggeredGrid;
            setTargetPosition(TARGET_UNDEFINED);
        }

        void increasePendingMoves() {
            if (mPendingMoves < mMaxPendingMoves) {
                mPendingMoves++;
            }
        }

        void decreasePendingMoves() {
            if (mPendingMoves > -mMaxPendingMoves) {
                mPendingMoves--;
            }
        }

        /**
         * Called before laid out an item when non-staggered grid can handle pending movements
         * by skipping "mNumRows" per movement;  staggered grid will have to wait the item
         * has been laid out in consumePendingMovesAfterLayout().
         */
        void consumePendingMovesBeforeLayout() {
            if (mStaggeredGrid || mPendingMoves == 0) {
                return;
            }
            View newSelected = null;
            int startPos = mPendingMoves > 0 ? mFocusPosition + mNumRows :
                    mFocusPosition - mNumRows;
            for (int pos = startPos; mPendingMoves != 0;
                    pos = mPendingMoves > 0 ? pos + mNumRows : pos - mNumRows) {
                View v = findViewByPosition(pos);
                if (v == null) {
                    break;
                }
                if (!canScrollTo(v)) {
                    continue;
                }
                newSelected = v;
                mFocusPosition = pos;
                mSubFocusPosition = 0;
                if (mPendingMoves > 0) {
                    mPendingMoves--;
                } else {
                    mPendingMoves++;
                }
            }
            if (newSelected != null && hasFocus()) {
                mFlag |= PF_IN_SELECTION;
                newSelected.requestFocus();
                mFlag &= ~PF_IN_SELECTION;
            }
        }

        /**
         * Called after laid out an item.  Staggered grid should find view on same
         * Row and consume pending movements.
         */
        void consumePendingMovesAfterLayout() {
            if (mStaggeredGrid && mPendingMoves != 0) {
                // consume pending moves, focus to item on the same row.
                mPendingMoves = processSelectionMoves(true, mPendingMoves);
            }
            if (mPendingMoves == 0 || (mPendingMoves > 0 && hasCreatedLastItem())
                    || (mPendingMoves < 0 && hasCreatedFirstItem())) {
                setTargetPosition(mFocusPosition);
                stop();
            }
        }

        @Override
        public PointF computeScrollVectorForPosition(int targetPosition) {
            if (mPendingMoves == 0) {
                return null;
            }
            int direction = ((mFlag & PF_REVERSE_FLOW_PRIMARY) != 0
                    ? mPendingMoves > 0 : mPendingMoves < 0)
                    ? -1 : 1;
            if (mOrientation == HORIZONTAL) {
                return new PointF(direction, 0);
            } else {
                return new PointF(0, direction);
            }
        }

        @Override
        protected void onStopInternal() {
            super.onStopInternal();
            // if we hit wall,  need clear the remaining pending moves.
            mPendingMoves = 0;
            View v = findViewByPosition(getTargetPosition());
            if (v != null) scrollToView(v, true);
        }
    }

    private static final String TAG = "GridLayoutManager";
    static final boolean DEBUG = false;

    // maximum pending movement in one direction.
    static final int DEFAULT_MAX_PENDING_MOVES = 10;
    float mSmoothScrollSpeedFactor = 1f;
    int mMaxPendingMoves = DEFAULT_MAX_PENDING_MOVES;
    // minimal milliseconds to scroll window size in major direction,  we put a cap to prevent the
    // effect smooth scrolling too over to bind an item view then drag the item view back.
    static final int MIN_MS_SMOOTH_SCROLL_MAIN_SCREEN = 30;

    String getTag() {
        return TAG + ":" + mBaseGridView.getId();
    }

    BaseGridView mBaseGridView;

    /**
     * Note on conventions in the presence of RTL layout directions:
     * Many properties and method names reference entities related to the
     * beginnings and ends of things.  In the presence of RTL flows,
     * it may not be clear whether this is intended to reference a
     * quantity that changes direction in RTL cases, or a quantity that
     * does not.  Here are the conventions in use:
     *
     * start/end: coordinate quantities - do reverse
     * (optical) left/right: coordinate quantities - do not reverse
     * low/high: coordinate quantities - do not reverse
     * min/max: coordinate quantities - do not reverse
     * scroll offset - coordinate quantities - do not reverse
     * first/last: positional indices - do not reverse
     * front/end: positional indices - do not reverse
     * prepend/append: related to positional indices - do not reverse
     *
     * Note that although quantities do not reverse in RTL flows, their
     * relationship does.  In LTR flows, the first positional index is
     * leftmost; in RTL flows, it is rightmost.  Thus, anywhere that
     * positional quantities are mapped onto coordinate quantities,
     * the flow must be checked and the logic reversed.
     */

    /**
     * The orientation of a "row".
     */
    @RecyclerView.Orientation
    int mOrientation = HORIZONTAL;
    private OrientationHelper mOrientationHelper = OrientationHelper.createHorizontalHelper(this);

    private int mSaveContextLevel;
    RecyclerView.State mState;
    // Suppose currently showing 4, 5, 6, 7; removing 2,3,4 will make the layoutPosition to be
    // 2(deleted), 3, 4, 5 in prelayout pass. So when we add item in prelayout, we must subtract 2
    // from index of Grid.createItem.
    int mPositionDeltaInPreLayout;
    // Extra layout space needs to fill in prelayout pass. Note we apply the extra space to both
    // appends and prepends due to the fact leanback is doing mario scrolling: removing items to
    // the left of focused item might need extra layout on the right.
    int mExtraLayoutSpaceInPreLayout;
    // mPositionToRowInPostLayout and mDisappearingPositions are temp variables in post layout.
    final SparseIntArray mPositionToRowInPostLayout = new SparseIntArray();
    int[] mDisappearingPositions;

    AudioManager mAudioManager;

    RecyclerView.Recycler mRecycler;

    private static final Rect sTempRect = new Rect();

    // 2 bits mask is for 3 STAGEs: 0, PF_STAGE_LAYOUT or PF_STAGE_SCROLL.
    static final int PF_STAGE_MASK = 0x3;
    static final int PF_STAGE_LAYOUT = 0x1;
    static final int PF_STAGE_SCROLL = 0x2;

    // Flag for "in fast relayout", determined by layoutInit() result.
    static final int PF_FAST_RELAYOUT = 1 << 2;

    // Flag for the selected item being updated in fast relayout.
    static final int PF_FAST_RELAYOUT_UPDATED_SELECTED_POSITION = 1 << 3;
    /**
     * During full layout pass, when GridView had focus: onLayoutChildren will
     * skip non-focusable child and adjust mFocusPosition.
     */
    static final int PF_IN_LAYOUT_SEARCH_FOCUS = 1 << 4;

    // flag to prevent reentry if it's already processing selection request.
    static final int PF_IN_SELECTION = 1 << 5;

    // Represents whether child views are temporarily sliding out
    static final int PF_SLIDING = 1 << 6;
    static final int PF_LAYOUT_EATEN_IN_SLIDING = 1 << 7;

    /**
     * Force a full layout under certain situations.  E.g. Rows change, jump to invisible child.
     */
    static final int PF_FORCE_FULL_LAYOUT = 1 << 8;

    /**
     * True if layout is enabled.
     */
    static final int PF_LAYOUT_ENABLED = 1 << 9;

    /**
     * Flag controlling whether the current/next layout should
     * be updating the secondary size of rows.
     */
    static final int PF_ROW_SECONDARY_SIZE_REFRESH = 1 << 10;

    /**
     * Allow DPAD key to navigate out at the front of the View (where position = 0),
     * default is false.
     */
    static final int PF_FOCUS_OUT_FRONT = 1 << 11;

    /**
     * Allow DPAD key to navigate out at the back of the view, default is false.
     */
    static final int PF_FOCUS_OUT_BACK = 1 << 12;

    static final int PF_FOCUS_OUT_MASKS = PF_FOCUS_OUT_FRONT | PF_FOCUS_OUT_BACK;

    /**
     * Allow DPAD key to navigate out of second axis.
     * default is true.
     */
    static final int PF_FOCUS_OUT_SIDE_START = 1 << 13;

    /**
     * Allow DPAD key to navigate out of second axis.
     */
    static final int PF_FOCUS_OUT_SIDE_END = 1 << 14;

    static final int PF_FOCUS_OUT_SIDE_MASKS = PF_FOCUS_OUT_SIDE_START | PF_FOCUS_OUT_SIDE_END;

    /**
     * True if focus search is disabled.
     */
    static final int PF_FOCUS_SEARCH_DISABLED = 1 << 15;

    /**
     * True if prune child,  might be disabled during transition.
     */
    static final int PF_PRUNE_CHILD = 1 << 16;

    /**
     * True if scroll content,  might be disabled during transition.
     */
    static final int PF_SCROLL_ENABLED = 1 << 17;

    /**
     * Set to true for RTL layout in horizontal orientation
     */
    static final int PF_REVERSE_FLOW_PRIMARY = 1 << 18;

    /**
     * Set to true for RTL layout in vertical orientation
     */
    static final int PF_REVERSE_FLOW_SECONDARY = 1 << 19;

    static final int PF_REVERSE_FLOW_MASK = PF_REVERSE_FLOW_PRIMARY | PF_REVERSE_FLOW_SECONDARY;

    int mFlag = PF_LAYOUT_ENABLED
            | PF_FOCUS_OUT_SIDE_START | PF_FOCUS_OUT_SIDE_END
            | PF_PRUNE_CHILD | PF_SCROLL_ENABLED;

    @SuppressWarnings("deprecation")
    private OnChildSelectedListener mChildSelectedListener = null;

    private ArrayList<OnChildViewHolderSelectedListener> mChildViewHolderSelectedListeners = null;

    @VisibleForTesting
    ArrayList<BaseGridView.OnLayoutCompletedListener> mOnLayoutCompletedListeners = null;

    OnChildLaidOutListener mChildLaidOutListener = null;

    /**
     * The focused position, it's not the currently visually aligned position
     * but it is the final position that we intend to focus on. If there are
     * multiple setSelection() called, mFocusPosition saves last value.
     */
    int mFocusPosition = NO_POSITION;

    /**
     * A view can have multiple alignment position,  this is the index of which
     * alignment is used,  by default is 0.
     */
    int mSubFocusPosition = 0;

    /**
     * Current running SmoothScroller.
     */
    GridLinearSmoothScroller mCurrentSmoothScroller;

    /**
     * LinearSmoothScroller that consume pending DPAD movements. Can be same object as
     * mCurrentSmoothScroller when mCurrentSmoothScroller is PendingMoveSmoothScroller.
     */
    PendingMoveSmoothScroller mPendingMoveSmoothScroller;

    /**
     * The offset to be applied to mFocusPosition, due to adapter change, on the next
     * layout.  Set to Integer.MIN_VALUE means we should stop adding delta to mFocusPosition
     * until next layout cycle.
     * TODO:  This is somewhat duplication of RecyclerView getOldPosition() which is
     * unfortunately cleared after prelayout.
     */
    private int mFocusPositionOffset = 0;

    /**
     * Extra pixels applied on primary direction.
     */
    private int mPrimaryScrollExtra;

    /**
     * override child visibility
     */
    @Visibility
    int mChildVisibility;

    /**
     * Pixels that scrolled in secondary forward direction. Negative value means backward.
     * Note that we treat secondary differently than main. For the main axis, update scroll min/max
     * based on first/last item's view location. For second axis, we don't use item's view location.
     * We are using the {@link #getRowSizeSecondary(int)} plus mScrollOffsetSecondary. see
     * details in {@link #updateSecondaryScrollLimits()}.
     */
    int mScrollOffsetSecondary;

    /**
     * User-specified row height/column width.  Can be WRAP_CONTENT.
     */
    private int mRowSizeSecondaryRequested;

    /**
     * The fixed size of each grid item in the secondary direction. This corresponds to
     * the row height, equal for all rows. Grid items may have variable length
     * in the primary direction.
     */
    private int mFixedRowSizeSecondary;

    /**
     * Tracks the secondary size of each row.
     */
    private int[] mRowSizeSecondary;

    /**
     * The maximum measured size of the view.
     */
    private int mMaxSizeSecondary;

    /**
     * Margin between items.
     */
    private int mHorizontalSpacing;
    /**
     * Margin between items vertically.
     */
    private int mVerticalSpacing;
    /**
     * Margin in main direction.
     */
    private int mSpacingPrimary;
    /**
     * Margin in second direction.
     */
    private int mSpacingSecondary;
    /**
     * How to position child in secondary direction.
     */
    private int mGravity = Gravity.START | Gravity.TOP;
    /**
     * The number of rows in the grid.
     */
    int mNumRows;
    /**
     * Number of rows requested, can be 0 to be determined by parent size and
     * rowHeight.
     */
    private int mNumRowsRequested = 1;

    /**
     * Saves grid information of each view.
     */
    Grid mGrid;

    /**
     * Focus Scroll strategy.
     */
    private int mFocusScrollStrategy = BaseGridView.FOCUS_SCROLL_ALIGNED;
    /**
     * Defines how item view is aligned in the window.
     */
    final WindowAlignment mWindowAlignment = new WindowAlignment();

    /**
     * Defines how item view is aligned.
     */
    private final ItemAlignment mItemAlignment = new ItemAlignment();

    /**
     * Dimensions of the view, width or height depending on orientation.
     */
    private int mSizePrimary;

    /**
     * Pixels of extra space for layout item (outside the widget)
     */
    private int mExtraLayoutSpace;

    /**
     * Temporary variable: an int array of length=2.
     */
    static int[] sTwoInts = new int[2];

    /**
     * Temporaries used for measuring.
     */
    private final int[] mMeasuredDimension = new int[2];

    final ViewsStateBundle mChildrenStates = new ViewsStateBundle();

    /**
     * Optional interface implemented by Adapter.
     */
    private FacetProviderAdapter mFacetProviderAdapter;

    public GridLayoutManager() {
        this(null);
    }

    @SuppressLint("WrongConstant")
    GridLayoutManager(@Nullable BaseGridView baseGridView) {
        mBaseGridView = baseGridView;
        mChildVisibility = -1;
        // disable prefetch by default, prefetch causes regression on low power chipset
        setItemPrefetchEnabled(false);
    }

    void setGridView(BaseGridView baseGridView) {
        mBaseGridView = baseGridView;
        mGrid = null;
    }

    /**
     * Sets grid view orientation.
     */
    public void setOrientation(@RecyclerView.Orientation int orientation) {
        if (orientation != HORIZONTAL && orientation != VERTICAL) {
            if (DEBUG) Log.v(getTag(), "invalid orientation: " + orientation);
            return;
        }

        mOrientation = orientation;
        mOrientationHelper = OrientationHelper.createOrientationHelper(this, mOrientation);
        mWindowAlignment.setOrientation(orientation);
        mItemAlignment.setOrientation(orientation);
        mFlag |= PF_FORCE_FULL_LAYOUT;
    }

    /**
     * Sets whether focus can move out from the front and/or back of the grid view.
     *
     * @param throughFront For the vertical orientation, this controls whether focus can move out
     * from the top of the grid. For the horizontal orientation, this controls whether focus can
     * move out the front side of the grid.
     *
     * @param throughBack For the vertical orientation, this controls whether focus can move out
     * from the bottom of the grid. For the horizontal orientation, this controls whether focus can
     * move out the back side of the grid.
     */
    public void setFocusOutAllowed(boolean throughFront, boolean throughBack) {
        mFlag = (mFlag & ~PF_FOCUS_OUT_MASKS)
                | (throughFront ? PF_FOCUS_OUT_FRONT : 0)
                | (throughBack ? PF_FOCUS_OUT_BACK : 0);
    }

    void onRtlPropertiesChanged(int layoutDirection) {
        final int flags;
        if (mOrientation == HORIZONTAL) {
            flags = layoutDirection == View.LAYOUT_DIRECTION_RTL ? PF_REVERSE_FLOW_PRIMARY : 0;
        } else {
            flags = layoutDirection == View.LAYOUT_DIRECTION_RTL ? PF_REVERSE_FLOW_SECONDARY : 0;
        }
        if ((mFlag & PF_REVERSE_FLOW_MASK) == flags) {
            return;
        }
        mFlag = (mFlag & ~PF_REVERSE_FLOW_MASK) | flags;
        mFlag |= PF_FORCE_FULL_LAYOUT;
        mWindowAlignment.horizontal.setReversedFlow(layoutDirection == View.LAYOUT_DIRECTION_RTL);
    }

    int getFocusScrollStrategy() {
        return mFocusScrollStrategy;
    }

    void setFocusScrollStrategy(int focusScrollStrategy) {
        mFocusScrollStrategy = focusScrollStrategy;
    }

    void setWindowAlignment(int windowAlignment) {
        mWindowAlignment.mainAxis().setWindowAlignment(windowAlignment);
    }

    int getWindowAlignment() {
        return mWindowAlignment.mainAxis().getWindowAlignment();
    }

    void setWindowAlignmentOffset(int alignmentOffset) {
        mWindowAlignment.mainAxis().setWindowAlignmentOffset(alignmentOffset);
    }

    int getWindowAlignmentOffset() {
        return mWindowAlignment.mainAxis().getWindowAlignmentOffset();
    }

    void setWindowAlignmentOffsetPercent(float offsetPercent) {
        mWindowAlignment.mainAxis().setWindowAlignmentOffsetPercent(offsetPercent);
    }

    float getWindowAlignmentOffsetPercent() {
        return mWindowAlignment.mainAxis().getWindowAlignmentOffsetPercent();
    }

    void setItemAlignmentOffset(int alignmentOffset) {
        mItemAlignment.mainAxis().setItemAlignmentOffset(alignmentOffset);
        updateChildAlignments();
    }

    int getItemAlignmentOffset() {
        return mItemAlignment.mainAxis().getItemAlignmentOffset();
    }

    void setItemAlignmentOffsetWithPadding(boolean withPadding) {
        mItemAlignment.mainAxis().setItemAlignmentOffsetWithPadding(withPadding);
        updateChildAlignments();
    }

    boolean isItemAlignmentOffsetWithPadding() {
        return mItemAlignment.mainAxis().isItemAlignmentOffsetWithPadding();
    }

    void setItemAlignmentOffsetPercent(float offsetPercent) {
        mItemAlignment.mainAxis().setItemAlignmentOffsetPercent(offsetPercent);
        updateChildAlignments();
    }

    float getItemAlignmentOffsetPercent() {
        return mItemAlignment.mainAxis().getItemAlignmentOffsetPercent();
    }

    void setItemAlignmentViewId(int viewId) {
        mItemAlignment.mainAxis().setItemAlignmentViewId(viewId);
        updateChildAlignments();
    }

    int getItemAlignmentViewId() {
        return mItemAlignment.mainAxis().getItemAlignmentViewId();
    }

    void setFocusOutSideAllowed(boolean throughStart, boolean throughEnd) {
        mFlag = (mFlag & ~PF_FOCUS_OUT_SIDE_MASKS)
                | (throughStart ? PF_FOCUS_OUT_SIDE_START : 0)
                | (throughEnd ? PF_FOCUS_OUT_SIDE_END : 0);
    }

    void setNumRows(int numRows) {
        if (numRows < 0) throw new IllegalArgumentException();
        mNumRowsRequested = numRows;
    }

    /**
     * Set the row height. May be WRAP_CONTENT, or a size in pixels.
     */
    void setRowHeight(int height) {
        if (height >= 0 || height == ViewGroup.LayoutParams.WRAP_CONTENT) {
            mRowSizeSecondaryRequested = height;
        } else {
            throw new IllegalArgumentException("Invalid row height: " + height);
        }
    }

    void setItemSpacing(int space) {
        mVerticalSpacing = mHorizontalSpacing = space;
        mSpacingPrimary = mSpacingSecondary = space;
    }

    void setVerticalSpacing(int space) {
        if (mOrientation == VERTICAL) {
            mSpacingPrimary = mVerticalSpacing = space;
        } else {
            mSpacingSecondary = mVerticalSpacing = space;
        }
    }

    void setHorizontalSpacing(int space) {
        if (mOrientation == HORIZONTAL) {
            mSpacingPrimary = mHorizontalSpacing = space;
        } else {
            mSpacingSecondary = mHorizontalSpacing = space;
        }
    }

    int getVerticalSpacing() {
        return mVerticalSpacing;
    }

    int getHorizontalSpacing() {
        return mHorizontalSpacing;
    }

    void setGravity(int gravity) {
        mGravity = gravity;
    }

    boolean hasDoneFirstLayout() {
        return mGrid != null;
    }

    @SuppressWarnings("deprecation")
    void setOnChildSelectedListener(OnChildSelectedListener listener) {
        mChildSelectedListener = listener;
    }

    void setOnChildViewHolderSelectedListener(OnChildViewHolderSelectedListener listener) {
        if (listener == null) {
            mChildViewHolderSelectedListeners = null;
            return;
        }
        if (mChildViewHolderSelectedListeners == null) {
            mChildViewHolderSelectedListeners = new ArrayList<>();
        } else {
            mChildViewHolderSelectedListeners.clear();
        }
        mChildViewHolderSelectedListeners.add(listener);
    }

    void addOnChildViewHolderSelectedListener(OnChildViewHolderSelectedListener listener) {
        if (mChildViewHolderSelectedListeners == null) {
            mChildViewHolderSelectedListeners = new ArrayList<OnChildViewHolderSelectedListener>();
        }
        mChildViewHolderSelectedListeners.add(listener);
    }

    void removeOnChildViewHolderSelectedListener(OnChildViewHolderSelectedListener
            listener) {
        if (mChildViewHolderSelectedListeners != null) {
            mChildViewHolderSelectedListeners.remove(listener);
        }
    }

    boolean hasOnChildViewHolderSelectedListener() {
        return mChildViewHolderSelectedListeners != null
                && mChildViewHolderSelectedListeners.size() > 0;
    }

    void fireOnChildViewHolderSelected(RecyclerView parent, RecyclerView.ViewHolder child,
            int position, int subposition) {
        if (mChildViewHolderSelectedListeners == null) {
            return;
        }
        for (int i = mChildViewHolderSelectedListeners.size() - 1; i >= 0; i--) {
            mChildViewHolderSelectedListeners.get(i).onChildViewHolderSelected(parent, child,
                    position, subposition);
        }
    }

    void fireOnChildViewHolderSelectedAndPositioned(RecyclerView parent, RecyclerView.ViewHolder
            child, int position, int subposition) {
        if (mChildViewHolderSelectedListeners == null) {
            return;
        }
        for (int i = mChildViewHolderSelectedListeners.size() - 1; i >= 0; i--) {
            mChildViewHolderSelectedListeners.get(i).onChildViewHolderSelectedAndPositioned(parent,
                    child, position, subposition);
        }
    }

    void addOnLayoutCompletedListener(BaseGridView.OnLayoutCompletedListener listener) {
        if (mOnLayoutCompletedListeners == null) {
            mOnLayoutCompletedListeners = new ArrayList<>();
        }
        mOnLayoutCompletedListeners.add(listener);
    }

    void removeOnLayoutCompletedListener(BaseGridView.OnLayoutCompletedListener listener) {
        if (mOnLayoutCompletedListeners != null) {
            mOnLayoutCompletedListeners.remove(listener);
        }
    }

    void setOnChildLaidOutListener(OnChildLaidOutListener listener) {
        mChildLaidOutListener = listener;
    }

    private int getAdapterPositionByView(View view) {
        if (view == null) {
            return NO_POSITION;
        }
        LayoutParams params = (LayoutParams) view.getLayoutParams();
        if (params == null || params.isItemRemoved()) {
            // when item is removed, the position value can be any value.
            return NO_POSITION;
        }
        return params.getAbsoluteAdapterPosition();
    }

    int getSubPositionByView(View view, View childView) {
        if (view == null || childView == null) {
            return 0;
        }
        final LayoutParams lp = (LayoutParams) view.getLayoutParams();
        final ItemAlignmentFacet facet = lp.getItemAlignmentFacet();
        if (facet != null) {
            final ItemAlignmentFacet.ItemAlignmentDef[] defs = facet.getAlignmentDefs();
            if (defs.length > 1) {
                while (childView != view) {
                    int id = childView.getId();
                    if (id != View.NO_ID) {
                        for (int i = 1; i < defs.length; i++) {
                            if (defs[i].getItemAlignmentFocusViewId() == id) {
                                return i;
                            }
                        }
                    }
                    childView = (View) childView.getParent();
                }
            }
        }
        return 0;
    }

    private int getAdapterPositionByIndex(int index) {
        return getAdapterPositionByView(getChildAt(index));
    }

    void dispatchChildSelected() {
        if (mChildSelectedListener == null && !hasOnChildViewHolderSelectedListener()) {
            return;
        }

        View view = mFocusPosition == NO_POSITION ? null : findViewByPosition(mFocusPosition);
        if (view != null) {
            RecyclerView.ViewHolder vh = mBaseGridView.getChildViewHolder(view);
            if (mChildSelectedListener != null) {
                mChildSelectedListener.onChildSelected(mBaseGridView, view, mFocusPosition,
                        vh == null ? NO_ID : vh.getItemId());
            }
            fireOnChildViewHolderSelected(mBaseGridView, vh, mFocusPosition, mSubFocusPosition);
        } else {
            if (mChildSelectedListener != null) {
                mChildSelectedListener.onChildSelected(mBaseGridView, null, NO_POSITION, NO_ID);
            }
            fireOnChildViewHolderSelected(mBaseGridView, null, NO_POSITION, 0);
        }

        // Children may request layout when a child selection event occurs (such as a change of
        // padding on the current and previously selected rows).
        // If in layout, a child requesting layout may have been laid out before the selection
        // callback.
        // If it was not, the child will be laid out after the selection callback.
        // If so, the layout request will be honoured though the view system will emit a double-
        // layout warning.
        // If not in layout, we may be scrolling in which case the child layout request will be
        // eaten by recyclerview.  Post a requestLayout.
        if ((mFlag & PF_STAGE_MASK) != PF_STAGE_LAYOUT && !mBaseGridView.isLayoutRequested()) {
            int childCount = getChildCount();
            for (int i = 0; i < childCount; i++) {
                if (getChildAt(i).isLayoutRequested()) {
                    forceRequestLayout();
                    break;
                }
            }
        }
    }

    @SuppressWarnings("WeakerAccess") /* synthetic access */
    void dispatchChildSelectedAndPositioned() {
        if (!hasOnChildViewHolderSelectedListener()) {
            return;
        }

        View view = mFocusPosition == NO_POSITION ? null : findViewByPosition(mFocusPosition);
        if (view != null) {
            RecyclerView.ViewHolder vh = mBaseGridView.getChildViewHolder(view);
            fireOnChildViewHolderSelectedAndPositioned(mBaseGridView, vh, mFocusPosition,
                    mSubFocusPosition);
        } else {
            if (mChildSelectedListener != null) {
                mChildSelectedListener.onChildSelected(mBaseGridView, null, NO_POSITION, NO_ID);
            }
            fireOnChildViewHolderSelectedAndPositioned(mBaseGridView, null, NO_POSITION, 0);
        }

    }

    @Override
    public boolean checkLayoutParams(@Nullable RecyclerView.LayoutParams lp) {
        return lp instanceof LayoutParams;
    }

    @Override
    public boolean canScrollHorizontally() {
        // We can scroll horizontally if we have horizontal orientation, or if
        // we are vertical and have more than one column.
        return mOrientation == HORIZONTAL || mNumRows > 1;
    }

    @Override
    public boolean canScrollVertically() {
        // We can scroll vertically if we have vertical orientation, or if we
        // are horizontal and have more than one row.
        return mOrientation == VERTICAL || mNumRows > 1;
    }

    /**
     * {@inheritDoc}
     */
    @NonNull
    @Override
    public RecyclerView.LayoutParams generateDefaultLayoutParams() {
        return new LayoutParams(ViewGroup.LayoutParams.WRAP_CONTENT,
                ViewGroup.LayoutParams.WRAP_CONTENT);
    }

    /**
     * {@inheritDoc}
     */
    @NonNull
    @Override
    public RecyclerView.LayoutParams generateLayoutParams(@NonNull Context context,
            @NonNull AttributeSet attrs) {
        return new LayoutParams(context, attrs);
    }

    /**
     * {@inheritDoc}
     */
    @NonNull
    @Override
    public RecyclerView.LayoutParams generateLayoutParams(@NonNull ViewGroup.LayoutParams lp) {
        if (lp instanceof LayoutParams) {
            return new LayoutParams((LayoutParams) lp);
        } else if (lp instanceof RecyclerView.LayoutParams) {
            return new LayoutParams((RecyclerView.LayoutParams) lp);
        } else if (lp instanceof MarginLayoutParams) {
            return new LayoutParams((MarginLayoutParams) lp);
        } else {
            return new LayoutParams(lp);
        }
    }

    View getViewForPosition(int position) {
        View v = mRecycler.getViewForPosition(position);
        LayoutParams lp = (LayoutParams) v.getLayoutParams();
        RecyclerView.ViewHolder vh = mBaseGridView.getChildViewHolder(v);
        lp.setItemAlignmentFacet((ItemAlignmentFacet) getFacet(vh, ItemAlignmentFacet.class));
        return v;
    }

    int getOpticalLeft(View v) {
        return ((LayoutParams) v.getLayoutParams()).getOpticalLeft(v);
    }

    int getOpticalRight(View v) {
        return ((LayoutParams) v.getLayoutParams()).getOpticalRight(v);
    }

    int getOpticalTop(View v) {
        return ((LayoutParams) v.getLayoutParams()).getOpticalTop(v);
    }

    int getOpticalBottom(View v) {
        return ((LayoutParams) v.getLayoutParams()).getOpticalBottom(v);
    }

    @Override
    public int getDecoratedLeft(@NonNull View child) {
        return super.getDecoratedLeft(child) + ((LayoutParams) child.getLayoutParams()).mLeftInset;
    }

    @Override
    public int getDecoratedTop(@NonNull View child) {
        return super.getDecoratedTop(child) + ((LayoutParams) child.getLayoutParams()).mTopInset;
    }

    @Override
    public int getDecoratedRight(@NonNull View child) {
        return super.getDecoratedRight(child)
                - ((LayoutParams) child.getLayoutParams()).mRightInset;
    }

    @Override
    public int getDecoratedBottom(@NonNull View child) {
        return super.getDecoratedBottom(child)
                - ((LayoutParams) child.getLayoutParams()).mBottomInset;
    }

    @Override
    public void getDecoratedBoundsWithMargins(@NonNull View view, @NonNull Rect outBounds) {
        super.getDecoratedBoundsWithMargins(view, outBounds);
        LayoutParams params = ((LayoutParams) view.getLayoutParams());
        outBounds.left += params.mLeftInset;
        outBounds.top += params.mTopInset;
        outBounds.right -= params.mRightInset;
        outBounds.bottom -= params.mBottomInset;
    }

    int getViewMin(View v) {
        return mOrientationHelper.getDecoratedStart(v);
    }

    int getViewMax(View v) {
        return mOrientationHelper.getDecoratedEnd(v);
    }

    int getViewPrimarySize(View view) {
        getDecoratedBoundsWithMargins(view, sTempRect);
        return mOrientation == HORIZONTAL ? sTempRect.width() : sTempRect.height();
    }

    private int getViewCenter(View view) {
        return (mOrientation == HORIZONTAL) ? getViewCenterX(view) : getViewCenterY(view);
    }

    private int getViewCenterSecondary(View view) {
        return (mOrientation == HORIZONTAL) ? getViewCenterY(view) : getViewCenterX(view);
    }

    private int getViewCenterX(View v) {
        LayoutParams p = (LayoutParams) v.getLayoutParams();
        return p.getOpticalLeft(v) + p.getAlignX();
    }

    private int getViewCenterY(View v) {
        LayoutParams p = (LayoutParams) v.getLayoutParams();
        return p.getOpticalTop(v) + p.getAlignY();
    }

    AudioManager getAudioManager() {
        if (mAudioManager == null) {
            mAudioManager = (AudioManager) mBaseGridView.getContext()
                .getSystemService(Context.AUDIO_SERVICE);
        }
        return mAudioManager;
    }

    /**
     * Save Recycler and State for convenience.  Must be paired with leaveContext().
     */
    private void saveContext(Recycler recycler, State state) {
        if (mSaveContextLevel == 0) {
            mRecycler = recycler;
            mState = state;
            mPositionDeltaInPreLayout = 0;
            mExtraLayoutSpaceInPreLayout = 0;
        }
        mSaveContextLevel++;
    }

    /**
     * Discard saved Recycler and State.
     */
    private void leaveContext() {
        mSaveContextLevel--;
        if (mSaveContextLevel == 0) {
            mRecycler = null;
            mState = null;
            mPositionDeltaInPreLayout = 0;
            mExtraLayoutSpaceInPreLayout = 0;
        }
    }

    /**
     * Re-initialize data structures for a data change or handling invisible
     * selection. The method tries its best to preserve position information so
     * that staggered grid looks same before and after re-initialize.
     *
     * @return true if can fastRelayout()
     */
    private boolean layoutInit() {
        final int newItemCount = mState.getItemCount();
        if (newItemCount == 0) {
            mFocusPosition = NO_POSITION;
            mSubFocusPosition = 0;
        } else if (mFocusPosition >= newItemCount) {
            mFocusPosition = newItemCount - 1;
            mSubFocusPosition = 0;
        } else if (mFocusPosition == NO_POSITION && newItemCount > 0) {
            // if focus position is never set before,  initialize it to 0
            mFocusPosition = 0;
            mSubFocusPosition = 0;
        }
        if (!mState.didStructureChange() && mGrid != null && mGrid.getFirstVisibleIndex() >= 0
                && (mFlag & PF_FORCE_FULL_LAYOUT) == 0 && mGrid.getNumRows() == mNumRows) {
            updateScrollController();
            updateSecondaryScrollLimits();
            mGrid.setSpacing(mSpacingPrimary);
            return true;
        } else {
            mFlag &= ~PF_FORCE_FULL_LAYOUT;

            if (mGrid == null || mNumRows != mGrid.getNumRows()
                    || ((mFlag & PF_REVERSE_FLOW_PRIMARY) != 0) != mGrid.isReversedFlow()) {
                mGrid = Grid.createGrid(mNumRows);
                mGrid.setProvider(mGridProvider);
                mGrid.setReversedFlow((mFlag & PF_REVERSE_FLOW_PRIMARY) != 0);
            }
            initScrollController();
            updateSecondaryScrollLimits();
            mGrid.setSpacing(mSpacingPrimary);
            detachAndScrapAttachedViews(mRecycler);
            mGrid.resetVisibleIndex();
            mWindowAlignment.mainAxis().invalidateScrollMin();
            mWindowAlignment.mainAxis().invalidateScrollMax();
            return false;
        }
    }

    private int getRowSizeSecondary(int rowIndex) {
        if (mFixedRowSizeSecondary != 0) {
            return mFixedRowSizeSecondary;
        }
        if (mRowSizeSecondary == null) {
            return 0;
        }
        return mRowSizeSecondary[rowIndex];
    }

    int getRowStartSecondary(int rowIndex) {
        int start = 0;
        // Iterate from left to right, which is a different index traversal
        // in RTL flow
        if ((mFlag & PF_REVERSE_FLOW_SECONDARY) != 0) {
            for (int i = mNumRows - 1; i > rowIndex; i--) {
                start += getRowSizeSecondary(i) + mSpacingSecondary;
            }
        } else {
            for (int i = 0; i < rowIndex; i++) {
                start += getRowSizeSecondary(i) + mSpacingSecondary;
            }
        }
        return start;
    }

    private int getSizeSecondary() {
        int rightmostIndex = (mFlag & PF_REVERSE_FLOW_SECONDARY) != 0 ? 0 : mNumRows - 1;
        return getRowStartSecondary(rightmostIndex) + getRowSizeSecondary(rightmostIndex);
    }

    int getDecoratedMeasuredWidthWithMargin(View v) {
        final LayoutParams lp = (LayoutParams) v.getLayoutParams();
        return getDecoratedMeasuredWidth(v) + lp.leftMargin + lp.rightMargin;
    }

    int getDecoratedMeasuredHeightWithMargin(View v) {
        final LayoutParams lp = (LayoutParams) v.getLayoutParams();
        return getDecoratedMeasuredHeight(v) + lp.topMargin + lp.bottomMargin;
    }

    private void measureScrapChild(int position, int widthSpec, int heightSpec,
            int[] measuredDimension) {
        View view = mRecycler.getViewForPosition(position);
        if (view != null) {
            final LayoutParams p = (LayoutParams) view.getLayoutParams();
            calculateItemDecorationsForChild(view, sTempRect);
            int widthUsed = p.leftMargin + p.rightMargin + sTempRect.left + sTempRect.right;
            int heightUsed = p.topMargin + p.bottomMargin + sTempRect.top + sTempRect.bottom;

            int childWidthSpec = ViewGroup.getChildMeasureSpec(widthSpec,
                    getPaddingLeft() + getPaddingRight() + widthUsed, p.width);
            int childHeightSpec = ViewGroup.getChildMeasureSpec(heightSpec,
                    getPaddingTop() + getPaddingBottom() + heightUsed, p.height);
            view.measure(childWidthSpec, childHeightSpec);

            measuredDimension[0] = getDecoratedMeasuredWidthWithMargin(view);
            measuredDimension[1] = getDecoratedMeasuredHeightWithMargin(view);
            mRecycler.recycleView(view);
        }
    }

    private boolean processRowSizeSecondary(boolean measure) {
        if (mFixedRowSizeSecondary != 0 || mRowSizeSecondary == null) {
            return false;
        }

        CircularIntArray[] rows = mGrid == null ? null : mGrid.getItemPositionsInRows();
        boolean changed = false;
        int scrapeChildSize = -1;

        for (int rowIndex = 0; rowIndex < mNumRows; rowIndex++) {
            CircularIntArray row = rows == null ? null : rows[rowIndex];
            final int rowItemsPairCount = row == null ? 0 : row.size();
            int rowSize = -1;
            for (int rowItemPairIndex = 0; rowItemPairIndex < rowItemsPairCount;
                    rowItemPairIndex += 2) {
                final int rowIndexStart = row.get(rowItemPairIndex);
                final int rowIndexEnd = row.get(rowItemPairIndex + 1);
                for (int i = rowIndexStart; i <= rowIndexEnd; i++) {
                    final View view = findViewByPosition(i - mPositionDeltaInPreLayout);
                    if (view == null) {
                        continue;
                    }
                    if (measure) {
                        measureChild(view);
                    }
                    final int secondarySize = mOrientation == HORIZONTAL
                            ? getDecoratedMeasuredHeightWithMargin(view)
                            : getDecoratedMeasuredWidthWithMargin(view);
                    if (secondarySize > rowSize) {
                        rowSize = secondarySize;
                    }
                }
            }

            final int itemCount = mState.getItemCount();
            if (!mBaseGridView.hasFixedSize() && measure && rowSize < 0 && itemCount > 0) {
                if (scrapeChildSize < 0) {
                    // measure a child that is close to mFocusPosition but not currently visible
                    int position = mFocusPosition;
                    if (position < 0) {
                        position = 0;
                    } else if (position >= itemCount) {
                        position = itemCount - 1;
                    }
                    if (getChildCount() > 0) {
                        int firstPos = mBaseGridView.getChildViewHolder(
                                getChildAt(0)).getLayoutPosition();
                        int lastPos = mBaseGridView.getChildViewHolder(
                                getChildAt(getChildCount() - 1)).getLayoutPosition();
                        // if mFocusPosition is between first and last, choose either
                        // first - 1 or last + 1
                        if (position >= firstPos && position <= lastPos) {
                            position = (position - firstPos <= lastPos - position)
                                    ? (firstPos - 1) : (lastPos + 1);
                            // try the other value if the position is invalid. if both values are
                            // invalid, skip measureScrapChild below.
                            if (position < 0 && lastPos < itemCount - 1) {
                                position = lastPos + 1;
                            } else if (position >= itemCount && firstPos > 0) {
                                position = firstPos - 1;
                            }
                        }
                    }
                    if (position >= 0 && position < itemCount) {
                        measureScrapChild(position,
                                MeasureSpec.makeMeasureSpec(0, MeasureSpec.UNSPECIFIED),
                                MeasureSpec.makeMeasureSpec(0, MeasureSpec.UNSPECIFIED),
                                mMeasuredDimension);
                        scrapeChildSize = mOrientation == HORIZONTAL ? mMeasuredDimension[1] :
                                mMeasuredDimension[0];
                        if (DEBUG) {
                            Log.v(TAG, "measured scrap child: " + mMeasuredDimension[0] + " "
                                    + mMeasuredDimension[1]);
                        }
                    }
                }
                if (scrapeChildSize >= 0) {
                    rowSize = scrapeChildSize;
                }
            }
            if (rowSize < 0) {
                rowSize = 0;
            }
            if (mRowSizeSecondary[rowIndex] != rowSize) {
                if (DEBUG) {
                    Log.v(getTag(), "row size secondary changed: " + mRowSizeSecondary[rowIndex]
                            + ", " + rowSize);
                }
                mRowSizeSecondary[rowIndex] = rowSize;
                changed = true;
            }
        }

        return changed;
    }

    /**
     * Checks if we need to update row secondary sizes.
     */
    private void updateRowSecondarySizeRefresh() {
        mFlag = (mFlag & ~PF_ROW_SECONDARY_SIZE_REFRESH)
                | (processRowSizeSecondary(false) ? PF_ROW_SECONDARY_SIZE_REFRESH : 0);
        if ((mFlag & PF_ROW_SECONDARY_SIZE_REFRESH) != 0) {
            if (DEBUG) Log.v(getTag(), "mRowSecondarySizeRefresh now set");
            forceRequestLayout();
        }
    }

    private void forceRequestLayout() {
        if (DEBUG) Log.v(getTag(), "forceRequestLayout");
        // RecyclerView prevents us from requesting layout in many cases
        // (during layout, during scroll, etc.)
        // For secondary row size wrap_content support we currently need a
        // second layout pass to update the measured size after having measured
        // and added child views in layoutChildren.
        // Force the second layout by posting a delayed runnable.
        // TODO: investigate allowing a second layout pass,
        // or move child add/measure logic to the measure phase.
        ViewCompat.postOnAnimation(mBaseGridView, mRequestLayoutRunnable);
    }

    private final Runnable mRequestLayoutRunnable = new Runnable() {
        @Override
        public void run() {
            if (DEBUG) Log.v(getTag(), "request Layout from runnable");
            requestLayout();
        }
    };

    @Override
    @SuppressWarnings("ObjectToString")
    public void onMeasure(@NonNull Recycler recycler, @NonNull State state,
            int widthSpec, int heightSpec) {
        saveContext(recycler, state);

        int sizePrimary, sizeSecondary, modeSecondary, paddingSecondary;
        int measuredSizeSecondary;
        if (mOrientation == HORIZONTAL) {
            sizePrimary = MeasureSpec.getSize(widthSpec);
            sizeSecondary = MeasureSpec.getSize(heightSpec);
            modeSecondary = MeasureSpec.getMode(heightSpec);
            paddingSecondary = getPaddingTop() + getPaddingBottom();
        } else {
            sizeSecondary = MeasureSpec.getSize(widthSpec);
            sizePrimary = MeasureSpec.getSize(heightSpec);
            modeSecondary = MeasureSpec.getMode(widthSpec);
            paddingSecondary = getPaddingLeft() + getPaddingRight();
        }
        if (DEBUG) {
            Log.v(getTag(), "onMeasure widthSpec " + Integer.toHexString(widthSpec)
                    + " heightSpec " + Integer.toHexString(heightSpec)
                    + " modeSecondary " + Integer.toHexString(modeSecondary)
                    + " sizeSecondary " + sizeSecondary + " " + this);
        }

        mMaxSizeSecondary = sizeSecondary;

        if (mRowSizeSecondaryRequested == ViewGroup.LayoutParams.WRAP_CONTENT) {
            mNumRows = mNumRowsRequested == 0 ? 1 : mNumRowsRequested;
            mFixedRowSizeSecondary = 0;

            if (mRowSizeSecondary == null || mRowSizeSecondary.length != mNumRows) {
                mRowSizeSecondary = new int[mNumRows];
            }

            if (mState.isPreLayout()) {
                updatePositionDeltaInPreLayout();
            }
            // Measure all current children and update cached row height or column width
            processRowSizeSecondary(true);

            switch (modeSecondary) {
                case MeasureSpec.UNSPECIFIED:
                    measuredSizeSecondary = getSizeSecondary() + paddingSecondary;
                    break;
                case MeasureSpec.AT_MOST:
                    measuredSizeSecondary = Math.min(getSizeSecondary() + paddingSecondary,
                            mMaxSizeSecondary);
                    break;
                case MeasureSpec.EXACTLY:
                    measuredSizeSecondary = mMaxSizeSecondary;
                    break;
                default:
                    throw new IllegalStateException("wrong spec");
            }

        } else {
            switch (modeSecondary) {
                case MeasureSpec.UNSPECIFIED:
                    mFixedRowSizeSecondary = mRowSizeSecondaryRequested == 0
                            ? sizeSecondary - paddingSecondary : mRowSizeSecondaryRequested;
                    mNumRows = mNumRowsRequested == 0 ? 1 : mNumRowsRequested;
                    measuredSizeSecondary = mFixedRowSizeSecondary * mNumRows + mSpacingSecondary
                            * (mNumRows - 1) + paddingSecondary;
                    break;
                case MeasureSpec.AT_MOST:
                case MeasureSpec.EXACTLY:
                    if (mNumRowsRequested == 0 && mRowSizeSecondaryRequested == 0) {
                        mNumRows = 1;
                        mFixedRowSizeSecondary = sizeSecondary - paddingSecondary;
                    } else if (mNumRowsRequested == 0) {
                        mFixedRowSizeSecondary = mRowSizeSecondaryRequested;
                        mNumRows = (sizeSecondary + mSpacingSecondary)
                                / (mRowSizeSecondaryRequested + mSpacingSecondary);
                    } else if (mRowSizeSecondaryRequested == 0) {
                        mNumRows = mNumRowsRequested;
                        mFixedRowSizeSecondary = (sizeSecondary - paddingSecondary
                                - mSpacingSecondary * (mNumRows - 1)) / mNumRows;
                    } else {
                        mNumRows = mNumRowsRequested;
                        mFixedRowSizeSecondary = mRowSizeSecondaryRequested;
                    }
                    measuredSizeSecondary = sizeSecondary;
                    if (modeSecondary == MeasureSpec.AT_MOST) {
                        int childrenSize = mFixedRowSizeSecondary * mNumRows + mSpacingSecondary
                                * (mNumRows - 1) + paddingSecondary;
                        if (childrenSize < measuredSizeSecondary) {
                            measuredSizeSecondary = childrenSize;
                        }
                    }
                    break;
                default:
                    throw new IllegalStateException("wrong spec");
            }
        }
        if (mOrientation == HORIZONTAL) {
            setMeasuredDimension(sizePrimary, measuredSizeSecondary);
        } else {
            setMeasuredDimension(measuredSizeSecondary, sizePrimary);
        }
        if (DEBUG) {
            Log.v(getTag(), "onMeasure sizePrimary " + sizePrimary
                    + " measuredSizeSecondary " + measuredSizeSecondary
                    + " mFixedRowSizeSecondary " + mFixedRowSizeSecondary
                    + " mNumRows " + mNumRows);
        }
        leaveContext();
    }

    void measureChild(View child) {
        final LayoutParams lp = (LayoutParams) child.getLayoutParams();
        calculateItemDecorationsForChild(child, sTempRect);
        int widthUsed = lp.leftMargin + lp.rightMargin + sTempRect.left + sTempRect.right;
        int heightUsed = lp.topMargin + lp.bottomMargin + sTempRect.top + sTempRect.bottom;

        final int secondarySpec =
                (mRowSizeSecondaryRequested == ViewGroup.LayoutParams.WRAP_CONTENT)
                        ? MeasureSpec.makeMeasureSpec(0, MeasureSpec.UNSPECIFIED)
                        : MeasureSpec.makeMeasureSpec(mFixedRowSizeSecondary, MeasureSpec.EXACTLY);
        int widthSpec, heightSpec;

        if (mOrientation == HORIZONTAL) {
            widthSpec = ViewGroup.getChildMeasureSpec(
                    MeasureSpec.makeMeasureSpec(0, MeasureSpec.UNSPECIFIED), widthUsed, lp.width);
            heightSpec = ViewGroup.getChildMeasureSpec(secondarySpec, heightUsed, lp.height);
        } else {
            heightSpec = ViewGroup.getChildMeasureSpec(
                    MeasureSpec.makeMeasureSpec(0, MeasureSpec.UNSPECIFIED), heightUsed, lp.height);
            widthSpec = ViewGroup.getChildMeasureSpec(secondarySpec, widthUsed, lp.width);
        }
        child.measure(widthSpec, heightSpec);
        if (DEBUG) {
            Log.v(getTag(), "measureChild secondarySpec " + Integer.toHexString(secondarySpec)
                    + " widthSpec " + Integer.toHexString(widthSpec)
                    + " heightSpec " + Integer.toHexString(heightSpec)
                    + " measuredWidth " + child.getMeasuredWidth()
                    + " measuredHeight " + child.getMeasuredHeight());
        }
        if (DEBUG) Log.v(getTag(), "child lp width " + lp.width + " height " + lp.height);
    }

    /**
     * Get facet from the ViewHolder or the viewType.
     */
    @SuppressWarnings("unchecked")
    <E> E getFacet(RecyclerView.ViewHolder vh, Class<? extends E> facetClass) {
        E facet = null;
        if (vh instanceof FacetProvider) {
            facet = (E) ((FacetProvider) vh).getFacet(facetClass);
        }
        if (facet == null && mFacetProviderAdapter != null) {
            FacetProvider p = mFacetProviderAdapter.getFacetProvider(vh.getItemViewType());
            if (p != null) {
                facet = (E) p.getFacet(facetClass);
            }
        }
        return facet;
    }

    private final Grid.Provider mGridProvider = new Grid.Provider() {

        @Override
        public int getMinIndex() {
            return mPositionDeltaInPreLayout;
        }

        @Override
        public int getCount() {
            return mState.getItemCount() + mPositionDeltaInPreLayout;
        }

        @Override
        public int createItem(int index, boolean append, Object[] item, boolean disappearingItem) {
            View v = getViewForPosition(index - mPositionDeltaInPreLayout);
            LayoutParams lp = (LayoutParams) v.getLayoutParams();
            // See recyclerView docs:  we don't need re-add scraped view if it was removed.
            if (!lp.isItemRemoved()) {
                if (disappearingItem) {
                    if (append) {
                        addDisappearingView(v);
                    } else {
                        addDisappearingView(v, 0);
                    }
                } else {
                    if (append) {
                        addView(v);
                    } else {
                        addView(v, 0);
                    }
                }
                if (mChildVisibility != -1) {
                    v.setVisibility(mChildVisibility);
                }

                if (mPendingMoveSmoothScroller != null) {
                    mPendingMoveSmoothScroller.consumePendingMovesBeforeLayout();
                }
                int subindex = getSubPositionByView(v, v.findFocus());
                if ((mFlag & PF_STAGE_MASK) != PF_STAGE_LAYOUT) {
                    // when we are appending item during scroll pass and the item's position
                    // matches the mFocusPosition,  we should signal a childSelected event.
                    // However if we are still running PendingMoveSmoothScroller,  we defer and
                    // signal the event in PendingMoveSmoothScroller.onStop().  This can
                    // avoid lots of childSelected events during a long smooth scrolling and
                    // increase performance.
                    if (index == mFocusPosition && subindex == mSubFocusPosition
                            && mPendingMoveSmoothScroller == null) {
                        dispatchChildSelected();
                    }
                } else if ((mFlag & PF_FAST_RELAYOUT) == 0) {
                    // fastRelayout will dispatch event at end of onLayoutChildren().
                    // For full layout, two situations here:
                    // 1. mInLayoutSearchFocus is false, dispatchChildSelected() at mFocusPosition.
                    // 2. mInLayoutSearchFocus is true:  dispatchChildSelected() on first child
                    //    equal to or after mFocusPosition that can take focus.
                    if ((mFlag & PF_IN_LAYOUT_SEARCH_FOCUS) == 0 && index == mFocusPosition
                            && subindex == mSubFocusPosition) {
                        dispatchChildSelected();
                    } else if ((mFlag & PF_IN_LAYOUT_SEARCH_FOCUS) != 0 && index >= mFocusPosition
                            && v.hasFocusable()) {
                        mFocusPosition = index;
                        mSubFocusPosition = subindex;
                        mFlag &= ~PF_IN_LAYOUT_SEARCH_FOCUS;
                        dispatchChildSelected();
                    }
                }
                measureChild(v);
            }
            item[0] = v;
            return mOrientation == HORIZONTAL ? getDecoratedMeasuredWidthWithMargin(v)
                    : getDecoratedMeasuredHeightWithMargin(v);
        }

        @Override
        public void addItem(Object item, int index, int length, int rowIndex, int edge) {
            View v = (View) item;
            int start, end;
            if (edge == Integer.MIN_VALUE || edge == Integer.MAX_VALUE) {
                edge = !mGrid.isReversedFlow() ? mWindowAlignment.mainAxis().getPaddingMin()
                        : mWindowAlignment.mainAxis().getSize()
                                - mWindowAlignment.mainAxis().getPaddingMax();
            }
            boolean edgeIsMin = !mGrid.isReversedFlow();
            if (edgeIsMin) {
                start = edge;
                end = edge + length;
            } else {
                start = edge - length;
                end = edge;
            }
            int startSecondary = getRowStartSecondary(rowIndex)
                    + mWindowAlignment.secondAxis().getPaddingMin() - mScrollOffsetSecondary;
            mChildrenStates.loadView(v, index);
            layoutChild(rowIndex, v, start, end, startSecondary);
            if (DEBUG) {
                Log.d(getTag(), "addView " + index + " " + v);
            }

            if (!mState.isPreLayout()) {
                updateScrollLimits();
            }
            if ((mFlag & PF_STAGE_MASK) != PF_STAGE_LAYOUT && mPendingMoveSmoothScroller != null) {
                mPendingMoveSmoothScroller.consumePendingMovesAfterLayout();
            }
            if (mChildLaidOutListener != null) {
                RecyclerView.ViewHolder vh = mBaseGridView.getChildViewHolder(v);
                mChildLaidOutListener.onChildLaidOut(mBaseGridView, v, index,
                        vh == null ? NO_ID : vh.getItemId());
            }
        }

        @Override
        public void removeItem(int index) {
            View v = findViewByPosition(index - mPositionDeltaInPreLayout);
            if ((mFlag & PF_STAGE_MASK) == PF_STAGE_LAYOUT) {
                detachAndScrapView(v, mRecycler);
            } else {
                removeAndRecycleView(v, mRecycler);
            }
        }

        @Override
        public int getEdge(int index) {
            View v = findViewByPosition(index - mPositionDeltaInPreLayout);
            return (mFlag & PF_REVERSE_FLOW_PRIMARY) != 0 ? getViewMax(v) : getViewMin(v);
        }

        @Override
        public int getSize(int index) {
            return getViewPrimarySize(findViewByPosition(index - mPositionDeltaInPreLayout));
        }
    };

    void layoutChild(int rowIndex, View v, int start, int end, int startSecondary) {
        int sizeSecondary = mOrientation == HORIZONTAL ? getDecoratedMeasuredHeightWithMargin(v)
                : getDecoratedMeasuredWidthWithMargin(v);
        if (mFixedRowSizeSecondary > 0) {
            sizeSecondary = Math.min(sizeSecondary, mFixedRowSizeSecondary);
        }
        final int verticalGravity = mGravity & Gravity.VERTICAL_GRAVITY_MASK;
        final int horizontalGravity = (mFlag & PF_REVERSE_FLOW_MASK) != 0
                ? Gravity.getAbsoluteGravity(mGravity & Gravity.RELATIVE_HORIZONTAL_GRAVITY_MASK,
                View.LAYOUT_DIRECTION_RTL)
                : mGravity & Gravity.HORIZONTAL_GRAVITY_MASK;
        if ((mOrientation == HORIZONTAL && verticalGravity == Gravity.TOP)
                || (mOrientation == VERTICAL && horizontalGravity == Gravity.LEFT)) {
            // do nothing
        } else if ((mOrientation == HORIZONTAL && verticalGravity == Gravity.BOTTOM)
                || (mOrientation == VERTICAL && horizontalGravity == Gravity.RIGHT)) {
            startSecondary += getRowSizeSecondary(rowIndex) - sizeSecondary;
        } else if ((mOrientation == HORIZONTAL && verticalGravity == Gravity.CENTER_VERTICAL)
                || (mOrientation == VERTICAL && horizontalGravity == Gravity.CENTER_HORIZONTAL)) {
            startSecondary += (getRowSizeSecondary(rowIndex) - sizeSecondary) / 2;
        }
        int left, top, right, bottom;
        if (mOrientation == HORIZONTAL) {
            left = start;
            top = startSecondary;
            right = end;
            bottom = startSecondary + sizeSecondary;
        } else {
            top = start;
            left = startSecondary;
            bottom = end;
            right = startSecondary + sizeSecondary;
        }
        LayoutParams params = (LayoutParams) v.getLayoutParams();
        layoutDecoratedWithMargins(v, left, top, right, bottom);
        // Now super.getDecoratedBoundsWithMargins() includes the extra space for optical bounds,
        // subtracting it from value passed in layoutDecoratedWithMargins(), we can get the optical
        // bounds insets.
        super.getDecoratedBoundsWithMargins(v, sTempRect);
        params.setOpticalInsets(left - sTempRect.left, top - sTempRect.top,
                sTempRect.right - right, sTempRect.bottom - bottom);
        updateChildAlignments(v);
    }

    private void updateChildAlignments(View v) {
        final LayoutParams p = (LayoutParams) v.getLayoutParams();
        if (p.getItemAlignmentFacet() == null) {
            // Fallback to global settings on grid view
            p.setAlignX(mItemAlignment.horizontal.getAlignmentPosition(v));
            p.setAlignY(mItemAlignment.vertical.getAlignmentPosition(v));
        } else {
            // Use ItemAlignmentFacet defined on specific ViewHolder
            p.calculateItemAlignments(mOrientation, v);
            if (mOrientation == HORIZONTAL) {
                p.setAlignY(mItemAlignment.vertical.getAlignmentPosition(v));
            } else {
                p.setAlignX(mItemAlignment.horizontal.getAlignmentPosition(v));
            }
        }
    }

    private void updateChildAlignments() {
        for (int i = 0, c = getChildCount(); i < c; i++) {
            updateChildAlignments(getChildAt(i));
        }
    }

    void setExtraLayoutSpace(int extraLayoutSpace) {
        if (mExtraLayoutSpace == extraLayoutSpace) {
            return;
        } else if (mExtraLayoutSpace < 0) {
            throw new IllegalArgumentException("ExtraLayoutSpace must >= 0");
        }
        mExtraLayoutSpace = extraLayoutSpace;
        requestLayout();
    }

    int getExtraLayoutSpace() {
        return mExtraLayoutSpace;
    }

    private void removeInvisibleViewsAtEnd() {
        if ((mFlag & (PF_PRUNE_CHILD | PF_SLIDING)) == PF_PRUNE_CHILD) {
            mGrid.removeInvisibleItemsAtEnd(mFocusPosition, (mFlag & PF_REVERSE_FLOW_PRIMARY) != 0
                    ? -mExtraLayoutSpace : mSizePrimary + mExtraLayoutSpace);
        }
    }

    private void removeInvisibleViewsAtFront() {
        if ((mFlag & (PF_PRUNE_CHILD | PF_SLIDING)) == PF_PRUNE_CHILD) {
            mGrid.removeInvisibleItemsAtFront(mFocusPosition, (mFlag & PF_REVERSE_FLOW_PRIMARY) != 0
                    ? mSizePrimary + mExtraLayoutSpace : -mExtraLayoutSpace);
        }
    }

    private boolean appendOneColumnVisibleItems() {
        return mGrid.appendOneColumnVisibleItems();
    }

    void slideIn() {
        if ((mFlag & PF_SLIDING) != 0) {
            mFlag &= ~PF_SLIDING;
            if (mFocusPosition >= 0) {
                scrollToSelection(mFocusPosition, mSubFocusPosition, true, mPrimaryScrollExtra);
            } else {
                mFlag &= ~PF_LAYOUT_EATEN_IN_SLIDING;
                requestLayout();
            }
            if ((mFlag & PF_LAYOUT_EATEN_IN_SLIDING) != 0) {
                mFlag &= ~PF_LAYOUT_EATEN_IN_SLIDING;
                if (mBaseGridView.getScrollState() != SCROLL_STATE_IDLE || isSmoothScrolling()) {
                    mBaseGridView.addOnScrollListener(new RecyclerView.OnScrollListener() {
                        @Override
                        public void onScrollStateChanged(@NonNull RecyclerView recyclerView,
                                int newState) {
                            if (newState == SCROLL_STATE_IDLE) {
                                mBaseGridView.removeOnScrollListener(this);
                                requestLayout();
                            }
                        }
                    });
                } else {
                    requestLayout();
                }
            }
        }
    }

    int getSlideOutDistance() {
        int distance;
        if (mOrientation == VERTICAL) {
            distance = -getHeight();
            if (getChildCount() > 0) {
                int top = getChildAt(0).getTop();
                if (top < 0) {
                    // scroll more if first child is above top edge
                    distance = distance + top;
                }
            }
        } else {
            if ((mFlag & PF_REVERSE_FLOW_PRIMARY) != 0) {
                distance = getWidth();
                if (getChildCount() > 0) {
                    int start = getChildAt(0).getRight();
                    if (start > distance) {
                        // scroll more if first child is outside right edge
                        distance = start;
                    }
                }
            } else {
                distance = -getWidth();
                if (getChildCount() > 0) {
                    int start = getChildAt(0).getLeft();
                    if (start < 0) {
                        // scroll more if first child is out side left edge
                        distance = distance + start;
                    }
                }
            }
        }
        return distance;
    }

    boolean isSlidingChildViews() {
        return (mFlag & PF_SLIDING) != 0;
    }

    /**
     * Temporarily slide out child and block layout and scroll requests.
     */
    void slideOut() {
        if ((mFlag & PF_SLIDING) != 0) {
            return;
        }
        mFlag |= PF_SLIDING;
        if (getChildCount() == 0) {
            return;
        }
        if (mOrientation == VERTICAL) {
            mBaseGridView.smoothScrollBy(0, getSlideOutDistance(),
                    new AccelerateDecelerateInterpolator());
        } else {
            mBaseGridView.smoothScrollBy(getSlideOutDistance(), 0,
                    new AccelerateDecelerateInterpolator());
        }
    }

    private boolean prependOneColumnVisibleItems() {
        return mGrid.prependOneColumnVisibleItems();
    }

    private void appendVisibleItems() {
        mGrid.appendVisibleItems((mFlag & PF_REVERSE_FLOW_PRIMARY) != 0
                ? -mExtraLayoutSpace - mExtraLayoutSpaceInPreLayout
                : mSizePrimary + mExtraLayoutSpace + mExtraLayoutSpaceInPreLayout);
    }

    private void prependVisibleItems() {
        mGrid.prependVisibleItems((mFlag & PF_REVERSE_FLOW_PRIMARY) != 0
                ? mSizePrimary + mExtraLayoutSpace + mExtraLayoutSpaceInPreLayout
                : -mExtraLayoutSpace - mExtraLayoutSpaceInPreLayout);
    }

    /**
     * Fast layout when there is no structure change, adapter change, etc.
     * It will layout all views was layout requested or updated, until hit a view
     * with different size,  then it break and detachAndScrap all views after that.
     */
    private void fastRelayout() {
        boolean invalidateAfter = false;
        final int childCount = getChildCount();
        int position = mGrid.getFirstVisibleIndex();
        int index = 0;
        mFlag &= ~PF_FAST_RELAYOUT_UPDATED_SELECTED_POSITION;
        for (; index < childCount; index++, position++) {
            View view = getChildAt(index);
            // We don't hit fastRelayout() if State.didStructure() is true, but prelayout may add
            // extra views and invalidate existing Grid position. Also the prelayout calling
            // getViewForPosotion() may retrieve item from cache with FLAG_INVALID. The adapter
            // postion will be -1 for this case. Either case, we should invalidate after this item
            // and call getViewForPosition() again to rebind.
            if (position != getAdapterPositionByView(view)) {
                invalidateAfter = true;
                break;
            }
            Grid.Location location = mGrid.getLocation(position);
            if (location == null) {
                invalidateAfter = true;
                break;
            }

            int startSecondary = getRowStartSecondary(location.mRow)
                    + mWindowAlignment.secondAxis().getPaddingMin() - mScrollOffsetSecondary;
            int primarySize, end;
            int start = getViewMin(view);
            int oldPrimarySize = getViewPrimarySize(view);

            LayoutParams lp = (LayoutParams) view.getLayoutParams();
            if (lp.viewNeedsUpdate()) {
                mFlag |= PF_FAST_RELAYOUT_UPDATED_SELECTED_POSITION;
                detachAndScrapView(view, mRecycler);
                view = getViewForPosition(position);
                addView(view, index);
            }

            measureChild(view);
            if (mOrientation == HORIZONTAL) {
                primarySize = getDecoratedMeasuredWidthWithMargin(view);
                end = start + primarySize;
            } else {
                primarySize = getDecoratedMeasuredHeightWithMargin(view);
                end = start + primarySize;
            }
            layoutChild(location.mRow, view, start, end, startSecondary);
            if (oldPrimarySize != primarySize) {
                // size changed invalidate remaining Locations
                if (DEBUG) Log.d(getTag(), "fastRelayout: view size changed at " + position);
                invalidateAfter = true;
                break;
            }
        }
        if (invalidateAfter) {
            final int savedLastPos = mGrid.getLastVisibleIndex();
            for (int i = childCount - 1; i >= index; i--) {
                View v = getChildAt(i);
                detachAndScrapView(v, mRecycler);
            }
            mGrid.invalidateItemsAfter(position);
            if ((mFlag & PF_PRUNE_CHILD) != 0) {
                // in regular prune child mode, we just append items up to edge limit
                appendVisibleItems();
                if (mFocusPosition >= 0 && mFocusPosition <= savedLastPos) {
                    // make sure add focus view back:  the view might be outside edge limit
                    // when there is delta in onLayoutChildren().
                    while (mGrid.getLastVisibleIndex() < mFocusPosition) {
                        mGrid.appendOneColumnVisibleItems();
                    }
                }
            } else {
                // prune disabled(e.g. in RowsFragment transition): append all removed items
                while (mGrid.appendOneColumnVisibleItems()
                        && mGrid.getLastVisibleIndex() < savedLastPos) {
                    // Do nothing.
                }
            }
        }
        updateScrollLimits();
        updateSecondaryScrollLimits();
    }

    @Override
    public void removeAndRecycleAllViews(@NonNull RecyclerView.Recycler recycler) {
        if (DEBUG) Log.v(TAG, "removeAndRecycleAllViews " + getChildCount());
        for (int i = getChildCount() - 1; i >= 0; i--) {
            removeAndRecycleViewAt(i, recycler);
        }
    }

    // called by onLayoutChildren, either focus to FocusPosition or declare focusViewAvailable
    // and scroll to the view if framework focus on it.
    private void focusToViewInLayout(boolean hadFocus, boolean alignToView, int extraDelta,
            int extraDeltaSecondary) {
        View focusView = findViewByPosition(mFocusPosition);
        if (focusView != null && alignToView) {
            scrollToView(focusView, false, extraDelta, extraDeltaSecondary);
        }
        if (focusView != null && hadFocus && !focusView.hasFocus()) {
            focusView.requestFocus();
        } else if (!hadFocus && !mBaseGridView.hasFocus()) {
            if (focusView != null && focusView.hasFocusable()) {
                mBaseGridView.focusableViewAvailable(focusView);
            } else {
                for (int i = 0, count = getChildCount(); i < count; i++) {
                    focusView = getChildAt(i);
                    if (focusView != null && focusView.hasFocusable()) {
                        mBaseGridView.focusableViewAvailable(focusView);
                        break;
                    }
                }
            }
            // focusViewAvailable() might focus to the view, scroll to it if that is the case.
            if (alignToView && focusView != null && focusView.hasFocus()) {
                scrollToView(focusView, false, extraDelta, extraDeltaSecondary);
            }
        }
    }

    @Override
    public void onLayoutCompleted(@NonNull State state) {
        if (mOnLayoutCompletedListeners != null) {
            for (int i = mOnLayoutCompletedListeners.size() - 1; i >= 0; i--) {
                mOnLayoutCompletedListeners.get(i).onLayoutCompleted(state);
            }
        }
    }

    @Override
    public boolean supportsPredictiveItemAnimations() {
        return true;
    }

    void updatePositionToRowMapInPostLayout() {
        mPositionToRowInPostLayout.clear();
        final int childCount = getChildCount();
        for (int i = 0; i < childCount; i++) {
            // Grid still maps to old positions at this point, use old position to get row infor
            int position = mBaseGridView.getChildViewHolder(getChildAt(i)).getOldPosition();
            if (position >= 0) {
                Grid.Location loc = mGrid.getLocation(position);
                if (loc != null) {
                    mPositionToRowInPostLayout.put(position, loc.mRow);
                }
            }
        }
    }

    void fillScrapViewsInPostLayout() {
        List<RecyclerView.ViewHolder> scrapList = mRecycler.getScrapList();
        final int scrapSize = scrapList.size();
        if (scrapSize == 0) {
            return;
        }
        // initialize the int array or re-allocate the array.
        if (mDisappearingPositions == null || scrapSize > mDisappearingPositions.length) {
            int length = mDisappearingPositions == null ? 16 : mDisappearingPositions.length;
            while (length < scrapSize) {
                length = length << 1;
            }
            mDisappearingPositions = new int[length];
        }
        int totalItems = 0;
        for (int i = 0; i < scrapSize; i++) {
            int pos = scrapList.get(i).getAbsoluteAdapterPosition();
            if (pos >= 0) {
                mDisappearingPositions[totalItems++] = pos;
            }
        }
        // totalItems now has the length of disappearing items
        if (totalItems > 0) {
            Arrays.sort(mDisappearingPositions, 0, totalItems);
            mGrid.fillDisappearingItems(mDisappearingPositions, totalItems,
                    mPositionToRowInPostLayout);
        }
        mPositionToRowInPostLayout.clear();
    }

    // in prelayout, first child's getViewPosition can be smaller than old adapter position
    // if there were items removed before first visible index. For example:
    // visible items are 3, 4, 5, 6, deleting 1, 2, 3 from adapter; the view position in
    // prelayout are not 3(deleted), 4, 5, 6. Instead it's 1(deleted), 2, 3, 4.
    // So there is a delta (2 in this case) between last cached position and prelayout position.
    void updatePositionDeltaInPreLayout() {
        if (getChildCount() > 0) {
            View view = getChildAt(0);
            LayoutParams lp = (LayoutParams) view.getLayoutParams();
            mPositionDeltaInPreLayout = mGrid.getFirstVisibleIndex()
                    - lp.getViewLayoutPosition();
        } else {
            mPositionDeltaInPreLayout = 0;
        }
    }

    // Lays out items based on the current scroll position
    @Override
    public void onLayoutChildren(@NonNull RecyclerView.Recycler recycler,
            @NonNull RecyclerView.State state) {
        if (DEBUG) {
            Log.v(getTag(), "layoutChildren start numRows " + mNumRows
                    + " inPreLayout " + state.isPreLayout()
                    + " didStructureChange " + state.didStructureChange()
                    + " mForceFullLayout " + ((mFlag & PF_FORCE_FULL_LAYOUT) != 0));
            Log.v(getTag(), "width " + getWidth() + " height " + getHeight());
        }

        if (mNumRows == 0) {
            // haven't done measure yet
            return;
        }
        final int itemCount = state.getItemCount();
        if (itemCount < 0) {
            return;
        }

        if ((mFlag & PF_SLIDING) != 0) {
            // if there is already children, delay the layout process until slideIn(), if it's
            // first time layout children: scroll them offscreen at end of onLayoutChildren()
            if (getChildCount() > 0) {
                mFlag |= PF_LAYOUT_EATEN_IN_SLIDING;
                return;
            }
        }
        if ((mFlag & PF_LAYOUT_ENABLED) == 0) {
            discardLayoutInfo();
            removeAndRecycleAllViews(recycler);
            return;
        }
        mFlag = (mFlag & ~PF_STAGE_MASK) | PF_STAGE_LAYOUT;

        saveContext(recycler, state);
        if (state.isPreLayout()) {
            updatePositionDeltaInPreLayout();
            int childCount = getChildCount();
            if (mGrid != null && childCount > 0) {
                int minChangedEdge = Integer.MAX_VALUE;
                int maxChangeEdge = Integer.MIN_VALUE;
                int minOldAdapterPosition = mBaseGridView.getChildViewHolder(
                        getChildAt(0)).getOldPosition();
                int maxOldAdapterPosition = mBaseGridView.getChildViewHolder(
                        getChildAt(childCount - 1)).getOldPosition();
                for (int i = 0; i < childCount; i++) {
                    View view = getChildAt(i);
                    LayoutParams lp = (LayoutParams) view.getLayoutParams();
                    int newAdapterPosition = mBaseGridView.getChildAdapterPosition(view);
                    // if either of following happening
                    // 1. item itself has changed or layout parameter changed
                    // 2. item is losing focus
                    // 3. item is gaining focus
                    // 4. item is moved out of old adapter position range.
                    if (lp.isItemChanged() || lp.isItemRemoved() || view.isLayoutRequested()
                            || (!view.hasFocus()
                            && mFocusPosition == lp.getAbsoluteAdapterPosition())
                            || (view.hasFocus()
                            && mFocusPosition != lp.getAbsoluteAdapterPosition())
                            || newAdapterPosition < minOldAdapterPosition
                            || newAdapterPosition > maxOldAdapterPosition) {
                        minChangedEdge = Math.min(minChangedEdge, getViewMin(view));
                        maxChangeEdge = Math.max(maxChangeEdge, getViewMax(view));
                    }
                }
                if (maxChangeEdge > minChangedEdge) {
                    mExtraLayoutSpaceInPreLayout = maxChangeEdge - minChangedEdge;
                }
                // append items for mExtraLayoutSpaceInPreLayout
                appendVisibleItems();
                prependVisibleItems();
            }
            mFlag &= ~PF_STAGE_MASK;
            leaveContext();
            if (DEBUG) Log.v(getTag(), "layoutChildren end");
            return;
        }

        // save all view's row information before detach all views
        if (state.willRunPredictiveAnimations()) {
            updatePositionToRowMapInPostLayout();
        }
        // check if we need align to mFocusPosition, this is usually true unless in smoothScrolling
        final boolean scrollToFocus = !isSmoothScrolling()
                && mFocusScrollStrategy == BaseGridView.FOCUS_SCROLL_ALIGNED;
        if (mFocusPosition != NO_POSITION && mFocusPositionOffset != Integer.MIN_VALUE) {
            mFocusPosition = mFocusPosition + mFocusPositionOffset;
            mSubFocusPosition = 0;
        }
        mFocusPositionOffset = 0;

        View savedFocusView = findViewByPosition(mFocusPosition);
        int savedFocusPos = mFocusPosition;
        int savedSubFocusPos = mSubFocusPosition;
        boolean hadFocus = mBaseGridView.hasFocus();
        final int firstVisibleIndex = mGrid != null ? mGrid.getFirstVisibleIndex() : NO_POSITION;
        final int lastVisibleIndex = mGrid != null ? mGrid.getLastVisibleIndex() : NO_POSITION;
        final int deltaPrimary;
        final int deltaSecondary;
        if (mOrientation == HORIZONTAL) {
            deltaPrimary = state.getRemainingScrollHorizontal();
            deltaSecondary = state.getRemainingScrollVertical();
        } else {
            deltaSecondary = state.getRemainingScrollHorizontal();
            deltaPrimary = state.getRemainingScrollVertical();
        }
        if (layoutInit()) {
            mFlag |= PF_FAST_RELAYOUT;
            // If grid view is empty, we will start from mFocusPosition
            mGrid.setStart(mFocusPosition);
            fastRelayout();
        } else {
            mFlag &= ~PF_FAST_RELAYOUT;
            // layoutInit() has detached all views, so start from scratch
            mFlag = (mFlag & ~PF_IN_LAYOUT_SEARCH_FOCUS)
                    | (scrollToFocus ? PF_IN_LAYOUT_SEARCH_FOCUS : 0);
            int startFromPosition, endPos;
            if (scrollToFocus && (firstVisibleIndex < 0 || mFocusPosition > lastVisibleIndex
                    || mFocusPosition < firstVisibleIndex)) {
                startFromPosition = endPos = mFocusPosition;
            } else {
                startFromPosition = firstVisibleIndex;
                endPos = lastVisibleIndex;
            }
            mGrid.setStart(startFromPosition);
            if (endPos != NO_POSITION) {
                while (appendOneColumnVisibleItems() && findViewByPosition(endPos) == null) {
                    // continuously append items until endPos
                }
            }
        }
        // multiple rounds: scrollToView of first round may drag first/last child into
        // "visible window" and we update scrollMin/scrollMax then run second scrollToView
        // we must do this for fastRelayout() for the append item case
        int oldFirstVisible;
        int oldLastVisible;
        do {
            updateScrollLimits();
            oldFirstVisible = mGrid.getFirstVisibleIndex();
            oldLastVisible = mGrid.getLastVisibleIndex();
            focusToViewInLayout(hadFocus, scrollToFocus, -deltaPrimary, -deltaSecondary);
            appendVisibleItems();
            prependVisibleItems();
            // b/67370222: do not removeInvisibleViewsAtFront/End() in the loop, otherwise
            // loop may bounce between scroll forward and scroll backward forever. Example:
            // Assuming there are 19 items, child#18 and child#19 are both in RV, we are
            // trying to focus to child#18 and there are 200px remaining scroll distance.
            //   1  focusToViewInLayout() tries scroll forward 50 px to align focused child#18 on
            //      right edge, but there to compensate remaining scroll 200px, also scroll
            //      backward 200px, 150px pushes last child#19 out side of right edge.
            //   2  removeInvisibleViewsAtEnd() remove last child#19, updateScrollLimits()
            //      invalidates scroll max
            //   3  In next iteration, when scroll max/min is unknown, focusToViewInLayout() will
            //      align focused child#18 at center of screen.
            //   4  Because #18 is aligned at center, appendVisibleItems() will fill child#19 to
            //      the right.
            //   5  (back to 1 and loop forever)
        } while (mGrid.getFirstVisibleIndex() != oldFirstVisible
                || mGrid.getLastVisibleIndex() != oldLastVisible);
        removeInvisibleViewsAtFront();
        removeInvisibleViewsAtEnd();

        if (state.willRunPredictiveAnimations()) {
            fillScrapViewsInPostLayout();
        }

        if (DEBUG) {
            StringWriter sw = new StringWriter();
            PrintWriter pw = new PrintWriter(sw);
            mGrid.debugPrint(pw);
            Log.d(getTag(), sw.toString());
        }

        if ((mFlag & PF_ROW_SECONDARY_SIZE_REFRESH) != 0) {
            mFlag &= ~PF_ROW_SECONDARY_SIZE_REFRESH;
        } else {
            updateRowSecondarySizeRefresh();
        }

        // For fastRelayout, only dispatch event when focus position changes or selected item
        // being updated.
        if ((mFlag & PF_FAST_RELAYOUT) != 0 && (mFocusPosition != savedFocusPos || mSubFocusPosition
                != savedSubFocusPos || findViewByPosition(mFocusPosition) != savedFocusView
                || (mFlag & PF_FAST_RELAYOUT_UPDATED_SELECTED_POSITION) != 0)) {
            dispatchChildSelected();
        } else if ((mFlag & (PF_FAST_RELAYOUT | PF_IN_LAYOUT_SEARCH_FOCUS))
                == PF_IN_LAYOUT_SEARCH_FOCUS) {
            // For full layout we dispatchChildSelected() in createItem() unless searched all
            // children and found none is focusable then dispatchChildSelected() here.
            dispatchChildSelected();
        }
        dispatchChildSelectedAndPositioned();
        if ((mFlag & PF_SLIDING) != 0) {
            scrollDirectionPrimary(getSlideOutDistance());
        }

        mFlag &= ~PF_STAGE_MASK;
        leaveContext();
        if (DEBUG) Log.v(getTag(), "layoutChildren end");
    }

    private void offsetChildrenSecondary(int increment) {
        final int childCount = getChildCount();
        if (mOrientation == HORIZONTAL) {
            for (int i = 0; i < childCount; i++) {
                getChildAt(i).offsetTopAndBottom(increment);
            }
        } else {
            for (int i = 0; i < childCount; i++) {
                getChildAt(i).offsetLeftAndRight(increment);
            }
        }
    }

    private void offsetChildrenPrimary(int increment) {
        final int childCount = getChildCount();
        if (mOrientation == VERTICAL) {
            for (int i = 0; i < childCount; i++) {
                getChildAt(i).offsetTopAndBottom(increment);
            }
        } else {
            for (int i = 0; i < childCount; i++) {
                getChildAt(i).offsetLeftAndRight(increment);
            }
        }
    }

    @Override
    public int scrollHorizontallyBy(int dx, @NonNull Recycler recycler,
            @NonNull RecyclerView.State state) {
        if (DEBUG) Log.v(getTag(), "scrollHorizontallyBy " + dx);
        if ((mFlag & PF_LAYOUT_ENABLED) == 0 || !hasDoneFirstLayout()) {
            return 0;
        }
        saveContext(recycler, state);
        mFlag = (mFlag & ~PF_STAGE_MASK) | PF_STAGE_SCROLL;
        int result;
        if (mOrientation == HORIZONTAL) {
            result = scrollDirectionPrimary(dx);
        } else {
            result = scrollDirectionSecondary(dx);
        }
        leaveContext();
        mFlag &= ~PF_STAGE_MASK;
        return result;
    }

    @Override
    public int scrollVerticallyBy(int dy, @NonNull Recycler recycler,
            @NonNull RecyclerView.State state) {
        if (DEBUG) Log.v(getTag(), "scrollVerticallyBy " + dy);
        if ((mFlag & PF_LAYOUT_ENABLED) == 0 || !hasDoneFirstLayout()) {
            return 0;
        }
        mFlag = (mFlag & ~PF_STAGE_MASK) | PF_STAGE_SCROLL;
        saveContext(recycler, state);
        int result;
        if (mOrientation == VERTICAL) {
            result = scrollDirectionPrimary(dy);
        } else {
            result = scrollDirectionSecondary(dy);
        }
        leaveContext();
        mFlag &= ~PF_STAGE_MASK;
        return result;
    }

    // scroll in main direction may add/prune views
    private int scrollDirectionPrimary(int da) {
        // We apply the cap of maxScroll/minScroll to the delta, except for two cases:
        // 1. when children are in sliding out mode
        // 2. During onLayoutChildren(), it may compensate the remaining scroll delta,
        //    we should honor the request regardless if it goes over minScroll / maxScroll.
        //    (see b/64931938 testScrollAndRemove and testScrollAndRemoveSample1)
        if ((mFlag & PF_SLIDING) == 0 && (mFlag & PF_STAGE_MASK) != PF_STAGE_LAYOUT) {
            if (da > 0) {
                if (!mWindowAlignment.mainAxis().isMaxUnknown()) {
                    int maxScroll = mWindowAlignment.mainAxis().getMaxScroll();
                    if (da > maxScroll) {
                        da = maxScroll;
                    }
                }
            } else if (da < 0) {
                if (!mWindowAlignment.mainAxis().isMinUnknown()) {
                    int minScroll = mWindowAlignment.mainAxis().getMinScroll();
                    if (da < minScroll) {
                        da = minScroll;
                    }
                }
            }
        }
        if (da == 0) {
            return 0;
        }
        offsetChildrenPrimary(-da);
        if ((mFlag & PF_STAGE_MASK) == PF_STAGE_LAYOUT) {
            updateScrollLimits();
            return da;
        }

        int childCount = getChildCount();
        boolean updated;

        if ((mFlag & PF_REVERSE_FLOW_PRIMARY) != 0 ? da > 0 : da < 0) {
            prependVisibleItems();
        } else {
            appendVisibleItems();
        }
        updated = getChildCount() > childCount;
        childCount = getChildCount();

        if ((mFlag & PF_REVERSE_FLOW_PRIMARY) != 0 ? da > 0 : da < 0) {
            removeInvisibleViewsAtEnd();
        } else {
            removeInvisibleViewsAtFront();
        }
        updated |= getChildCount() < childCount;
        if (updated) {
            updateRowSecondarySizeRefresh();
        }

        mBaseGridView.invalidate();
        updateScrollLimits();
        return da;
    }

    // scroll in second direction will not add/prune views
    private int scrollDirectionSecondary(int dy) {
        if (dy == 0) {
            return 0;
        }
        offsetChildrenSecondary(-dy);
        mScrollOffsetSecondary += dy;
        updateSecondaryScrollLimits();
        mBaseGridView.invalidate();
        return dy;
    }

    @Override
    public void collectAdjacentPrefetchPositions(int dx, int dy, @NonNull State state,
            @NonNull LayoutPrefetchRegistry layoutPrefetchRegistry) {
        try {
            saveContext(null, state);
            int da = (mOrientation == HORIZONTAL) ? dx : dy;
            if (getChildCount() == 0 || da == 0) {
                // can't support this scroll, so don't bother prefetching
                return;
            }

            int fromLimit = da < 0
                    ? -mExtraLayoutSpace
                    : mSizePrimary + mExtraLayoutSpace;
            mGrid.collectAdjacentPrefetchPositions(fromLimit, da, layoutPrefetchRegistry);
        } finally {
            leaveContext();
        }
    }

    @Override
    public void collectInitialPrefetchPositions(int adapterItemCount,
            @NonNull LayoutPrefetchRegistry layoutPrefetchRegistry) {
        int numToPrefetch = mBaseGridView.mInitialPrefetchItemCount;
        if (adapterItemCount != 0 && numToPrefetch != 0) {
            // prefetch items centered around mFocusPosition
            int initialPos = Math.max(0, Math.min(mFocusPosition - (numToPrefetch - 1) / 2,
                    adapterItemCount - numToPrefetch));
            for (int i = initialPos; i < adapterItemCount && i < initialPos + numToPrefetch; i++) {
                layoutPrefetchRegistry.addPosition(i, 0);
            }
        }
    }

    void updateScrollLimits() {
        if (mState.getItemCount() == 0) {
            return;
        }
        int highVisiblePos, lowVisiblePos;
        int highMaxPos, lowMinPos;
        if ((mFlag & PF_REVERSE_FLOW_PRIMARY) == 0) {
            highVisiblePos = mGrid.getLastVisibleIndex();
            highMaxPos = mState.getItemCount() - 1;
            lowVisiblePos = mGrid.getFirstVisibleIndex();
            lowMinPos = 0;
        } else {
            highVisiblePos = mGrid.getFirstVisibleIndex();
            highMaxPos = 0;
            lowVisiblePos = mGrid.getLastVisibleIndex();
            lowMinPos = mState.getItemCount() - 1;
        }
        if (highVisiblePos < 0 || lowVisiblePos < 0) {
            return;
        }
        final boolean highAvailable = highVisiblePos == highMaxPos;
        final boolean lowAvailable = lowVisiblePos == lowMinPos;
        if (!highAvailable && mWindowAlignment.mainAxis().isMaxUnknown()
                && !lowAvailable && mWindowAlignment.mainAxis().isMinUnknown()) {
            return;
        }
        int maxEdge, maxViewCenter;
        if (highAvailable) {
            maxEdge = mGrid.findRowMax(true, sTwoInts);
            View maxChild = findViewByPosition(sTwoInts[1]);
            maxViewCenter = getViewCenter(maxChild);
            final LayoutParams lp = (LayoutParams) maxChild.getLayoutParams();
            int[] multipleAligns = lp.getAlignMultiple();
            if (multipleAligns != null && multipleAligns.length > 0) {
                maxViewCenter += multipleAligns[multipleAligns.length - 1] - multipleAligns[0];
            }
        } else {
            maxEdge = Integer.MAX_VALUE;
            maxViewCenter = Integer.MAX_VALUE;
        }
        int minEdge, minViewCenter;
        if (lowAvailable) {
            minEdge = mGrid.findRowMin(false, sTwoInts);
            View minChild = findViewByPosition(sTwoInts[1]);
            minViewCenter = getViewCenter(minChild);
        } else {
            minEdge = Integer.MIN_VALUE;
            minViewCenter = Integer.MIN_VALUE;
        }
        mWindowAlignment.mainAxis().updateMinMax(minEdge, maxEdge, minViewCenter, maxViewCenter);
    }

    /**
     * Update secondary axis's scroll min/max, should be updated in
     * {@link #scrollDirectionSecondary(int)}.
     */
    private void updateSecondaryScrollLimits() {
        WindowAlignment.Axis secondAxis = mWindowAlignment.secondAxis();
        int minEdge = secondAxis.getPaddingMin() - mScrollOffsetSecondary;
        int maxEdge = minEdge + getSizeSecondary();
        secondAxis.updateMinMax(minEdge, maxEdge, minEdge, maxEdge);
    }

    private void initScrollController() {
        mWindowAlignment.reset();
        mWindowAlignment.horizontal.setSize(getWidth());
        mWindowAlignment.vertical.setSize(getHeight());
        mWindowAlignment.horizontal.setPadding(getPaddingLeft(), getPaddingRight());
        mWindowAlignment.vertical.setPadding(getPaddingTop(), getPaddingBottom());
        mSizePrimary = mWindowAlignment.mainAxis().getSize();
        mScrollOffsetSecondary = 0;

        if (DEBUG) {
            Log.v(getTag(), "initScrollController mSizePrimary " + mSizePrimary
                    + " mWindowAlignment " + mWindowAlignment);
        }
    }

    private void updateScrollController() {
        mWindowAlignment.horizontal.setSize(getWidth());
        mWindowAlignment.vertical.setSize(getHeight());
        mWindowAlignment.horizontal.setPadding(getPaddingLeft(), getPaddingRight());
        mWindowAlignment.vertical.setPadding(getPaddingTop(), getPaddingBottom());
        mSizePrimary = mWindowAlignment.mainAxis().getSize();

        if (DEBUG) {
            Log.v(getTag(), "updateScrollController mSizePrimary " + mSizePrimary
                    + " mWindowAlignment " + mWindowAlignment);
        }
    }

    @Override
    public void scrollToPosition(int position) {
        setSelection(position, 0, false, 0);
    }

    @Override
    public void smoothScrollToPosition(@NonNull RecyclerView recyclerView, @NonNull State state,
            int position) {
        setSelection(position, 0, true, 0);
    }

    void setSelection(int position,
            int primaryScrollExtra) {
        setSelection(position, 0, false, primaryScrollExtra);
    }

    void setSelectionSmooth(int position) {
        setSelection(position, 0, true, 0);
    }

    void setSelectionWithSub(int position, int subposition,
            int primaryScrollExtra) {
        setSelection(position, subposition, false, primaryScrollExtra);
    }

    void setSelectionSmoothWithSub(int position, int subposition) {
        setSelection(position, subposition, true, 0);
    }

    int getSelection() {
        return mFocusPosition;
    }

    int getSubSelection() {
        return mSubFocusPosition;
    }

    void setSelection(int position, int subposition, boolean smooth,
            int primaryScrollExtra) {
        if ((mFocusPosition != position && position != NO_POSITION)
                || subposition != mSubFocusPosition || primaryScrollExtra != mPrimaryScrollExtra) {
            scrollToSelection(position, subposition, smooth, primaryScrollExtra);
        }
    }

    void scrollToSelection(int position, int subposition,
            boolean smooth, int primaryScrollExtra) {
        mPrimaryScrollExtra = primaryScrollExtra;

        View view = findViewByPosition(position);
        // scrollToView() is based on Adapter position. Only call scrollToView() when item
        // is still valid and no layout is requested, otherwise defer to next layout pass.
        // If it is still in smoothScrolling, we should either update smoothScroller or initiate
        // a layout.
        final boolean notSmoothScrolling = !isSmoothScrolling();
        if (notSmoothScrolling && !mBaseGridView.isLayoutRequested()
                && view != null && getAdapterPositionByView(view) == position) {
            mFlag |= PF_IN_SELECTION;
            scrollToView(view, smooth);
            mFlag &= ~PF_IN_SELECTION;
        } else {
            if ((mFlag & PF_LAYOUT_ENABLED) == 0 || (mFlag & PF_SLIDING) != 0) {
                mFocusPosition = position;
                mSubFocusPosition = subposition;
                mFocusPositionOffset = Integer.MIN_VALUE;
                return;
            }
            if (smooth && !mBaseGridView.isLayoutRequested()) {
                mFocusPosition = position;
                mSubFocusPosition = subposition;
                mFocusPositionOffset = Integer.MIN_VALUE;
                if (!hasDoneFirstLayout()) {
                    Log.w(getTag(), "setSelectionSmooth should "
                            + "not be called before first layout pass");
                    return;
                }
                position = startPositionSmoothScroller(position);
                if (position != mFocusPosition) {
                    // gets cropped by adapter size
                    mFocusPosition = position;
                    mSubFocusPosition = 0;
                }
            } else {
                // stopScroll might change mFocusPosition, so call it before assign value to
                // mFocusPosition
                if (!notSmoothScrolling) {
                    skipSmoothScrollerOnStopInternal();
                    mBaseGridView.stopScroll();
                }
                if (!mBaseGridView.isLayoutRequested()
                        && view != null && getAdapterPositionByView(view) == position) {
                    mFlag |= PF_IN_SELECTION;
                    scrollToView(view, smooth);
                    mFlag &= ~PF_IN_SELECTION;
                } else {
                    mFocusPosition = position;
                    mSubFocusPosition = subposition;
                    mFocusPositionOffset = Integer.MIN_VALUE;
                    mFlag |= PF_FORCE_FULL_LAYOUT;
                    requestLayout();
                }
            }
        }
    }

    int startPositionSmoothScroller(int position) {
        LinearSmoothScroller linearSmoothScroller = new GridLinearSmoothScroller() {
            @Override
            public PointF computeScrollVectorForPosition(int targetPosition) {
                if (getChildCount() == 0) {
                    return null;
                }
                final int firstChildPos = getPosition(getChildAt(0));
                // TODO We should be able to deduce direction from bounds of current and target
                // focus, rather than making assumptions about positions and directionality
                final boolean isStart = (mFlag & PF_REVERSE_FLOW_PRIMARY) != 0
                        ? targetPosition > firstChildPos
                        : targetPosition < firstChildPos;
                final int direction = isStart ? -1 : 1;
                if (mOrientation == HORIZONTAL) {
                    return new PointF(direction, 0);
                } else {
                    return new PointF(0, direction);
                }
            }

        };
        linearSmoothScroller.setTargetPosition(position);
        startSmoothScroll(linearSmoothScroller);
        return linearSmoothScroller.getTargetPosition();
    }

    /**
     * when start a new SmoothScroller or scroll to a different location, dont need
     * current SmoothScroller.onStopInternal() doing the scroll work.
     */
    void skipSmoothScrollerOnStopInternal() {
        if (mCurrentSmoothScroller != null) {
            mCurrentSmoothScroller.mSkipOnStopInternal = true;
        }
    }

    @Override
    public void startSmoothScroll(@NonNull RecyclerView.SmoothScroller smoothScroller) {
        skipSmoothScrollerOnStopInternal();
        super.startSmoothScroll(smoothScroller);
        if (smoothScroller.isRunning() && smoothScroller instanceof GridLinearSmoothScroller) {
            mCurrentSmoothScroller = (GridLinearSmoothScroller) smoothScroller;
            if (mCurrentSmoothScroller instanceof PendingMoveSmoothScroller) {
                mPendingMoveSmoothScroller = (PendingMoveSmoothScroller) mCurrentSmoothScroller;
            } else {
                mPendingMoveSmoothScroller = null;
            }
        } else {
            mCurrentSmoothScroller = null;
            mPendingMoveSmoothScroller = null;
        }
    }

    void processPendingMovement(boolean forward) {
        if (forward ? hasCreatedLastItem() : hasCreatedFirstItem()) {
            return;
        }
        if (mPendingMoveSmoothScroller == null) {
            PendingMoveSmoothScroller linearSmoothScroller = new PendingMoveSmoothScroller(
                    forward ? 1 : -1, mNumRows > 1);
            mFocusPositionOffset = 0;
            startSmoothScroll(linearSmoothScroller);
        } else {
            if (forward) {
                mPendingMoveSmoothScroller.increasePendingMoves();
            } else {
                mPendingMoveSmoothScroller.decreasePendingMoves();
            }
        }
        int soundEffect;
        if (mOrientation == HORIZONTAL) {
            boolean rtl = getLayoutDirection() == ViewCompat.LAYOUT_DIRECTION_RTL;
            if (rtl) {
                soundEffect = forward ? AudioManager.FX_FOCUS_NAVIGATION_LEFT :
                        AudioManager.FX_FOCUS_NAVIGATION_RIGHT;
            } else {
                soundEffect = forward ? AudioManager.FX_FOCUS_NAVIGATION_RIGHT :
                        AudioManager.FX_FOCUS_NAVIGATION_LEFT;
            }
        } else {
            soundEffect = forward ? AudioManager.FX_FOCUS_NAVIGATION_DOWN :
                    AudioManager.FX_FOCUS_NAVIGATION_UP;
        }
        getAudioManager().playSoundEffect(soundEffect);
    }

    @Override
    public void onItemsAdded(@NonNull RecyclerView recyclerView, int positionStart,
            int itemCount) {
        if (DEBUG) {
            Log.v(getTag(), "onItemsAdded positionStart "
                    + positionStart + " itemCount " + itemCount);
        }
        if (mFocusPosition != NO_POSITION && mGrid != null && mGrid.getFirstVisibleIndex() >= 0
                && mFocusPositionOffset != Integer.MIN_VALUE) {
            int pos = mFocusPosition + mFocusPositionOffset;
            if (positionStart <= pos) {
                mFocusPositionOffset += itemCount;
            }
        }
        mChildrenStates.clear();
    }

    @Override
    public void onItemsChanged(@NonNull RecyclerView recyclerView) {
        if (DEBUG) Log.v(getTag(), "onItemsChanged");
        mFocusPositionOffset = 0;
        mChildrenStates.clear();
    }

    @Override
    public void onItemsRemoved(@NonNull RecyclerView recyclerView,
            int positionStart, int itemCount) {
        if (DEBUG) {
            Log.v(getTag(), "onItemsRemoved positionStart "
                    + positionStart + " itemCount " + itemCount);
        }
        if (mFocusPosition != NO_POSITION && mGrid != null && mGrid.getFirstVisibleIndex() >= 0
                && mFocusPositionOffset != Integer.MIN_VALUE) {
            int pos = mFocusPosition + mFocusPositionOffset;
            if (positionStart <= pos) {
                if (positionStart + itemCount > pos) {
                    // stop updating offset after the focus item was removed
                    mFocusPositionOffset += positionStart - pos;
                    mFocusPosition += mFocusPositionOffset;
                    mFocusPositionOffset = Integer.MIN_VALUE;
                } else {
                    mFocusPositionOffset -= itemCount;
                }
            }
        }
        mChildrenStates.clear();
    }

    @Override
    public void onItemsMoved(@NonNull RecyclerView recyclerView,
            int fromPosition, int toPosition, int itemCount) {
        if (DEBUG) {
            Log.v(getTag(), "onItemsMoved fromPosition "
                    + fromPosition + " toPosition " + toPosition);
        }
        if (mFocusPosition != NO_POSITION && mFocusPositionOffset != Integer.MIN_VALUE) {
            int pos = mFocusPosition + mFocusPositionOffset;
            if (fromPosition <= pos && pos < fromPosition + itemCount) {
                // moved items include focused position
                mFocusPositionOffset += toPosition - fromPosition;
            } else if (fromPosition < pos && toPosition > pos - itemCount) {
                // move items before focus position to after focused position
                mFocusPositionOffset -= itemCount;
            } else if (fromPosition > pos && toPosition < pos) {
                // move items after focus position to before focused position
                mFocusPositionOffset += itemCount;
            }
        }
        mChildrenStates.clear();
    }

    @Override
    public void onItemsUpdated(@NonNull RecyclerView recyclerView,
            int positionStart, int itemCount) {
        if (DEBUG) {
            Log.v(getTag(), "onItemsUpdated positionStart "
                    + positionStart + " itemCount " + itemCount);
        }
        for (int i = positionStart, end = positionStart + itemCount; i < end; i++) {
            mChildrenStates.remove(i);
        }
    }

    @Override
    public boolean onRequestChildFocus(@NonNull RecyclerView parent, @NonNull State state,
            @NonNull View child, @Nullable View focused) {
        if ((mFlag & PF_FOCUS_SEARCH_DISABLED) != 0) {
            return true;
        }
        if (getAdapterPositionByView(child) == NO_POSITION) {
            // This is could be the last view in DISAPPEARING animation.
            return true;
        }
        if ((mFlag & (PF_STAGE_MASK | PF_IN_SELECTION)) == 0) {
            scrollToView(child, focused, true);
        }
        return true;
    }

    @Override
    public boolean requestChildRectangleOnScreen(@NonNull RecyclerView parent,
            @NonNull View child, @NonNull Rect rect, boolean immediate) {
        if (DEBUG) Log.v(getTag(), "requestChildRectangleOnScreen " + child + " " + rect);
        return false;
    }

    void getViewSelectedOffsets(View view, int[] offsets) {
        if (mOrientation == HORIZONTAL) {
            offsets[0] = getPrimaryAlignedScrollDistance(view);
            offsets[1] = getSecondaryScrollDistance(view);
        } else {
            offsets[1] = getPrimaryAlignedScrollDistance(view);
            offsets[0] = getSecondaryScrollDistance(view);
        }
    }

    /**
     * Return the scroll delta on primary direction to make the view selected. If the return value
     * is 0, there is no need to scroll.
     */
    private int getPrimaryAlignedScrollDistance(View view) {
        return mWindowAlignment.mainAxis().getScroll(getViewCenter(view));
    }

    /**
     * Get adjusted primary position for a given childView (if there is multiple ItemAlignment
     * defined on the view).
     */
    private int getAdjustedPrimaryAlignedScrollDistance(int scrollPrimary, View view,
            View childView) {
        int subindex = getSubPositionByView(view, childView);
        if (subindex != 0) {
            final LayoutParams lp = (LayoutParams) view.getLayoutParams();
            scrollPrimary += lp.getAlignMultiple()[subindex] - lp.getAlignMultiple()[0];
        }
        return scrollPrimary;
    }

    private int getSecondaryScrollDistance(View view) {
        int viewCenterSecondary = getViewCenterSecondary(view);
        return mWindowAlignment.secondAxis().getScroll(viewCenterSecondary);
    }

    /**
     * Scroll to a given child view and change mFocusPosition. Ignored when in slideOut() state.
     */
    void scrollToView(View view, boolean smooth) {
        scrollToView(view, view == null ? null : view.findFocus(), smooth);
    }

    void scrollToView(View view, boolean smooth, int extraDelta, int extraDeltaSecondary) {
        scrollToView(view, view == null ? null : view.findFocus(), smooth, extraDelta,
                extraDeltaSecondary);
    }

    private void scrollToView(View view, View childView, boolean smooth) {
        scrollToView(view, childView, smooth, 0, 0);
    }

    /**
     * Scroll to a given child view and change mFocusPosition. Ignored when in slideOut() state.
     */
    private void scrollToView(View view, View childView, boolean smooth, int extraDelta,
            int extraDeltaSecondary) {
        if ((mFlag & PF_SLIDING) != 0) {
            return;
        }
        int newFocusPosition = getAdapterPositionByView(view);
        int newSubFocusPosition = getSubPositionByView(view, childView);
        if (newFocusPosition != mFocusPosition || newSubFocusPosition != mSubFocusPosition) {
            mFocusPosition = newFocusPosition;
            mSubFocusPosition = newSubFocusPosition;
            mFocusPositionOffset = 0;
            if ((mFlag & PF_STAGE_MASK) != PF_STAGE_LAYOUT) {
                dispatchChildSelected();
            }
            if (mBaseGridView.isChildrenDrawingOrderEnabledInternal()) {
                mBaseGridView.invalidate();
            }
        }
        if (view == null) {
            return;
        }
        if (!view.hasFocus() && mBaseGridView.hasFocus()) {
            // transfer focus to the child if it does not have focus yet (e.g. triggered
            // by setSelection())
            view.requestFocus();
        }
        if ((mFlag & PF_SCROLL_ENABLED) == 0 && smooth) {
            return;
        }
        if (getScrollPosition(view, childView, sTwoInts)
                || extraDelta != 0 || extraDeltaSecondary != 0) {
            scrollGrid(sTwoInts[0] + extraDelta, sTwoInts[1] + extraDeltaSecondary, smooth);
        }
    }

    boolean getScrollPosition(View view, View childView, int[] deltas) {
        switch (mFocusScrollStrategy) {
            case BaseGridView.FOCUS_SCROLL_ALIGNED:
            default:
                return getAlignedPosition(view, childView, deltas);
            case BaseGridView.FOCUS_SCROLL_ITEM:
            case BaseGridView.FOCUS_SCROLL_PAGE:
                return getNoneAlignedPosition(view, deltas);
        }
    }

    private boolean getNoneAlignedPosition(View view, int[] deltas) {
        int pos = getAdapterPositionByView(view);
        int viewMin = getViewMin(view);
        int viewMax = getViewMax(view);
        // we either align "firstView" to left/top padding edge
        // or align "lastView" to right/bottom padding edge
        View firstView = null;
        View lastView = null;
        int paddingMin = mWindowAlignment.mainAxis().getPaddingMin();
        int clientSize = mWindowAlignment.mainAxis().getClientSize();
        final int row = mGrid.getRowIndex(pos);
        if (viewMin < paddingMin) {
            // view enters low padding area:
            firstView = view;
            if (mFocusScrollStrategy == BaseGridView.FOCUS_SCROLL_PAGE) {
                // scroll one "page" left/top,
                // align first visible item of the "page" at the low padding edge.
                while (prependOneColumnVisibleItems()) {
                    CircularIntArray positions =
                            mGrid.getItemPositionsInRows(mGrid.getFirstVisibleIndex(), pos)[row];
                    firstView = findViewByPosition(positions.get(0));
                    if (viewMax - getViewMin(firstView) > clientSize) {
                        if (positions.size() > 2) {
                            firstView = findViewByPosition(positions.get(2));
                        }
                        break;
                    }
                }
            }
        } else if (viewMax > clientSize + paddingMin) {
            // view enters high padding area:
            if (mFocusScrollStrategy == BaseGridView.FOCUS_SCROLL_PAGE) {
                // scroll whole one page right/bottom, align view at the low padding edge.
                firstView = view;
                do {
                    CircularIntArray positions =
                            mGrid.getItemPositionsInRows(pos, mGrid.getLastVisibleIndex())[row];
                    lastView = findViewByPosition(positions.get(positions.size() - 1));
                    if (getViewMax(lastView) - viewMin > clientSize) {
                        lastView = null;
                        break;
                    }
                } while (appendOneColumnVisibleItems());
                if (lastView != null) {
                    // however if we reached end,  we should align last view.
                    firstView = null;
                }
            } else {
                lastView = view;
            }
        }
        int scrollPrimary = 0;
        if (firstView != null) {
            scrollPrimary = getViewMin(firstView) - paddingMin;
        } else if (lastView != null) {
            scrollPrimary = getViewMax(lastView) - (paddingMin + clientSize);
        }
        View secondaryAlignedView;
        if (firstView != null) {
            secondaryAlignedView = firstView;
        } else if (lastView != null) {
            secondaryAlignedView = lastView;
        } else {
            secondaryAlignedView = view;
        }
        int scrollSecondary = getSecondaryScrollDistance(secondaryAlignedView);
        if (scrollPrimary != 0 || scrollSecondary != 0) {
            deltas[0] = scrollPrimary;
            deltas[1] = scrollSecondary;
            return true;
        }
        return false;
    }

    private boolean getAlignedPosition(View view, View childView, int[] deltas) {
        int scrollPrimary = getPrimaryAlignedScrollDistance(view);
        if (childView != null) {
            scrollPrimary = getAdjustedPrimaryAlignedScrollDistance(scrollPrimary, view, childView);
        }
        int scrollSecondary = getSecondaryScrollDistance(view);
        if (DEBUG) {
            Log.v(getTag(), "getAlignedPosition " + scrollPrimary + " " + scrollSecondary
                    + " " + mPrimaryScrollExtra + " " + mWindowAlignment);
        }
        scrollPrimary += mPrimaryScrollExtra;
        if (scrollPrimary != 0 || scrollSecondary != 0) {
            deltas[0] = scrollPrimary;
            deltas[1] = scrollSecondary;
            return true;
        } else {
            deltas[0] = 0;
            deltas[1] = 0;
        }
        return false;
    }

    private void scrollGrid(int scrollPrimary, int scrollSecondary, boolean smooth) {
        if ((mFlag & PF_STAGE_MASK) == PF_STAGE_LAYOUT) {
            scrollDirectionPrimary(scrollPrimary);
            scrollDirectionSecondary(scrollSecondary);
        } else {
            int scrollX;
            int scrollY;
            if (mOrientation == HORIZONTAL) {
                scrollX = scrollPrimary;
                scrollY = scrollSecondary;
            } else {
                scrollX = scrollSecondary;
                scrollY = scrollPrimary;
            }
            if (smooth) {
                mBaseGridView.smoothScrollBy(scrollX, scrollY);
            } else {
                mBaseGridView.scrollBy(scrollX, scrollY);
                dispatchChildSelectedAndPositioned();
            }
        }
    }

    void setPruneChild(boolean pruneChild) {
        if (((mFlag & PF_PRUNE_CHILD) != 0) != pruneChild) {
            mFlag = (mFlag & ~PF_PRUNE_CHILD) | (pruneChild ? PF_PRUNE_CHILD : 0);
            if (pruneChild) {
                requestLayout();
            }
        }
    }

    boolean getPruneChild() {
        return (mFlag & PF_PRUNE_CHILD) != 0;
    }

    void setScrollEnabled(boolean scrollEnabled) {
        if (((mFlag & PF_SCROLL_ENABLED) != 0) != scrollEnabled) {
            mFlag = (mFlag & ~PF_SCROLL_ENABLED) | (scrollEnabled ? PF_SCROLL_ENABLED : 0);
            if (((mFlag & PF_SCROLL_ENABLED) != 0)
                    && mFocusScrollStrategy == BaseGridView.FOCUS_SCROLL_ALIGNED
                    && mFocusPosition != NO_POSITION) {
                scrollToSelection(mFocusPosition, mSubFocusPosition,
                        true, mPrimaryScrollExtra);
            }
        }
    }

    boolean isScrollEnabled() {
        return (mFlag & PF_SCROLL_ENABLED) != 0;
    }

    private int findImmediateChildIndex(View view) {
        if (view != null && mBaseGridView != null && view != mBaseGridView) {
            view = findContainingItemView(view);
            if (view != null) {
                for (int i = 0, count = getChildCount(); i < count; i++) {
                    if (getChildAt(i) == view) {
                        return i;
                    }
                }
            }
        }
        return NO_POSITION;
    }

    void onFocusChanged(boolean gainFocus, int direction, Rect previouslyFocusedRect) {
        if (gainFocus) {
            // if gridview.requestFocus() is called, select first focusable child.
            int i = mFocusPosition;
            while (true) {
                View view = findViewByPosition(i);
                if (view == null) {
                    break;
                }
                if (view.getVisibility() == View.VISIBLE && view.hasFocusable()) {
                    view.requestFocus();
                    break;
                }
                i++;
            }
        }
    }

    void setFocusSearchDisabled(boolean disabled) {
        mFlag = (mFlag & ~PF_FOCUS_SEARCH_DISABLED) | (disabled ? PF_FOCUS_SEARCH_DISABLED : 0);
    }

    boolean isFocusSearchDisabled() {
        return (mFlag & PF_FOCUS_SEARCH_DISABLED) != 0;
    }

    @Nullable
    @Override
    public View onInterceptFocusSearch(@Nullable View focused, int direction) {
        if ((mFlag & PF_FOCUS_SEARCH_DISABLED) != 0) {
            return focused;
        }

        final FocusFinder ff = FocusFinder.getInstance();
        View result = null;
        if (direction == View.FOCUS_FORWARD || direction == View.FOCUS_BACKWARD) {
            // convert direction to absolute direction and see if we have a view there and if not
            // tell LayoutManager to add if it can.
            if (canScrollVertically()) {
                final int absDir =
                        direction == View.FOCUS_FORWARD ? View.FOCUS_DOWN : View.FOCUS_UP;
                result = ff.findNextFocus(mBaseGridView, focused, absDir);
            }
            if (canScrollHorizontally()) {
                boolean rtl = getLayoutDirection() == ViewCompat.LAYOUT_DIRECTION_RTL;
                final int absDir = (direction == View.FOCUS_FORWARD) ^ rtl
                        ? View.FOCUS_RIGHT : View.FOCUS_LEFT;
                result = ff.findNextFocus(mBaseGridView, focused, absDir);
            }
        } else {
            result = ff.findNextFocus(mBaseGridView, focused, direction);
        }
        if (result != null) {
            return result;
        }

        if (mBaseGridView.getDescendantFocusability() == ViewGroup.FOCUS_BLOCK_DESCENDANTS) {
            return mBaseGridView.getParent().focusSearch(focused, direction);
        }

        if (DEBUG) Log.v(getTag(), "regular focusSearch failed direction " + direction);
        int movement = getMovement(direction);
        final boolean isScroll = mBaseGridView.getScrollState() != RecyclerView.SCROLL_STATE_IDLE;
        if (movement == NEXT_ITEM) {
            if (isScroll || (mFlag & PF_FOCUS_OUT_BACK) == 0) {
                result = focused;
            }
            if ((mFlag & PF_SCROLL_ENABLED) != 0 && !hasCreatedLastItem()) {
                processPendingMovement(true);
                result = focused;
            }
        } else if (movement == PREV_ITEM) {
            if (isScroll || (mFlag & PF_FOCUS_OUT_FRONT) == 0) {
                result = focused;
            }
            if ((mFlag & PF_SCROLL_ENABLED) != 0 && !hasCreatedFirstItem()) {
                processPendingMovement(false);
                result = focused;
            }
        } else if (movement == NEXT_ROW) {
            if (isScroll || (mFlag & PF_FOCUS_OUT_SIDE_END) == 0) {
                result = focused;
            }
        } else if (movement == PREV_ROW) {
            if (isScroll || (mFlag & PF_FOCUS_OUT_SIDE_START) == 0) {
                result = focused;
            }
        }
        if (result != null) {
            return result;
        }

        if (DEBUG) Log.v(getTag(), "now focusSearch in parent");
        result = mBaseGridView.getParent().focusSearch(focused, direction);
        if (result != null) {
            return result;
        }
        return focused != null ? focused : mBaseGridView;
    }

    boolean hasPreviousViewInSameRow(int pos) {
        if (mGrid == null || pos == NO_POSITION || mGrid.getFirstVisibleIndex() < 0) {
            return false;
        }
        if (mGrid.getFirstVisibleIndex() > 0) {
            return true;
        }
        final int focusedRow = mGrid.getLocation(pos).mRow;
        for (int i = getChildCount() - 1; i >= 0; i--) {
            int position = getAdapterPositionByIndex(i);
            Grid.Location loc = mGrid.getLocation(position);
            if (loc != null && loc.mRow == focusedRow) {
                if (position < pos) {
                    return true;
                }
            }
        }
        return false;
    }

    @Override
    public boolean onAddFocusables(@NonNull RecyclerView recyclerView,
            @SuppressLint("ConcreteCollection") @NonNull ArrayList<View> views, int direction,
            int focusableMode) {
        if ((mFlag & PF_FOCUS_SEARCH_DISABLED) != 0) {
            return true;
        }
        // If this viewgroup or one of its children currently has focus then we
        // consider our children for focus searching in main direction on the same row.
        // If this viewgroup has no focus and using focus align, we want the system
        // to ignore our children and pass focus to the viewgroup, which will pass
        // focus on to its children appropriately.
        // If this viewgroup has no focus and not using focus align, we want to
        // consider the child that does not overlap with padding area.
        if (recyclerView.hasFocus()) {
            if (mPendingMoveSmoothScroller != null) {
                // don't find next focusable if has pending movement.
                return true;
            }
            final int movement = getMovement(direction);
            final View focused = recyclerView.findFocus();
            final int focusedIndex = findImmediateChildIndex(focused);
            final int focusedPos = getAdapterPositionByIndex(focusedIndex);
            // Even if focusedPos != NO_POSITION, findViewByPosition could return null if the view
            // is ignored or getLayoutPosition does not match the adapter position of focused view.
            final View immediateFocusedChild = (focusedPos == NO_POSITION) ? null
                    : findViewByPosition(focusedPos);
            // Add focusables of focused item.
            if (immediateFocusedChild != null) {
                immediateFocusedChild.addFocusables(views, direction, focusableMode);
            }
            if (mGrid == null || getChildCount() == 0) {
                // no grid information, or no child, bail out.
                return true;
            }
            if ((movement == NEXT_ROW || movement == PREV_ROW) && mGrid.getNumRows() <= 1) {
                // For single row, cannot navigate to previous/next row.
                return true;
            }
            // Add focusables of neighbor depending on the focus search direction.
            final int focusedRow = mGrid != null && immediateFocusedChild != null
                    ? mGrid.getLocation(focusedPos).mRow : NO_POSITION;
            final int focusableCount = views.size();
            int inc = movement == NEXT_ITEM || movement == NEXT_ROW ? 1 : -1;
            int loop_end = inc > 0 ? getChildCount() - 1 : 0;
            int loop_start;
            if (focusedIndex == NO_POSITION) {
                loop_start = inc > 0 ? 0 : getChildCount() - 1;
            } else {
                loop_start = focusedIndex + inc;
            }
            for (int i = loop_start; inc > 0 ? i <= loop_end : i >= loop_end; i += inc) {
                final View child = getChildAt(i);
                if (child.getVisibility() != View.VISIBLE || !child.hasFocusable()) {
                    continue;
                }
                // if there wasn't any focused item, add the very first focusable
                // items and stop.
                if (immediateFocusedChild == null) {
                    child.addFocusables(views, direction, focusableMode);
                    if (views.size() > focusableCount) {
                        break;
                    }
                    continue;
                }
                int position = getAdapterPositionByIndex(i);
                Grid.Location loc = mGrid.getLocation(position);
                if (loc == null) {
                    continue;
                }
                if (movement == NEXT_ITEM) {
                    // Add first focusable item on the same row
                    if (loc.mRow == focusedRow && position > focusedPos) {
                        child.addFocusables(views, direction, focusableMode);
                        if (views.size() > focusableCount) {
                            break;
                        }
                    }
                } else if (movement == PREV_ITEM) {
                    // Add first focusable item on the same row
                    if (loc.mRow == focusedRow && position < focusedPos) {
                        child.addFocusables(views, direction, focusableMode);
                        if (views.size() > focusableCount) {
                            break;
                        }
                    }
                } else if (movement == NEXT_ROW) {
                    // Add all focusable items after this item whose row index is bigger
                    if (loc.mRow == focusedRow) {
                        continue;
                    } else if (loc.mRow < focusedRow) {
                        break;
                    }
                    child.addFocusables(views, direction, focusableMode);
                } else if (movement == PREV_ROW) {
                    // Add all focusable items before this item whose row index is smaller
                    if (loc.mRow == focusedRow) {
                        continue;
                    } else if (loc.mRow > focusedRow) {
                        break;
                    }
                    child.addFocusables(views, direction, focusableMode);
                }
            }
        } else {
            int focusableCount = views.size();
            if (mFocusScrollStrategy != BaseGridView.FOCUS_SCROLL_ALIGNED) {
                // adding views not overlapping padding area to avoid scrolling in gaining focus
                int left = mWindowAlignment.mainAxis().getPaddingMin();
                int right = mWindowAlignment.mainAxis().getClientSize() + left;
                for (int i = 0, count = getChildCount(); i < count; i++) {
                    View child = getChildAt(i);
                    if (child.getVisibility() == View.VISIBLE) {
                        if (getViewMin(child) >= left && getViewMax(child) <= right) {
                            child.addFocusables(views, direction, focusableMode);
                        }
                    }
                }
                // if we cannot find any, then just add all children.
                if (views.size() == focusableCount) {
                    for (int i = 0, count = getChildCount(); i < count; i++) {
                        View child = getChildAt(i);
                        if (child.getVisibility() == View.VISIBLE) {
                            child.addFocusables(views, direction, focusableMode);
                        }
                    }
                }
            } else {
                View view = findViewByPosition(mFocusPosition);
                if (view != null) {
                    view.addFocusables(views, direction, focusableMode);
                }
            }
            // if still cannot find any, fall through and add itself
            if (views.size() != focusableCount) {
                return true;
            }
            if (recyclerView.isFocusable()) {
                views.add(recyclerView);
            }
        }
        return true;
    }

    boolean hasCreatedLastItem() {
        int count = getItemCount();
        return count == 0 || mBaseGridView.findViewHolderForAdapterPosition(count - 1) != null;
    }

    boolean hasCreatedFirstItem() {
        int count = getItemCount();
        return count == 0 || mBaseGridView.findViewHolderForAdapterPosition(0) != null;
    }

    boolean isItemFullyVisible(int pos) {
        RecyclerView.ViewHolder vh = mBaseGridView.findViewHolderForAdapterPosition(pos);
        if (vh == null) {
            return false;
        }
        return vh.itemView.getLeft() >= 0 && vh.itemView.getRight() <= mBaseGridView.getWidth()
                && vh.itemView.getTop() >= 0 && vh.itemView.getBottom()
                <= mBaseGridView.getHeight();
    }

    boolean canScrollTo(View view) {
        return view.getVisibility() == View.VISIBLE && (!hasFocus() || view.hasFocusable());
    }

    boolean gridOnRequestFocusInDescendants(RecyclerView recyclerView, int direction,
            Rect previouslyFocusedRect) {
        switch (mFocusScrollStrategy) {
            case BaseGridView.FOCUS_SCROLL_ALIGNED:
            default:
                return gridOnRequestFocusInDescendantsAligned(
                        direction, previouslyFocusedRect);
            case BaseGridView.FOCUS_SCROLL_PAGE:
            case BaseGridView.FOCUS_SCROLL_ITEM:
                return gridOnRequestFocusInDescendantsUnaligned(
                        direction, previouslyFocusedRect);
        }
    }

    private boolean gridOnRequestFocusInDescendantsAligned(int direction,
            Rect previouslyFocusedRect) {
        View view = findViewByPosition(mFocusPosition);
        if (view != null) {
            boolean result = view.requestFocus(direction, previouslyFocusedRect);
            if (!result && DEBUG) {
                Log.w(getTag(), "failed to request focus on " + view);
            }
            return result;
        }
        return false;
    }

    private boolean gridOnRequestFocusInDescendantsUnaligned(int direction,
            Rect previouslyFocusedRect) {
        // focus to view not overlapping padding area to avoid scrolling in gaining focus
        int index;
        int increment;
        int end;
        int count = getChildCount();
        if ((direction & View.FOCUS_FORWARD) != 0) {
            index = 0;
            increment = 1;
            end = count;
        } else {
            index = count - 1;
            increment = -1;
            end = -1;
        }
        int left = mWindowAlignment.mainAxis().getPaddingMin();
        int right = mWindowAlignment.mainAxis().getClientSize() + left;
        for (int i = index; i != end; i += increment) {
            View child = getChildAt(i);
            if (child.getVisibility() == View.VISIBLE) {
                if (getViewMin(child) >= left && getViewMax(child) <= right) {
                    if (child.requestFocus(direction, previouslyFocusedRect)) {
                        return true;
                    }
                }
            }
        }
        return false;
    }

    private static final int PREV_ITEM = 0;
    private static final int NEXT_ITEM = 1;
    private static final int PREV_ROW = 2;
    private static final int NEXT_ROW = 3;

    private int getMovement(int direction) {
        int movement = View.FOCUS_LEFT;

        if (mOrientation == HORIZONTAL) {
            switch (direction) {
                case View.FOCUS_LEFT:
                    movement = (mFlag & PF_REVERSE_FLOW_PRIMARY) == 0 ? PREV_ITEM : NEXT_ITEM;
                    break;
                case View.FOCUS_RIGHT:
                    movement = (mFlag & PF_REVERSE_FLOW_PRIMARY) == 0 ? NEXT_ITEM : PREV_ITEM;
                    break;
                case View.FOCUS_UP:
                    movement = PREV_ROW;
                    break;
                case View.FOCUS_DOWN:
                    movement = NEXT_ROW;
                    break;
            }
        } else if (mOrientation == VERTICAL) {
            switch (direction) {
                case View.FOCUS_LEFT:
                    movement = (mFlag & PF_REVERSE_FLOW_SECONDARY) == 0 ? PREV_ROW : NEXT_ROW;
                    break;
                case View.FOCUS_RIGHT:
                    movement = (mFlag & PF_REVERSE_FLOW_SECONDARY) == 0 ? NEXT_ROW : PREV_ROW;
                    break;
                case View.FOCUS_UP:
                    movement = PREV_ITEM;
                    break;
                case View.FOCUS_DOWN:
                    movement = NEXT_ITEM;
                    break;
            }
        }

        return movement;
    }

    int getChildDrawingOrder(RecyclerView recyclerView, int childCount, int i) {
        View view = findViewByPosition(mFocusPosition);
        if (view == null) {
            return i;
        }
        int focusIndex = recyclerView.indexOfChild(view);
        // supposely 0 1 2 3 4 5 6 7 8 9, 4 is the center item
        // drawing order is 0 1 2 3 9 8 7 6 5 4
        if (i < focusIndex) {
            return i;
        } else if (i < childCount - 1) {
            return focusIndex + childCount - 1 - i;
        } else {
            return focusIndex;
        }
    }

    @Override
    public void onAdapterChanged(@Nullable RecyclerView.Adapter oldAdapter,
            @Nullable RecyclerView.Adapter newAdapter) {
        if (DEBUG) Log.v(getTag(), "onAdapterChanged to " + newAdapter);
        if (oldAdapter != null) {
            discardLayoutInfo();
            mFocusPosition = NO_POSITION;
            mFocusPositionOffset = 0;
            mChildrenStates.clear();
        }
        if (newAdapter instanceof FacetProviderAdapter) {
            mFacetProviderAdapter = (FacetProviderAdapter) newAdapter;
        } else {
            mFacetProviderAdapter = null;
        }
        super.onAdapterChanged(oldAdapter, newAdapter);
    }

    private void discardLayoutInfo() {
        mGrid = null;
        mRowSizeSecondary = null;
        mFlag &= ~PF_ROW_SECONDARY_SIZE_REFRESH;
    }

    void setLayoutEnabled(boolean layoutEnabled) {
        if (((mFlag & PF_LAYOUT_ENABLED) != 0) != layoutEnabled) {
            mFlag = (mFlag & ~PF_LAYOUT_ENABLED) | (layoutEnabled ? PF_LAYOUT_ENABLED : 0);
            requestLayout();
        }
    }

    void setChildrenVisibility(int visibility) {
        mChildVisibility = visibility;
        if (mChildVisibility != -1) {
            int count = getChildCount();
            for (int i = 0; i < count; i++) {
                getChildAt(i).setVisibility(mChildVisibility);
            }
        }
    }

    @SuppressLint("BanParcelableUsage")
    static final class SavedState implements Parcelable {

        int mIndex; // index inside adapter of the current view
        Bundle mChildStates = Bundle.EMPTY;

        @Override
        public void writeToParcel(Parcel out, int flags) {
            out.writeInt(mIndex);
            out.writeBundle(mChildStates);
        }

        @SuppressWarnings("hiding")
        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];
                    }
                };

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

        SavedState(Parcel in) {
            mIndex = in.readInt();
            mChildStates = in.readBundle(GridLayoutManager.class.getClassLoader());
        }

        SavedState() {
        }
    }

    @NonNull
    @Override
    public Parcelable onSaveInstanceState() {
        if (DEBUG) Log.v(getTag(), "onSaveInstanceState getSelection() " + getSelection());
        SavedState ss = new SavedState();
        // save selected index
        ss.mIndex = getSelection();
        // save offscreen child (state when they are recycled)
        Bundle bundle = mChildrenStates.saveAsBundle();
        // save views currently is on screen (TODO save cached views)
        for (int i = 0, count = getChildCount(); i < count; i++) {
            View view = getChildAt(i);
            int position = getAdapterPositionByView(view);
            if (position != NO_POSITION) {
                bundle = mChildrenStates.saveOnScreenView(bundle, view, position);
            }
        }
        ss.mChildStates = bundle;
        return ss;
    }

    void onChildRecycled(RecyclerView.ViewHolder holder) {
        final int position = holder.getAbsoluteAdapterPosition();
        if (position != NO_POSITION) {
            mChildrenStates.saveOffscreenView(holder.itemView, position);
        }
    }

    @Override
    public void onRestoreInstanceState(@Nullable Parcelable state) {
        if (!(state instanceof SavedState)) {
            return;
        }
        SavedState loadingState = (SavedState) state;
        mFocusPosition = loadingState.mIndex;
        mFocusPositionOffset = 0;
        mChildrenStates.loadFromBundle(loadingState.mChildStates);
        mFlag |= PF_FORCE_FULL_LAYOUT;
        requestLayout();
        if (DEBUG) Log.v(getTag(), "onRestoreInstanceState mFocusPosition " + mFocusPosition);
    }

    @Override
    public int getRowCountForAccessibility(@NonNull RecyclerView.Recycler recycler,
            @NonNull RecyclerView.State state) {
        if (mOrientation == HORIZONTAL && mGrid != null) {
            return mGrid.getNumRows();
        }
        return super.getRowCountForAccessibility(recycler, state);
    }

    @Override
    public int getColumnCountForAccessibility(@NonNull RecyclerView.Recycler recycler,
            @NonNull RecyclerView.State state) {
        if (mOrientation == VERTICAL && mGrid != null) {
            return mGrid.getNumRows();
        }
        return super.getColumnCountForAccessibility(recycler, state);
    }

    @Override
    public void onInitializeAccessibilityNodeInfoForItem(@NonNull RecyclerView.Recycler recycler,
            @NonNull RecyclerView.State state, @NonNull View host,
            @NonNull AccessibilityNodeInfoCompat info) {
        ViewGroup.LayoutParams lp = host.getLayoutParams();
        if (mGrid == null || !(lp instanceof LayoutParams)) {
            return;
        }
        LayoutParams glp = (LayoutParams) lp;
        int position = glp.getAbsoluteAdapterPosition();
        int rowIndex = position >= 0 ? mGrid.getRowIndex(position) : -1;
        if (rowIndex < 0) {
            return;
        }
        int guessSpanIndex = position / mGrid.getNumRows();
        if (mOrientation == HORIZONTAL) {
            info.setCollectionItemInfo(AccessibilityNodeInfoCompat.CollectionItemInfoCompat.obtain(
                    rowIndex, 1, guessSpanIndex, 1, false, false));
        } else {
            info.setCollectionItemInfo(AccessibilityNodeInfoCompat.CollectionItemInfoCompat.obtain(
                    guessSpanIndex, 1, rowIndex, 1, false, false));
        }
    }

    /*
     * Leanback widget is different than the default implementation because the "scroll" is driven
     * by selection change.
     */
    @Override
    public boolean performAccessibilityAction(@NonNull Recycler recycler, @NonNull State state,
            int action, @Nullable Bundle args) {
        if (!isScrollEnabled()) {
            // eat action request so that talkback wont focus out of RV
            return true;
        }
        saveContext(recycler, state);
        int translatedAction = action;
        boolean reverseFlowPrimary = (mFlag & PF_REVERSE_FLOW_PRIMARY) != 0;
        if (Build.VERSION.SDK_INT >= 23) {
            if (mOrientation == HORIZONTAL) {
                if (action == AccessibilityNodeInfoCompat.AccessibilityActionCompat
                        .ACTION_SCROLL_LEFT.getId()) {
                    translatedAction = reverseFlowPrimary
                            ? AccessibilityNodeInfoCompat.ACTION_SCROLL_FORWARD :
                            AccessibilityNodeInfoCompat.ACTION_SCROLL_BACKWARD;
                } else if (action == AccessibilityNodeInfoCompat.AccessibilityActionCompat
                        .ACTION_SCROLL_RIGHT.getId()) {
                    translatedAction = reverseFlowPrimary
                            ? AccessibilityNodeInfoCompat.ACTION_SCROLL_BACKWARD :
                            AccessibilityNodeInfoCompat.ACTION_SCROLL_FORWARD;
                }
            } else { // VERTICAL layout
                if (action == AccessibilityNodeInfoCompat.AccessibilityActionCompat.ACTION_SCROLL_UP
                        .getId()) {
                    translatedAction = AccessibilityNodeInfoCompat.ACTION_SCROLL_BACKWARD;
                } else if (action == AccessibilityNodeInfoCompat.AccessibilityActionCompat
                        .ACTION_SCROLL_DOWN.getId()) {
                    translatedAction = AccessibilityNodeInfoCompat.ACTION_SCROLL_FORWARD;
                }
            }
        }
        boolean scrollingReachedBeginning = (mFocusPosition == 0
                && translatedAction == AccessibilityNodeInfoCompat.ACTION_SCROLL_BACKWARD);
        boolean scrollingReachedEnd = (mFocusPosition == state.getItemCount() - 1
                && translatedAction == AccessibilityNodeInfoCompat.ACTION_SCROLL_FORWARD);
        if (scrollingReachedBeginning || scrollingReachedEnd) {
            // Send a fake scroll completion event to notify Talkback that the scroll event was
            // successful. Hence, Talkback will only look for next focus within the RecyclerView.
            // Not sending this will result in Talkback classifying it as a failed scroll event, and
            // will try to jump focus out of the RecyclerView.
            // We know at this point that either focusOutFront or focusOutEnd is true (or both),
            // because otherwise, we never hit ACTION_SCROLL_BACKWARD/FORWARD here.
            sendTypeViewScrolledAccessibilityEvent();
        } else {
            switch (translatedAction) {
                case AccessibilityNodeInfoCompat.ACTION_SCROLL_BACKWARD:
                    processPendingMovement(false);
                    processSelectionMoves(false, -1);
                    break;
                case AccessibilityNodeInfoCompat.ACTION_SCROLL_FORWARD:
                    processPendingMovement(true);
                    processSelectionMoves(false, 1);
                    break;
            }
        }
        leaveContext();
        return true;
    }

    @SuppressWarnings("deprecation")
    private void sendTypeViewScrolledAccessibilityEvent() {
        AccessibilityEvent event = AccessibilityEvent.obtain(AccessibilityEvent.TYPE_VIEW_SCROLLED);
        mBaseGridView.onInitializeAccessibilityEvent(event);
        mBaseGridView.requestSendAccessibilityEvent(mBaseGridView, event);
    }

    /*
     * Move mFocusPosition multiple steps on the same row in main direction.
     * Stops when moves are all consumed or reach first/last visible item.
     * Returning remaining moves.
     */
    int processSelectionMoves(boolean preventScroll, int moves) {
        if (mGrid == null) {
            return moves;
        }
        int focusPosition = mFocusPosition;
        int focusedRow = focusPosition != NO_POSITION
                ? mGrid.getRowIndex(focusPosition) : NO_POSITION;
        View newSelected = null;
        for (int i = 0, count = getChildCount(); i < count && moves != 0; i++) {
            int index = moves > 0 ? i : count - 1 - i;
            final View child = getChildAt(index);
            if (!canScrollTo(child)) {
                continue;
            }
            int position = getAdapterPositionByIndex(index);
            int rowIndex = mGrid.getRowIndex(position);
            if (focusedRow == NO_POSITION) {
                focusPosition = position;
                newSelected = child;
                focusedRow = rowIndex;
            } else if (rowIndex == focusedRow) {
                if ((moves > 0 && position > focusPosition)
                        || (moves < 0 && position < focusPosition)) {
                    focusPosition = position;
                    newSelected = child;
                    if (moves > 0) {
                        moves--;
                    } else {
                        moves++;
                    }
                }
            }
        }
        if (newSelected != null) {
            if (preventScroll) {
                if (hasFocus()) {
                    mFlag |= PF_IN_SELECTION;
                    newSelected.requestFocus();
                    mFlag &= ~PF_IN_SELECTION;
                }
                mFocusPosition = focusPosition;
                mSubFocusPosition = 0;
            } else {
                scrollToView(newSelected, true);
            }
        }
        return moves;
    }

    private void addA11yActionMovingBackward(AccessibilityNodeInfoCompat info,
            boolean reverseFlowPrimary) {
        if (Build.VERSION.SDK_INT >= 23) {
            if (mOrientation == HORIZONTAL) {
                info.addAction(reverseFlowPrimary
                        ? AccessibilityNodeInfoCompat.AccessibilityActionCompat
                        .ACTION_SCROLL_RIGHT :
                        AccessibilityNodeInfoCompat.AccessibilityActionCompat
                                .ACTION_SCROLL_LEFT);
            } else {
                info.addAction(
                        AccessibilityNodeInfoCompat.AccessibilityActionCompat.ACTION_SCROLL_UP);
            }
        } else {
            info.addAction(AccessibilityNodeInfoCompat.ACTION_SCROLL_BACKWARD);
        }
        info.setScrollable(true);
    }

    private void addA11yActionMovingForward(AccessibilityNodeInfoCompat info,
            boolean reverseFlowPrimary) {
        if (Build.VERSION.SDK_INT >= 23) {
            if (mOrientation == HORIZONTAL) {
                info.addAction(reverseFlowPrimary
                        ? AccessibilityNodeInfoCompat.AccessibilityActionCompat
                        .ACTION_SCROLL_LEFT :
                        AccessibilityNodeInfoCompat.AccessibilityActionCompat
                                .ACTION_SCROLL_RIGHT);
            } else {
                info.addAction(AccessibilityNodeInfoCompat.AccessibilityActionCompat
                        .ACTION_SCROLL_DOWN);
            }
        } else {
            info.addAction(AccessibilityNodeInfoCompat.ACTION_SCROLL_FORWARD);
        }
        info.setScrollable(true);
    }

    @Override
    public void onInitializeAccessibilityNodeInfo(@NonNull Recycler recycler,
            @NonNull State state, @NonNull AccessibilityNodeInfoCompat info) {
        saveContext(recycler, state);
        int count = state.getItemCount();
        // reverseFlowPrimary is whether we are in LTR/RTL mode.
        boolean reverseFlowPrimary = (mFlag & PF_REVERSE_FLOW_PRIMARY) != 0;
        // If focusOutFront/focusOutEnd is false, override Talkback in handling
        // backward/forward actions by adding such actions to supported action list.
        if ((mFlag & PF_FOCUS_OUT_FRONT) == 0 || (count > 1 && !isItemFullyVisible(0))) {
            addA11yActionMovingBackward(info, reverseFlowPrimary);
        }
        if ((mFlag & PF_FOCUS_OUT_BACK) == 0 || (count > 1 && !isItemFullyVisible(count - 1))) {
            addA11yActionMovingForward(info, reverseFlowPrimary);
        }
        final AccessibilityNodeInfoCompat.CollectionInfoCompat collectionInfo =
                AccessibilityNodeInfoCompat.CollectionInfoCompat
                        .obtain(getRowCountForAccessibility(recycler, state),
                                getColumnCountForAccessibility(recycler, state),
                                isLayoutHierarchical(recycler, state),
                                getSelectionModeForAccessibility(recycler, state));
        info.setCollectionInfo(collectionInfo);
        // Set the class name so this is treated as a grid. A11y services should identify grids
        // and list via CollectionInfos, but an almost empty grid may be incorrectly identified
        // as a list.
        info.setClassName(GridView.class.getName());
        leaveContext();
    }
}