blob: f31e65ce11459a6337f456f7c26b4704ee33c8d3 [file] [log] [blame]
/*
* 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.os.TraceCompat;
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;
static final boolean TRACE = 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;
}
if (TRACE) TraceCompat.beginSection("onChildSelected");
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);
}
if (TRACE) TraceCompat.endSection();
// 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;
}
if (TRACE) TraceCompat.beginSection("onChildSelectedAndPositioned");
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);
}
if (TRACE) TraceCompat.endSection();
}
@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;
}
if (TRACE) TraceCompat.beginSection("processRowSizeSecondary");
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;
}
}
if (TRACE) TraceCompat.endSection();
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) {
if (TRACE) TraceCompat.beginSection("measureChild");
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);
if (TRACE) TraceCompat.endSection();
}
/**
* 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) {
if (TRACE) TraceCompat.beginSection("createItem");
if (TRACE) TraceCompat.beginSection("getview");
View v = getViewForPosition(index - mPositionDeltaInPreLayout);
if (TRACE) TraceCompat.endSection();
LayoutParams lp = (LayoutParams) v.getLayoutParams();
// See recyclerView docs: we don't need re-add scraped view if it was removed.
if (!lp.isItemRemoved()) {
if (TRACE) TraceCompat.beginSection("addView");
if (disappearingItem) {
if (append) {
addDisappearingView(v);
} else {
addDisappearingView(v, 0);
}
} else {
if (append) {
addView(v);
} else {
addView(v, 0);
}
}
if (TRACE) TraceCompat.endSection();
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 (TRACE) TraceCompat.endSection();
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) {
if (TRACE) TraceCompat.beginSection("removeItem");
View v = findViewByPosition(index - mPositionDeltaInPreLayout);
if ((mFlag & PF_STAGE_MASK) == PF_STAGE_LAYOUT) {
detachAndScrapView(v, mRecycler);
} else {
removeAndRecycleView(v, mRecycler);
}
if (TRACE) TraceCompat.endSection();
}
@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) {
if (TRACE) TraceCompat.beginSection("layoutChild");
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);
if (TRACE) TraceCompat.endSection();
}
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 (TRACE) TraceCompat.beginSection("removeAndRecycleAllViews");
if (DEBUG) Log.v(TAG, "removeAndRecycleAllViews " + getChildCount());
for (int i = getChildCount() - 1; i >= 0; i--) {
removeAndRecycleViewAt(i, recycler);
}
if (TRACE) TraceCompat.endSection();
}
// 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) {
if (TRACE) TraceCompat.beginSection("scrollPrimary");
// 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) {
if (TRACE) TraceCompat.endSection();
return 0;
}
offsetChildrenPrimary(-da);
if ((mFlag & PF_STAGE_MASK) == PF_STAGE_LAYOUT) {
updateScrollLimits();
if (TRACE) TraceCompat.endSection();
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 (TRACE) TraceCompat.beginSection("remove");
if ((mFlag & PF_REVERSE_FLOW_PRIMARY) != 0 ? da > 0 : da < 0) {
removeInvisibleViewsAtEnd();
} else {
removeInvisibleViewsAtFront();
}
if (TRACE) TraceCompat.endSection();
updated |= getChildCount() < childCount;
if (updated) {
updateRowSecondarySizeRefresh();
}
mBaseGridView.invalidate();
updateScrollLimits();
if (TRACE) TraceCompat.endSection();
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) {
if (TRACE) TraceCompat.beginSection("scrollToSelection");
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();
}
}
}
if (TRACE) TraceCompat.endSection();
}
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();
}
}