/* * Copyright (C) 2017 The Android Open Source Project * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package com.android.internal.widget; import static com.android.internal.widget.RecyclerView.NO_POSITION; import android.content.Context; import android.graphics.PointF; import android.os.Parcel; import android.os.Parcelable; import android.util.AttributeSet; import android.util.Log; import android.view.View; import android.view.ViewGroup; import android.view.accessibility.AccessibilityEvent; import com.android.internal.widget.RecyclerView.LayoutParams; import com.android.internal.widget.helper.ItemTouchHelper; import java.util.List; /** * A {@link com.android.internal.widget.RecyclerView.LayoutManager} implementation which provides * similar functionality to {@link android.widget.ListView}. */ public class LinearLayoutManager extends RecyclerView.LayoutManager implements ItemTouchHelper.ViewDropHandler, RecyclerView.SmoothScroller.ScrollVectorProvider { private static final String TAG = "LinearLayoutManager"; static final boolean DEBUG = false; public static final int HORIZONTAL = OrientationHelper.HORIZONTAL; public static final int VERTICAL = OrientationHelper.VERTICAL; public static final int INVALID_OFFSET = Integer.MIN_VALUE; /** * While trying to find next view to focus, LayoutManager will not try to scroll more * than this factor times the total space of the list. If layout is vertical, total space is the * height minus padding, if layout is horizontal, total space is the width minus padding. */ private static final float MAX_SCROLL_FACTOR = 1 / 3f; /** * Current orientation. Either {@link #HORIZONTAL} or {@link #VERTICAL} */ int mOrientation; /** * Helper class that keeps temporary layout state. * It does not keep state after layout is complete but we still keep a reference to re-use * the same object. */ private LayoutState mLayoutState; /** * Many calculations are made depending on orientation. To keep it clean, this interface * helps {@link LinearLayoutManager} make those decisions. * Based on {@link #mOrientation}, an implementation is lazily created in * {@link #ensureLayoutState} method. */ OrientationHelper mOrientationHelper; /** * We need to track this so that we can ignore current position when it changes. */ private boolean mLastStackFromEnd; /** * Defines if layout should be calculated from end to start. * * @see #mShouldReverseLayout */ private boolean mReverseLayout = false; /** * This keeps the final value for how LayoutManager should start laying out views. * It is calculated by checking {@link #getReverseLayout()} and View's layout direction. * {@link #onLayoutChildren(RecyclerView.Recycler, RecyclerView.State)} is run. */ boolean mShouldReverseLayout = false; /** * Works the same way as {@link android.widget.AbsListView#setStackFromBottom(boolean)} and * it supports both orientations. * see {@link android.widget.AbsListView#setStackFromBottom(boolean)} */ private boolean mStackFromEnd = false; /** * Works the same way as {@link android.widget.AbsListView#setSmoothScrollbarEnabled(boolean)}. * see {@link android.widget.AbsListView#setSmoothScrollbarEnabled(boolean)} */ private boolean mSmoothScrollbarEnabled = true; /** * When LayoutManager needs to scroll to a position, it sets this variable and requests a * layout which will check this variable and re-layout accordingly. */ int mPendingScrollPosition = NO_POSITION; /** * Used to keep the offset value when {@link #scrollToPositionWithOffset(int, int)} is * called. */ int mPendingScrollPositionOffset = INVALID_OFFSET; private boolean mRecycleChildrenOnDetach; SavedState mPendingSavedState = null; /** * Re-used variable to keep anchor information on re-layout. * Anchor position and coordinate defines the reference point for LLM while doing a layout. * */ final AnchorInfo mAnchorInfo = new AnchorInfo(); /** * Stashed to avoid allocation, currently only used in #fill() */ private final LayoutChunkResult mLayoutChunkResult = new LayoutChunkResult(); /** * Number of items to prefetch when first coming on screen with new data. */ private int mInitialItemPrefetchCount = 2; /** * Creates a vertical LinearLayoutManager * * @param context Current context, will be used to access resources. */ public LinearLayoutManager(Context context) { this(context, VERTICAL, false); } /** * @param context Current context, will be used to access resources. * @param orientation Layout orientation. Should be {@link #HORIZONTAL} or {@link * #VERTICAL}. * @param reverseLayout When set to true, layouts from end to start. */ public LinearLayoutManager(Context context, int orientation, boolean reverseLayout) { setOrientation(orientation); setReverseLayout(reverseLayout); setAutoMeasureEnabled(true); } /** * Constructor used when layout manager is set in XML by RecyclerView attribute * "layoutManager". Defaults to vertical orientation. * * @attr ref android.support.v7.recyclerview.R.styleable#RecyclerView_android_orientation * @attr ref android.support.v7.recyclerview.R.styleable#RecyclerView_reverseLayout * @attr ref android.support.v7.recyclerview.R.styleable#RecyclerView_stackFromEnd */ public LinearLayoutManager(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) { Properties properties = getProperties(context, attrs, defStyleAttr, defStyleRes); setOrientation(properties.orientation); setReverseLayout(properties.reverseLayout); setStackFromEnd(properties.stackFromEnd); setAutoMeasureEnabled(true); } /** * {@inheritDoc} */ @Override public LayoutParams generateDefaultLayoutParams() { return new LayoutParams(ViewGroup.LayoutParams.WRAP_CONTENT, ViewGroup.LayoutParams.WRAP_CONTENT); } /** * Returns whether LayoutManager will recycle its children when it is detached from * RecyclerView. * * @return true if LayoutManager will recycle its children when it is detached from * RecyclerView. */ public boolean getRecycleChildrenOnDetach() { return mRecycleChildrenOnDetach; } /** * Set whether LayoutManager will recycle its children when it is detached from * RecyclerView. *

* If you are using a {@link RecyclerView.RecycledViewPool}, it might be a good idea to set * this flag to true so that views will be available to other RecyclerViews * immediately. *

* Note that, setting this flag will result in a performance drop if RecyclerView * is restored. * * @param recycleChildrenOnDetach Whether children should be recycled in detach or not. */ public void setRecycleChildrenOnDetach(boolean recycleChildrenOnDetach) { mRecycleChildrenOnDetach = recycleChildrenOnDetach; } @Override public void onDetachedFromWindow(RecyclerView view, RecyclerView.Recycler recycler) { super.onDetachedFromWindow(view, recycler); if (mRecycleChildrenOnDetach) { removeAndRecycleAllViews(recycler); recycler.clear(); } } @Override public void onInitializeAccessibilityEvent(AccessibilityEvent event) { super.onInitializeAccessibilityEvent(event); if (getChildCount() > 0) { event.setFromIndex(findFirstVisibleItemPosition()); event.setToIndex(findLastVisibleItemPosition()); } } @Override public Parcelable onSaveInstanceState() { if (mPendingSavedState != null) { return new SavedState(mPendingSavedState); } SavedState state = new SavedState(); if (getChildCount() > 0) { ensureLayoutState(); boolean didLayoutFromEnd = mLastStackFromEnd ^ mShouldReverseLayout; state.mAnchorLayoutFromEnd = didLayoutFromEnd; if (didLayoutFromEnd) { final View refChild = getChildClosestToEnd(); state.mAnchorOffset = mOrientationHelper.getEndAfterPadding() - mOrientationHelper.getDecoratedEnd(refChild); state.mAnchorPosition = getPosition(refChild); } else { final View refChild = getChildClosestToStart(); state.mAnchorPosition = getPosition(refChild); state.mAnchorOffset = mOrientationHelper.getDecoratedStart(refChild) - mOrientationHelper.getStartAfterPadding(); } } else { state.invalidateAnchor(); } return state; } @Override public void onRestoreInstanceState(Parcelable state) { if (state instanceof SavedState) { mPendingSavedState = (SavedState) state; requestLayout(); if (DEBUG) { Log.d(TAG, "loaded saved state"); } } else if (DEBUG) { Log.d(TAG, "invalid saved state class"); } } /** * @return true if {@link #getOrientation()} is {@link #HORIZONTAL} */ @Override public boolean canScrollHorizontally() { return mOrientation == HORIZONTAL; } /** * @return true if {@link #getOrientation()} is {@link #VERTICAL} */ @Override public boolean canScrollVertically() { return mOrientation == VERTICAL; } /** * Compatibility support for {@link android.widget.AbsListView#setStackFromBottom(boolean)} */ public void setStackFromEnd(boolean stackFromEnd) { assertNotInLayoutOrScroll(null); if (mStackFromEnd == stackFromEnd) { return; } mStackFromEnd = stackFromEnd; requestLayout(); } public boolean getStackFromEnd() { return mStackFromEnd; } /** * Returns the current orientation of the layout. * * @return Current orientation, either {@link #HORIZONTAL} or {@link #VERTICAL} * @see #setOrientation(int) */ public int getOrientation() { return mOrientation; } /** * Sets the orientation of the layout. {@link com.android.internal.widget.LinearLayoutManager} * will do its best to keep scroll position. * * @param orientation {@link #HORIZONTAL} or {@link #VERTICAL} */ public void setOrientation(int orientation) { if (orientation != HORIZONTAL && orientation != VERTICAL) { throw new IllegalArgumentException("invalid orientation:" + orientation); } assertNotInLayoutOrScroll(null); if (orientation == mOrientation) { return; } mOrientation = orientation; mOrientationHelper = null; requestLayout(); } /** * Calculates the view layout order. (e.g. from end to start or start to end) * RTL layout support is applied automatically. So if layout is RTL and * {@link #getReverseLayout()} is {@code true}, elements will be laid out starting from left. */ private void resolveShouldLayoutReverse() { // A == B is the same result, but we rather keep it readable if (mOrientation == VERTICAL || !isLayoutRTL()) { mShouldReverseLayout = mReverseLayout; } else { mShouldReverseLayout = !mReverseLayout; } } /** * Returns if views are laid out from the opposite direction of the layout. * * @return If layout is reversed or not. * @see #setReverseLayout(boolean) */ public boolean getReverseLayout() { return mReverseLayout; } /** * Used to reverse item traversal and layout order. * This behaves similar to the layout change for RTL views. When set to true, first item is * laid out at the end of the UI, second item is laid out before it etc. * * For horizontal layouts, it depends on the layout direction. * When set to true, If {@link com.android.internal.widget.RecyclerView} is LTR, than it will * layout from RTL, if {@link com.android.internal.widget.RecyclerView}} is RTL, it will layout * from LTR. * * If you are looking for the exact same behavior of * {@link android.widget.AbsListView#setStackFromBottom(boolean)}, use * {@link #setStackFromEnd(boolean)} */ public void setReverseLayout(boolean reverseLayout) { assertNotInLayoutOrScroll(null); if (reverseLayout == mReverseLayout) { return; } mReverseLayout = reverseLayout; requestLayout(); } /** * {@inheritDoc} */ @Override public View findViewByPosition(int position) { final int childCount = getChildCount(); if (childCount == 0) { return null; } final int firstChild = getPosition(getChildAt(0)); final int viewPosition = position - firstChild; if (viewPosition >= 0 && viewPosition < childCount) { final View child = getChildAt(viewPosition); if (getPosition(child) == position) { return child; // in pre-layout, this may not match } } // fallback to traversal. This might be necessary in pre-layout. return super.findViewByPosition(position); } /** *

Returns the amount of extra space that should be laid out by LayoutManager.

* *

By default, {@link com.android.internal.widget.LinearLayoutManager} lays out 1 extra page * of items while smooth scrolling and 0 otherwise. You can override this method to implement * your custom layout pre-cache logic.

* *

Note:Laying out invisible elements generally comes with significant * performance cost. It's typically only desirable in places like smooth scrolling to an unknown * location, where 1) the extra content helps LinearLayoutManager know in advance when its * target is approaching, so it can decelerate early and smoothly and 2) while motion is * continuous.

* *

Extending the extra layout space is especially expensive if done while the user may change * scrolling direction. Changing direction will cause the extra layout space to swap to the * opposite side of the viewport, incurring many rebinds/recycles, unless the cache is large * enough to handle it.

* * @return The extra space that should be laid out (in pixels). */ protected int getExtraLayoutSpace(RecyclerView.State state) { if (state.hasTargetScrollPosition()) { return mOrientationHelper.getTotalSpace(); } else { return 0; } } @Override public void smoothScrollToPosition(RecyclerView recyclerView, RecyclerView.State state, int position) { LinearSmoothScroller linearSmoothScroller = new LinearSmoothScroller(recyclerView.getContext()); linearSmoothScroller.setTargetPosition(position); startSmoothScroll(linearSmoothScroller); } @Override public PointF computeScrollVectorForPosition(int targetPosition) { if (getChildCount() == 0) { return null; } final int firstChildPos = getPosition(getChildAt(0)); final int direction = targetPosition < firstChildPos != mShouldReverseLayout ? -1 : 1; if (mOrientation == HORIZONTAL) { return new PointF(direction, 0); } else { return new PointF(0, direction); } } /** * {@inheritDoc} */ @Override public void onLayoutChildren(RecyclerView.Recycler recycler, RecyclerView.State state) { // layout algorithm: // 1) by checking children and other variables, find an anchor coordinate and an anchor // item position. // 2) fill towards start, stacking from bottom // 3) fill towards end, stacking from top // 4) scroll to fulfill requirements like stack from bottom. // create layout state if (DEBUG) { Log.d(TAG, "is pre layout:" + state.isPreLayout()); } if (mPendingSavedState != null || mPendingScrollPosition != NO_POSITION) { if (state.getItemCount() == 0) { removeAndRecycleAllViews(recycler); return; } } if (mPendingSavedState != null && mPendingSavedState.hasValidAnchor()) { mPendingScrollPosition = mPendingSavedState.mAnchorPosition; } ensureLayoutState(); mLayoutState.mRecycle = false; // resolve layout direction resolveShouldLayoutReverse(); if (!mAnchorInfo.mValid || mPendingScrollPosition != NO_POSITION || mPendingSavedState != null) { mAnchorInfo.reset(); mAnchorInfo.mLayoutFromEnd = mShouldReverseLayout ^ mStackFromEnd; // calculate anchor position and coordinate updateAnchorInfoForLayout(recycler, state, mAnchorInfo); mAnchorInfo.mValid = true; } if (DEBUG) { Log.d(TAG, "Anchor info:" + mAnchorInfo); } // LLM may decide to layout items for "extra" pixels to account for scrolling target, // caching or predictive animations. int extraForStart; int extraForEnd; final int extra = getExtraLayoutSpace(state); // If the previous scroll delta was less than zero, the extra space should be laid out // at the start. Otherwise, it should be at the end. if (mLayoutState.mLastScrollDelta >= 0) { extraForEnd = extra; extraForStart = 0; } else { extraForStart = extra; extraForEnd = 0; } extraForStart += mOrientationHelper.getStartAfterPadding(); extraForEnd += mOrientationHelper.getEndPadding(); if (state.isPreLayout() && mPendingScrollPosition != NO_POSITION && mPendingScrollPositionOffset != INVALID_OFFSET) { // if the child is visible and we are going to move it around, we should layout // extra items in the opposite direction to make sure new items animate nicely // instead of just fading in final View existing = findViewByPosition(mPendingScrollPosition); if (existing != null) { final int current; final int upcomingOffset; if (mShouldReverseLayout) { current = mOrientationHelper.getEndAfterPadding() - mOrientationHelper.getDecoratedEnd(existing); upcomingOffset = current - mPendingScrollPositionOffset; } else { current = mOrientationHelper.getDecoratedStart(existing) - mOrientationHelper.getStartAfterPadding(); upcomingOffset = mPendingScrollPositionOffset - current; } if (upcomingOffset > 0) { extraForStart += upcomingOffset; } else { extraForEnd -= upcomingOffset; } } } int startOffset; int endOffset; final int firstLayoutDirection; if (mAnchorInfo.mLayoutFromEnd) { firstLayoutDirection = mShouldReverseLayout ? LayoutState.ITEM_DIRECTION_TAIL : LayoutState.ITEM_DIRECTION_HEAD; } else { firstLayoutDirection = mShouldReverseLayout ? LayoutState.ITEM_DIRECTION_HEAD : LayoutState.ITEM_DIRECTION_TAIL; } onAnchorReady(recycler, state, mAnchorInfo, firstLayoutDirection); detachAndScrapAttachedViews(recycler); mLayoutState.mInfinite = resolveIsInfinite(); mLayoutState.mIsPreLayout = state.isPreLayout(); if (mAnchorInfo.mLayoutFromEnd) { // fill towards start updateLayoutStateToFillStart(mAnchorInfo); mLayoutState.mExtra = extraForStart; fill(recycler, mLayoutState, state, false); startOffset = mLayoutState.mOffset; final int firstElement = mLayoutState.mCurrentPosition; if (mLayoutState.mAvailable > 0) { extraForEnd += mLayoutState.mAvailable; } // fill towards end updateLayoutStateToFillEnd(mAnchorInfo); mLayoutState.mExtra = extraForEnd; mLayoutState.mCurrentPosition += mLayoutState.mItemDirection; fill(recycler, mLayoutState, state, false); endOffset = mLayoutState.mOffset; if (mLayoutState.mAvailable > 0) { // end could not consume all. add more items towards start extraForStart = mLayoutState.mAvailable; updateLayoutStateToFillStart(firstElement, startOffset); mLayoutState.mExtra = extraForStart; fill(recycler, mLayoutState, state, false); startOffset = mLayoutState.mOffset; } } else { // fill towards end updateLayoutStateToFillEnd(mAnchorInfo); mLayoutState.mExtra = extraForEnd; fill(recycler, mLayoutState, state, false); endOffset = mLayoutState.mOffset; final int lastElement = mLayoutState.mCurrentPosition; if (mLayoutState.mAvailable > 0) { extraForStart += mLayoutState.mAvailable; } // fill towards start updateLayoutStateToFillStart(mAnchorInfo); mLayoutState.mExtra = extraForStart; mLayoutState.mCurrentPosition += mLayoutState.mItemDirection; fill(recycler, mLayoutState, state, false); startOffset = mLayoutState.mOffset; if (mLayoutState.mAvailable > 0) { extraForEnd = mLayoutState.mAvailable; // start could not consume all it should. add more items towards end updateLayoutStateToFillEnd(lastElement, endOffset); mLayoutState.mExtra = extraForEnd; fill(recycler, mLayoutState, state, false); endOffset = mLayoutState.mOffset; } } // changes may cause gaps on the UI, try to fix them. // TODO we can probably avoid this if neither stackFromEnd/reverseLayout/RTL values have // changed if (getChildCount() > 0) { // because layout from end may be changed by scroll to position // we re-calculate it. // find which side we should check for gaps. if (mShouldReverseLayout ^ mStackFromEnd) { int fixOffset = fixLayoutEndGap(endOffset, recycler, state, true); startOffset += fixOffset; endOffset += fixOffset; fixOffset = fixLayoutStartGap(startOffset, recycler, state, false); startOffset += fixOffset; endOffset += fixOffset; } else { int fixOffset = fixLayoutStartGap(startOffset, recycler, state, true); startOffset += fixOffset; endOffset += fixOffset; fixOffset = fixLayoutEndGap(endOffset, recycler, state, false); startOffset += fixOffset; endOffset += fixOffset; } } layoutForPredictiveAnimations(recycler, state, startOffset, endOffset); if (!state.isPreLayout()) { mOrientationHelper.onLayoutComplete(); } else { mAnchorInfo.reset(); } mLastStackFromEnd = mStackFromEnd; if (DEBUG) { validateChildOrder(); } } @Override public void onLayoutCompleted(RecyclerView.State state) { super.onLayoutCompleted(state); mPendingSavedState = null; // we don't need this anymore mPendingScrollPosition = NO_POSITION; mPendingScrollPositionOffset = INVALID_OFFSET; mAnchorInfo.reset(); } /** * Method called when Anchor position is decided. Extending class can setup accordingly or * even update anchor info if necessary. * @param recycler The recycler for the layout * @param state The layout state * @param anchorInfo The mutable POJO that keeps the position and offset. * @param firstLayoutItemDirection The direction of the first layout filling in terms of adapter * indices. */ void onAnchorReady(RecyclerView.Recycler recycler, RecyclerView.State state, AnchorInfo anchorInfo, int firstLayoutItemDirection) { } /** * If necessary, layouts new items for predictive animations */ private void layoutForPredictiveAnimations(RecyclerView.Recycler recycler, RecyclerView.State state, int startOffset, int endOffset) { // If there are scrap children that we did not layout, we need to find where they did go // and layout them accordingly so that animations can work as expected. // This case may happen if new views are added or an existing view expands and pushes // another view out of bounds. if (!state.willRunPredictiveAnimations() || getChildCount() == 0 || state.isPreLayout() || !supportsPredictiveItemAnimations()) { return; } // to make the logic simpler, we calculate the size of children and call fill. int scrapExtraStart = 0, scrapExtraEnd = 0; final List scrapList = recycler.getScrapList(); final int scrapSize = scrapList.size(); final int firstChildPos = getPosition(getChildAt(0)); for (int i = 0; i < scrapSize; i++) { RecyclerView.ViewHolder scrap = scrapList.get(i); if (scrap.isRemoved()) { continue; } final int position = scrap.getLayoutPosition(); final int direction = position < firstChildPos != mShouldReverseLayout ? LayoutState.LAYOUT_START : LayoutState.LAYOUT_END; if (direction == LayoutState.LAYOUT_START) { scrapExtraStart += mOrientationHelper.getDecoratedMeasurement(scrap.itemView); } else { scrapExtraEnd += mOrientationHelper.getDecoratedMeasurement(scrap.itemView); } } if (DEBUG) { Log.d(TAG, "for unused scrap, decided to add " + scrapExtraStart + " towards start and " + scrapExtraEnd + " towards end"); } mLayoutState.mScrapList = scrapList; if (scrapExtraStart > 0) { View anchor = getChildClosestToStart(); updateLayoutStateToFillStart(getPosition(anchor), startOffset); mLayoutState.mExtra = scrapExtraStart; mLayoutState.mAvailable = 0; mLayoutState.assignPositionFromScrapList(); fill(recycler, mLayoutState, state, false); } if (scrapExtraEnd > 0) { View anchor = getChildClosestToEnd(); updateLayoutStateToFillEnd(getPosition(anchor), endOffset); mLayoutState.mExtra = scrapExtraEnd; mLayoutState.mAvailable = 0; mLayoutState.assignPositionFromScrapList(); fill(recycler, mLayoutState, state, false); } mLayoutState.mScrapList = null; } private void updateAnchorInfoForLayout(RecyclerView.Recycler recycler, RecyclerView.State state, AnchorInfo anchorInfo) { if (updateAnchorFromPendingData(state, anchorInfo)) { if (DEBUG) { Log.d(TAG, "updated anchor info from pending information"); } return; } if (updateAnchorFromChildren(recycler, state, anchorInfo)) { if (DEBUG) { Log.d(TAG, "updated anchor info from existing children"); } return; } if (DEBUG) { Log.d(TAG, "deciding anchor info for fresh state"); } anchorInfo.assignCoordinateFromPadding(); anchorInfo.mPosition = mStackFromEnd ? state.getItemCount() - 1 : 0; } /** * Finds an anchor child from existing Views. Most of the time, this is the view closest to * start or end that has a valid position (e.g. not removed). *

* If a child has focus, it is given priority. */ private boolean updateAnchorFromChildren(RecyclerView.Recycler recycler, RecyclerView.State state, AnchorInfo anchorInfo) { if (getChildCount() == 0) { return false; } final View focused = getFocusedChild(); if (focused != null && anchorInfo.isViewValidAsAnchor(focused, state)) { anchorInfo.assignFromViewAndKeepVisibleRect(focused); return true; } if (mLastStackFromEnd != mStackFromEnd) { return false; } View referenceChild = anchorInfo.mLayoutFromEnd ? findReferenceChildClosestToEnd(recycler, state) : findReferenceChildClosestToStart(recycler, state); if (referenceChild != null) { anchorInfo.assignFromView(referenceChild); // If all visible views are removed in 1 pass, reference child might be out of bounds. // If that is the case, offset it back to 0 so that we use these pre-layout children. if (!state.isPreLayout() && supportsPredictiveItemAnimations()) { // validate this child is at least partially visible. if not, offset it to start final boolean notVisible = mOrientationHelper.getDecoratedStart(referenceChild) >= mOrientationHelper .getEndAfterPadding() || mOrientationHelper.getDecoratedEnd(referenceChild) < mOrientationHelper.getStartAfterPadding(); if (notVisible) { anchorInfo.mCoordinate = anchorInfo.mLayoutFromEnd ? mOrientationHelper.getEndAfterPadding() : mOrientationHelper.getStartAfterPadding(); } } return true; } return false; } /** * If there is a pending scroll position or saved states, updates the anchor info from that * data and returns true */ private boolean updateAnchorFromPendingData(RecyclerView.State state, AnchorInfo anchorInfo) { if (state.isPreLayout() || mPendingScrollPosition == NO_POSITION) { return false; } // validate scroll position if (mPendingScrollPosition < 0 || mPendingScrollPosition >= state.getItemCount()) { mPendingScrollPosition = NO_POSITION; mPendingScrollPositionOffset = INVALID_OFFSET; if (DEBUG) { Log.e(TAG, "ignoring invalid scroll position " + mPendingScrollPosition); } return false; } // if child is visible, try to make it a reference child and ensure it is fully visible. // if child is not visible, align it depending on its virtual position. anchorInfo.mPosition = mPendingScrollPosition; if (mPendingSavedState != null && mPendingSavedState.hasValidAnchor()) { // Anchor offset depends on how that child was laid out. Here, we update it // according to our current view bounds anchorInfo.mLayoutFromEnd = mPendingSavedState.mAnchorLayoutFromEnd; if (anchorInfo.mLayoutFromEnd) { anchorInfo.mCoordinate = mOrientationHelper.getEndAfterPadding() - mPendingSavedState.mAnchorOffset; } else { anchorInfo.mCoordinate = mOrientationHelper.getStartAfterPadding() + mPendingSavedState.mAnchorOffset; } return true; } if (mPendingScrollPositionOffset == INVALID_OFFSET) { View child = findViewByPosition(mPendingScrollPosition); if (child != null) { final int childSize = mOrientationHelper.getDecoratedMeasurement(child); if (childSize > mOrientationHelper.getTotalSpace()) { // item does not fit. fix depending on layout direction anchorInfo.assignCoordinateFromPadding(); return true; } final int startGap = mOrientationHelper.getDecoratedStart(child) - mOrientationHelper.getStartAfterPadding(); if (startGap < 0) { anchorInfo.mCoordinate = mOrientationHelper.getStartAfterPadding(); anchorInfo.mLayoutFromEnd = false; return true; } final int endGap = mOrientationHelper.getEndAfterPadding() - mOrientationHelper.getDecoratedEnd(child); if (endGap < 0) { anchorInfo.mCoordinate = mOrientationHelper.getEndAfterPadding(); anchorInfo.mLayoutFromEnd = true; return true; } anchorInfo.mCoordinate = anchorInfo.mLayoutFromEnd ? (mOrientationHelper.getDecoratedEnd(child) + mOrientationHelper .getTotalSpaceChange()) : mOrientationHelper.getDecoratedStart(child); } else { // item is not visible. if (getChildCount() > 0) { // get position of any child, does not matter int pos = getPosition(getChildAt(0)); anchorInfo.mLayoutFromEnd = mPendingScrollPosition < pos == mShouldReverseLayout; } anchorInfo.assignCoordinateFromPadding(); } return true; } // override layout from end values for consistency anchorInfo.mLayoutFromEnd = mShouldReverseLayout; // if this changes, we should update prepareForDrop as well if (mShouldReverseLayout) { anchorInfo.mCoordinate = mOrientationHelper.getEndAfterPadding() - mPendingScrollPositionOffset; } else { anchorInfo.mCoordinate = mOrientationHelper.getStartAfterPadding() + mPendingScrollPositionOffset; } return true; } /** * @return The final offset amount for children */ private int fixLayoutEndGap(int endOffset, RecyclerView.Recycler recycler, RecyclerView.State state, boolean canOffsetChildren) { int gap = mOrientationHelper.getEndAfterPadding() - endOffset; int fixOffset = 0; if (gap > 0) { fixOffset = -scrollBy(-gap, recycler, state); } else { return 0; // nothing to fix } // move offset according to scroll amount endOffset += fixOffset; if (canOffsetChildren) { // re-calculate gap, see if we could fix it gap = mOrientationHelper.getEndAfterPadding() - endOffset; if (gap > 0) { mOrientationHelper.offsetChildren(gap); return gap + fixOffset; } } return fixOffset; } /** * @return The final offset amount for children */ private int fixLayoutStartGap(int startOffset, RecyclerView.Recycler recycler, RecyclerView.State state, boolean canOffsetChildren) { int gap = startOffset - mOrientationHelper.getStartAfterPadding(); int fixOffset = 0; if (gap > 0) { // check if we should fix this gap. fixOffset = -scrollBy(gap, recycler, state); } else { return 0; // nothing to fix } startOffset += fixOffset; if (canOffsetChildren) { // re-calculate gap, see if we could fix it gap = startOffset - mOrientationHelper.getStartAfterPadding(); if (gap > 0) { mOrientationHelper.offsetChildren(-gap); return fixOffset - gap; } } return fixOffset; } private void updateLayoutStateToFillEnd(AnchorInfo anchorInfo) { updateLayoutStateToFillEnd(anchorInfo.mPosition, anchorInfo.mCoordinate); } private void updateLayoutStateToFillEnd(int itemPosition, int offset) { mLayoutState.mAvailable = mOrientationHelper.getEndAfterPadding() - offset; mLayoutState.mItemDirection = mShouldReverseLayout ? LayoutState.ITEM_DIRECTION_HEAD : LayoutState.ITEM_DIRECTION_TAIL; mLayoutState.mCurrentPosition = itemPosition; mLayoutState.mLayoutDirection = LayoutState.LAYOUT_END; mLayoutState.mOffset = offset; mLayoutState.mScrollingOffset = LayoutState.SCROLLING_OFFSET_NaN; } private void updateLayoutStateToFillStart(AnchorInfo anchorInfo) { updateLayoutStateToFillStart(anchorInfo.mPosition, anchorInfo.mCoordinate); } private void updateLayoutStateToFillStart(int itemPosition, int offset) { mLayoutState.mAvailable = offset - mOrientationHelper.getStartAfterPadding(); mLayoutState.mCurrentPosition = itemPosition; mLayoutState.mItemDirection = mShouldReverseLayout ? LayoutState.ITEM_DIRECTION_TAIL : LayoutState.ITEM_DIRECTION_HEAD; mLayoutState.mLayoutDirection = LayoutState.LAYOUT_START; mLayoutState.mOffset = offset; mLayoutState.mScrollingOffset = LayoutState.SCROLLING_OFFSET_NaN; } protected boolean isLayoutRTL() { return getLayoutDirection() == View.LAYOUT_DIRECTION_RTL; } void ensureLayoutState() { if (mLayoutState == null) { mLayoutState = createLayoutState(); } if (mOrientationHelper == null) { mOrientationHelper = OrientationHelper.createOrientationHelper(this, mOrientation); } } /** * Test overrides this to plug some tracking and verification. * * @return A new LayoutState */ LayoutState createLayoutState() { return new LayoutState(); } /** *

Scroll the RecyclerView to make the position visible.

* *

RecyclerView will scroll the minimum amount that is necessary to make the * target position visible. If you are looking for a similar behavior to * {@link android.widget.ListView#setSelection(int)} or * {@link android.widget.ListView#setSelectionFromTop(int, int)}, use * {@link #scrollToPositionWithOffset(int, int)}.

* *

Note that scroll position change will not be reflected until the next layout call.

* * @param position Scroll to this adapter position * @see #scrollToPositionWithOffset(int, int) */ @Override public void scrollToPosition(int position) { mPendingScrollPosition = position; mPendingScrollPositionOffset = INVALID_OFFSET; if (mPendingSavedState != null) { mPendingSavedState.invalidateAnchor(); } requestLayout(); } /** * Scroll to the specified adapter position with the given offset from resolved layout * start. Resolved layout start depends on {@link #getReverseLayout()}, * {@link View#getLayoutDirection()} and {@link #getStackFromEnd()}. *

* For example, if layout is {@link #VERTICAL} and {@link #getStackFromEnd()} is true, calling * scrollToPositionWithOffset(10, 20) will layout such that * item[10]'s bottom is 20 pixels above the RecyclerView's bottom. *

* Note that scroll position change will not be reflected until the next layout call. *

* If you are just trying to make a position visible, use {@link #scrollToPosition(int)}. * * @param position Index (starting at 0) of the reference item. * @param offset The distance (in pixels) between the start edge of the item view and * start edge of the RecyclerView. * @see #setReverseLayout(boolean) * @see #scrollToPosition(int) */ public void scrollToPositionWithOffset(int position, int offset) { mPendingScrollPosition = position; mPendingScrollPositionOffset = offset; if (mPendingSavedState != null) { mPendingSavedState.invalidateAnchor(); } requestLayout(); } /** * {@inheritDoc} */ @Override public int scrollHorizontallyBy(int dx, RecyclerView.Recycler recycler, RecyclerView.State state) { if (mOrientation == VERTICAL) { return 0; } return scrollBy(dx, recycler, state); } /** * {@inheritDoc} */ @Override public int scrollVerticallyBy(int dy, RecyclerView.Recycler recycler, RecyclerView.State state) { if (mOrientation == HORIZONTAL) { return 0; } return scrollBy(dy, recycler, state); } @Override public int computeHorizontalScrollOffset(RecyclerView.State state) { return computeScrollOffset(state); } @Override public int computeVerticalScrollOffset(RecyclerView.State state) { return computeScrollOffset(state); } @Override public int computeHorizontalScrollExtent(RecyclerView.State state) { return computeScrollExtent(state); } @Override public int computeVerticalScrollExtent(RecyclerView.State state) { return computeScrollExtent(state); } @Override public int computeHorizontalScrollRange(RecyclerView.State state) { return computeScrollRange(state); } @Override public int computeVerticalScrollRange(RecyclerView.State state) { return computeScrollRange(state); } private int computeScrollOffset(RecyclerView.State state) { if (getChildCount() == 0) { return 0; } ensureLayoutState(); return ScrollbarHelper.computeScrollOffset(state, mOrientationHelper, findFirstVisibleChildClosestToStart(!mSmoothScrollbarEnabled, true), findFirstVisibleChildClosestToEnd(!mSmoothScrollbarEnabled, true), this, mSmoothScrollbarEnabled, mShouldReverseLayout); } private int computeScrollExtent(RecyclerView.State state) { if (getChildCount() == 0) { return 0; } ensureLayoutState(); return ScrollbarHelper.computeScrollExtent(state, mOrientationHelper, findFirstVisibleChildClosestToStart(!mSmoothScrollbarEnabled, true), findFirstVisibleChildClosestToEnd(!mSmoothScrollbarEnabled, true), this, mSmoothScrollbarEnabled); } private int computeScrollRange(RecyclerView.State state) { if (getChildCount() == 0) { return 0; } ensureLayoutState(); return ScrollbarHelper.computeScrollRange(state, mOrientationHelper, findFirstVisibleChildClosestToStart(!mSmoothScrollbarEnabled, true), findFirstVisibleChildClosestToEnd(!mSmoothScrollbarEnabled, true), this, mSmoothScrollbarEnabled); } /** * When smooth scrollbar is enabled, the position and size of the scrollbar thumb is computed * based on the number of visible pixels in the visible items. This however assumes that all * list items have similar or equal widths or heights (depending on list orientation). * If you use a list in which items have different dimensions, the scrollbar will change * appearance as the user scrolls through the list. To avoid this issue, you need to disable * this property. * * When smooth scrollbar is disabled, the position and size of the scrollbar thumb is based * solely on the number of items in the adapter and the position of the visible items inside * the adapter. This provides a stable scrollbar as the user navigates through a list of items * with varying widths / heights. * * @param enabled Whether or not to enable smooth scrollbar. * * @see #setSmoothScrollbarEnabled(boolean) */ public void setSmoothScrollbarEnabled(boolean enabled) { mSmoothScrollbarEnabled = enabled; } /** * Returns the current state of the smooth scrollbar feature. It is enabled by default. * * @return True if smooth scrollbar is enabled, false otherwise. * * @see #setSmoothScrollbarEnabled(boolean) */ public boolean isSmoothScrollbarEnabled() { return mSmoothScrollbarEnabled; } private void updateLayoutState(int layoutDirection, int requiredSpace, boolean canUseExistingSpace, RecyclerView.State state) { // If parent provides a hint, don't measure unlimited. mLayoutState.mInfinite = resolveIsInfinite(); mLayoutState.mExtra = getExtraLayoutSpace(state); mLayoutState.mLayoutDirection = layoutDirection; int scrollingOffset; if (layoutDirection == LayoutState.LAYOUT_END) { mLayoutState.mExtra += mOrientationHelper.getEndPadding(); // get the first child in the direction we are going final View child = getChildClosestToEnd(); // the direction in which we are traversing children mLayoutState.mItemDirection = mShouldReverseLayout ? LayoutState.ITEM_DIRECTION_HEAD : LayoutState.ITEM_DIRECTION_TAIL; mLayoutState.mCurrentPosition = getPosition(child) + mLayoutState.mItemDirection; mLayoutState.mOffset = mOrientationHelper.getDecoratedEnd(child); // calculate how much we can scroll without adding new children (independent of layout) scrollingOffset = mOrientationHelper.getDecoratedEnd(child) - mOrientationHelper.getEndAfterPadding(); } else { final View child = getChildClosestToStart(); mLayoutState.mExtra += mOrientationHelper.getStartAfterPadding(); mLayoutState.mItemDirection = mShouldReverseLayout ? LayoutState.ITEM_DIRECTION_TAIL : LayoutState.ITEM_DIRECTION_HEAD; mLayoutState.mCurrentPosition = getPosition(child) + mLayoutState.mItemDirection; mLayoutState.mOffset = mOrientationHelper.getDecoratedStart(child); scrollingOffset = -mOrientationHelper.getDecoratedStart(child) + mOrientationHelper.getStartAfterPadding(); } mLayoutState.mAvailable = requiredSpace; if (canUseExistingSpace) { mLayoutState.mAvailable -= scrollingOffset; } mLayoutState.mScrollingOffset = scrollingOffset; } boolean resolveIsInfinite() { return mOrientationHelper.getMode() == View.MeasureSpec.UNSPECIFIED && mOrientationHelper.getEnd() == 0; } void collectPrefetchPositionsForLayoutState(RecyclerView.State state, LayoutState layoutState, LayoutPrefetchRegistry layoutPrefetchRegistry) { final int pos = layoutState.mCurrentPosition; if (pos >= 0 && pos < state.getItemCount()) { layoutPrefetchRegistry.addPosition(pos, layoutState.mScrollingOffset); } } @Override public void collectInitialPrefetchPositions(int adapterItemCount, LayoutPrefetchRegistry layoutPrefetchRegistry) { final boolean fromEnd; final int anchorPos; if (mPendingSavedState != null && mPendingSavedState.hasValidAnchor()) { // use restored state, since it hasn't been resolved yet fromEnd = mPendingSavedState.mAnchorLayoutFromEnd; anchorPos = mPendingSavedState.mAnchorPosition; } else { resolveShouldLayoutReverse(); fromEnd = mShouldReverseLayout; if (mPendingScrollPosition == NO_POSITION) { anchorPos = fromEnd ? adapterItemCount - 1 : 0; } else { anchorPos = mPendingScrollPosition; } } final int direction = fromEnd ? LayoutState.ITEM_DIRECTION_HEAD : LayoutState.ITEM_DIRECTION_TAIL; int targetPos = anchorPos; for (int i = 0; i < mInitialItemPrefetchCount; i++) { if (targetPos >= 0 && targetPos < adapterItemCount) { layoutPrefetchRegistry.addPosition(targetPos, 0); } else { break; // no more to prefetch } targetPos += direction; } } /** * Sets the number of items to prefetch in * {@link #collectInitialPrefetchPositions(int, LayoutPrefetchRegistry)}, which defines * how many inner items should be prefetched when this LayoutManager's RecyclerView * is nested inside another RecyclerView. * *

Set this value to the number of items this inner LayoutManager will display when it is * first scrolled into the viewport. RecyclerView will attempt to prefetch that number of items * so they are ready, avoiding jank as the inner RecyclerView is scrolled into the viewport.

* *

For example, take a vertically scrolling RecyclerView with horizontally scrolling inner * RecyclerViews. The rows always have 4 items visible in them (or 5 if not aligned). Passing * 4 to this method for each inner RecyclerView's LinearLayoutManager will enable * RecyclerView's prefetching feature to do create/bind work for 4 views within a row early, * before it is scrolled on screen, instead of just the default 2.

* *

Calling this method does nothing unless the LayoutManager is in a RecyclerView * nested in another RecyclerView.

* *

Note: Setting this value to be larger than the number of * views that will be visible in this view can incur unnecessary bind work, and an increase to * the number of Views created and in active use.

* * @param itemCount Number of items to prefetch * * @see #isItemPrefetchEnabled() * @see #getInitialItemPrefetchCount() * @see #collectInitialPrefetchPositions(int, LayoutPrefetchRegistry) */ public void setInitialPrefetchItemCount(int itemCount) { mInitialItemPrefetchCount = itemCount; } /** * Gets the number of items to prefetch in * {@link #collectInitialPrefetchPositions(int, LayoutPrefetchRegistry)}, which defines * how many inner items should be prefetched when this LayoutManager's RecyclerView * is nested inside another RecyclerView. * * @see #isItemPrefetchEnabled() * @see #setInitialPrefetchItemCount(int) * @see #collectInitialPrefetchPositions(int, LayoutPrefetchRegistry) * * @return number of items to prefetch. */ public int getInitialItemPrefetchCount() { return mInitialItemPrefetchCount; } @Override public void collectAdjacentPrefetchPositions(int dx, int dy, RecyclerView.State state, LayoutPrefetchRegistry layoutPrefetchRegistry) { int delta = (mOrientation == HORIZONTAL) ? dx : dy; if (getChildCount() == 0 || delta == 0) { // can't support this scroll, so don't bother prefetching return; } final int layoutDirection = delta > 0 ? LayoutState.LAYOUT_END : LayoutState.LAYOUT_START; final int absDy = Math.abs(delta); updateLayoutState(layoutDirection, absDy, true, state); collectPrefetchPositionsForLayoutState(state, mLayoutState, layoutPrefetchRegistry); } int scrollBy(int dy, RecyclerView.Recycler recycler, RecyclerView.State state) { if (getChildCount() == 0 || dy == 0) { return 0; } mLayoutState.mRecycle = true; ensureLayoutState(); final int layoutDirection = dy > 0 ? LayoutState.LAYOUT_END : LayoutState.LAYOUT_START; final int absDy = Math.abs(dy); updateLayoutState(layoutDirection, absDy, true, state); final int consumed = mLayoutState.mScrollingOffset + fill(recycler, mLayoutState, state, false); if (consumed < 0) { if (DEBUG) { Log.d(TAG, "Don't have any more elements to scroll"); } return 0; } final int scrolled = absDy > consumed ? layoutDirection * consumed : dy; mOrientationHelper.offsetChildren(-scrolled); if (DEBUG) { Log.d(TAG, "scroll req: " + dy + " scrolled: " + scrolled); } mLayoutState.mLastScrollDelta = scrolled; return scrolled; } @Override public void assertNotInLayoutOrScroll(String message) { if (mPendingSavedState == null) { super.assertNotInLayoutOrScroll(message); } } /** * Recycles children between given indices. * * @param startIndex inclusive * @param endIndex exclusive */ private void recycleChildren(RecyclerView.Recycler recycler, int startIndex, int endIndex) { if (startIndex == endIndex) { return; } if (DEBUG) { Log.d(TAG, "Recycling " + Math.abs(startIndex - endIndex) + " items"); } if (endIndex > startIndex) { for (int i = endIndex - 1; i >= startIndex; i--) { removeAndRecycleViewAt(i, recycler); } } else { for (int i = startIndex; i > endIndex; i--) { removeAndRecycleViewAt(i, recycler); } } } /** * Recycles views that went out of bounds after scrolling towards the end of the layout. *

* Checks both layout position and visible position to guarantee that the view is not visible. * * @param recycler Recycler instance of {@link com.android.internal.widget.RecyclerView} * @param dt This can be used to add additional padding to the visible area. This is used * to detect children that will go out of bounds after scrolling, without * actually moving them. */ private void recycleViewsFromStart(RecyclerView.Recycler recycler, int dt) { if (dt < 0) { if (DEBUG) { Log.d(TAG, "Called recycle from start with a negative value. This might happen" + " during layout changes but may be sign of a bug"); } return; } // ignore padding, ViewGroup may not clip children. final int limit = dt; final int childCount = getChildCount(); if (mShouldReverseLayout) { for (int i = childCount - 1; i >= 0; i--) { View child = getChildAt(i); if (mOrientationHelper.getDecoratedEnd(child) > limit || mOrientationHelper.getTransformedEndWithDecoration(child) > limit) { // stop here recycleChildren(recycler, childCount - 1, i); return; } } } else { for (int i = 0; i < childCount; i++) { View child = getChildAt(i); if (mOrientationHelper.getDecoratedEnd(child) > limit || mOrientationHelper.getTransformedEndWithDecoration(child) > limit) { // stop here recycleChildren(recycler, 0, i); return; } } } } /** * Recycles views that went out of bounds after scrolling towards the start of the layout. *

* Checks both layout position and visible position to guarantee that the view is not visible. * * @param recycler Recycler instance of {@link com.android.internal.widget.RecyclerView} * @param dt This can be used to add additional padding to the visible area. This is used * to detect children that will go out of bounds after scrolling, without * actually moving them. */ private void recycleViewsFromEnd(RecyclerView.Recycler recycler, int dt) { final int childCount = getChildCount(); if (dt < 0) { if (DEBUG) { Log.d(TAG, "Called recycle from end with a negative value. This might happen" + " during layout changes but may be sign of a bug"); } return; } final int limit = mOrientationHelper.getEnd() - dt; if (mShouldReverseLayout) { for (int i = 0; i < childCount; i++) { View child = getChildAt(i); if (mOrientationHelper.getDecoratedStart(child) < limit || mOrientationHelper.getTransformedStartWithDecoration(child) < limit) { // stop here recycleChildren(recycler, 0, i); return; } } } else { for (int i = childCount - 1; i >= 0; i--) { View child = getChildAt(i); if (mOrientationHelper.getDecoratedStart(child) < limit || mOrientationHelper.getTransformedStartWithDecoration(child) < limit) { // stop here recycleChildren(recycler, childCount - 1, i); return; } } } } /** * Helper method to call appropriate recycle method depending on current layout direction * * @param recycler Current recycler that is attached to RecyclerView * @param layoutState Current layout state. Right now, this object does not change but * we may consider moving it out of this view so passing around as a * parameter for now, rather than accessing {@link #mLayoutState} * @see #recycleViewsFromStart(com.android.internal.widget.RecyclerView.Recycler, int) * @see #recycleViewsFromEnd(com.android.internal.widget.RecyclerView.Recycler, int) * @see com.android.internal.widget.LinearLayoutManager.LayoutState#mLayoutDirection */ private void recycleByLayoutState(RecyclerView.Recycler recycler, LayoutState layoutState) { if (!layoutState.mRecycle || layoutState.mInfinite) { return; } if (layoutState.mLayoutDirection == LayoutState.LAYOUT_START) { recycleViewsFromEnd(recycler, layoutState.mScrollingOffset); } else { recycleViewsFromStart(recycler, layoutState.mScrollingOffset); } } /** * The magic functions :). Fills the given layout, defined by the layoutState. This is fairly * independent from the rest of the {@link com.android.internal.widget.LinearLayoutManager} * and with little change, can be made publicly available as a helper class. * * @param recycler Current recycler that is attached to RecyclerView * @param layoutState Configuration on how we should fill out the available space. * @param state Context passed by the RecyclerView to control scroll steps. * @param stopOnFocusable If true, filling stops in the first focusable new child * @return Number of pixels that it added. Useful for scroll functions. */ int fill(RecyclerView.Recycler recycler, LayoutState layoutState, RecyclerView.State state, boolean stopOnFocusable) { // max offset we should set is mFastScroll + available final int start = layoutState.mAvailable; if (layoutState.mScrollingOffset != LayoutState.SCROLLING_OFFSET_NaN) { // TODO ugly bug fix. should not happen if (layoutState.mAvailable < 0) { layoutState.mScrollingOffset += layoutState.mAvailable; } recycleByLayoutState(recycler, layoutState); } int remainingSpace = layoutState.mAvailable + layoutState.mExtra; LayoutChunkResult layoutChunkResult = mLayoutChunkResult; while ((layoutState.mInfinite || remainingSpace > 0) && layoutState.hasMore(state)) { layoutChunkResult.resetInternal(); layoutChunk(recycler, state, layoutState, layoutChunkResult); if (layoutChunkResult.mFinished) { break; } layoutState.mOffset += layoutChunkResult.mConsumed * layoutState.mLayoutDirection; /** * Consume the available space if: * * layoutChunk did not request to be ignored * * OR we are laying out scrap children * * OR we are not doing pre-layout */ if (!layoutChunkResult.mIgnoreConsumed || mLayoutState.mScrapList != null || !state.isPreLayout()) { layoutState.mAvailable -= layoutChunkResult.mConsumed; // we keep a separate remaining space because mAvailable is important for recycling remainingSpace -= layoutChunkResult.mConsumed; } if (layoutState.mScrollingOffset != LayoutState.SCROLLING_OFFSET_NaN) { layoutState.mScrollingOffset += layoutChunkResult.mConsumed; if (layoutState.mAvailable < 0) { layoutState.mScrollingOffset += layoutState.mAvailable; } recycleByLayoutState(recycler, layoutState); } if (stopOnFocusable && layoutChunkResult.mFocusable) { break; } } if (DEBUG) { validateChildOrder(); } return start - layoutState.mAvailable; } void layoutChunk(RecyclerView.Recycler recycler, RecyclerView.State state, LayoutState layoutState, LayoutChunkResult result) { View view = layoutState.next(recycler); if (view == null) { if (DEBUG && layoutState.mScrapList == null) { throw new RuntimeException("received null view when unexpected"); } // if we are laying out views in scrap, this may return null which means there is // no more items to layout. result.mFinished = true; return; } LayoutParams params = (LayoutParams) view.getLayoutParams(); if (layoutState.mScrapList == null) { if (mShouldReverseLayout == (layoutState.mLayoutDirection == LayoutState.LAYOUT_START)) { addView(view); } else { addView(view, 0); } } else { if (mShouldReverseLayout == (layoutState.mLayoutDirection == LayoutState.LAYOUT_START)) { addDisappearingView(view); } else { addDisappearingView(view, 0); } } measureChildWithMargins(view, 0, 0); result.mConsumed = mOrientationHelper.getDecoratedMeasurement(view); int left, top, right, bottom; if (mOrientation == VERTICAL) { if (isLayoutRTL()) { right = getWidth() - getPaddingRight(); left = right - mOrientationHelper.getDecoratedMeasurementInOther(view); } else { left = getPaddingLeft(); right = left + mOrientationHelper.getDecoratedMeasurementInOther(view); } if (layoutState.mLayoutDirection == LayoutState.LAYOUT_START) { bottom = layoutState.mOffset; top = layoutState.mOffset - result.mConsumed; } else { top = layoutState.mOffset; bottom = layoutState.mOffset + result.mConsumed; } } else { top = getPaddingTop(); bottom = top + mOrientationHelper.getDecoratedMeasurementInOther(view); if (layoutState.mLayoutDirection == LayoutState.LAYOUT_START) { right = layoutState.mOffset; left = layoutState.mOffset - result.mConsumed; } else { left = layoutState.mOffset; right = layoutState.mOffset + result.mConsumed; } } // We calculate everything with View's bounding box (which includes decor and margins) // To calculate correct layout position, we subtract margins. layoutDecoratedWithMargins(view, left, top, right, bottom); if (DEBUG) { Log.d(TAG, "laid out child at position " + getPosition(view) + ", with l:" + (left + params.leftMargin) + ", t:" + (top + params.topMargin) + ", r:" + (right - params.rightMargin) + ", b:" + (bottom - params.bottomMargin)); } // Consume the available space if the view is not removed OR changed if (params.isItemRemoved() || params.isItemChanged()) { result.mIgnoreConsumed = true; } result.mFocusable = view.isFocusable(); } @Override boolean shouldMeasureTwice() { return getHeightMode() != View.MeasureSpec.EXACTLY && getWidthMode() != View.MeasureSpec.EXACTLY && hasFlexibleChildInBothOrientations(); } /** * Converts a focusDirection to orientation. * * @param focusDirection One of {@link View#FOCUS_UP}, {@link View#FOCUS_DOWN}, * {@link View#FOCUS_LEFT}, {@link View#FOCUS_RIGHT}, * {@link View#FOCUS_BACKWARD}, {@link View#FOCUS_FORWARD} * or 0 for not applicable * @return {@link LayoutState#LAYOUT_START} or {@link LayoutState#LAYOUT_END} if focus direction * is applicable to current state, {@link LayoutState#INVALID_LAYOUT} otherwise. */ int convertFocusDirectionToLayoutDirection(int focusDirection) { switch (focusDirection) { case View.FOCUS_BACKWARD: if (mOrientation == VERTICAL) { return LayoutState.LAYOUT_START; } else if (isLayoutRTL()) { return LayoutState.LAYOUT_END; } else { return LayoutState.LAYOUT_START; } case View.FOCUS_FORWARD: if (mOrientation == VERTICAL) { return LayoutState.LAYOUT_END; } else if (isLayoutRTL()) { return LayoutState.LAYOUT_START; } else { return LayoutState.LAYOUT_END; } case View.FOCUS_UP: return mOrientation == VERTICAL ? LayoutState.LAYOUT_START : LayoutState.INVALID_LAYOUT; case View.FOCUS_DOWN: return mOrientation == VERTICAL ? LayoutState.LAYOUT_END : LayoutState.INVALID_LAYOUT; case View.FOCUS_LEFT: return mOrientation == HORIZONTAL ? LayoutState.LAYOUT_START : LayoutState.INVALID_LAYOUT; case View.FOCUS_RIGHT: return mOrientation == HORIZONTAL ? LayoutState.LAYOUT_END : LayoutState.INVALID_LAYOUT; default: if (DEBUG) { Log.d(TAG, "Unknown focus request:" + focusDirection); } return LayoutState.INVALID_LAYOUT; } } /** * Convenience method to find the child closes to start. Caller should check it has enough * children. * * @return The child closes to start of the layout from user's perspective. */ private View getChildClosestToStart() { return getChildAt(mShouldReverseLayout ? getChildCount() - 1 : 0); } /** * Convenience method to find the child closes to end. Caller should check it has enough * children. * * @return The child closes to end of the layout from user's perspective. */ private View getChildClosestToEnd() { return getChildAt(mShouldReverseLayout ? 0 : getChildCount() - 1); } /** * Convenience method to find the visible child closes to start. Caller should check if it has * enough children. * * @param completelyVisible Whether child should be completely visible or not * @return The first visible child closest to start of the layout from user's perspective. */ private View findFirstVisibleChildClosestToStart(boolean completelyVisible, boolean acceptPartiallyVisible) { if (mShouldReverseLayout) { return findOneVisibleChild(getChildCount() - 1, -1, completelyVisible, acceptPartiallyVisible); } else { return findOneVisibleChild(0, getChildCount(), completelyVisible, acceptPartiallyVisible); } } /** * Convenience method to find the visible child closes to end. Caller should check if it has * enough children. * * @param completelyVisible Whether child should be completely visible or not * @return The first visible child closest to end of the layout from user's perspective. */ private View findFirstVisibleChildClosestToEnd(boolean completelyVisible, boolean acceptPartiallyVisible) { if (mShouldReverseLayout) { return findOneVisibleChild(0, getChildCount(), completelyVisible, acceptPartiallyVisible); } else { return findOneVisibleChild(getChildCount() - 1, -1, completelyVisible, acceptPartiallyVisible); } } /** * Among the children that are suitable to be considered as an anchor child, returns the one * closest to the end of the layout. *

* Due to ambiguous adapter updates or children being removed, some children's positions may be * invalid. This method is a best effort to find a position within adapter bounds if possible. *

* It also prioritizes children that are within the visible bounds. * @return A View that can be used an an anchor View. */ private View findReferenceChildClosestToEnd(RecyclerView.Recycler recycler, RecyclerView.State state) { return mShouldReverseLayout ? findFirstReferenceChild(recycler, state) : findLastReferenceChild(recycler, state); } /** * Among the children that are suitable to be considered as an anchor child, returns the one * closest to the start of the layout. *

* Due to ambiguous adapter updates or children being removed, some children's positions may be * invalid. This method is a best effort to find a position within adapter bounds if possible. *

* It also prioritizes children that are within the visible bounds. * * @return A View that can be used an an anchor View. */ private View findReferenceChildClosestToStart(RecyclerView.Recycler recycler, RecyclerView.State state) { return mShouldReverseLayout ? findLastReferenceChild(recycler, state) : findFirstReferenceChild(recycler, state); } private View findFirstReferenceChild(RecyclerView.Recycler recycler, RecyclerView.State state) { return findReferenceChild(recycler, state, 0, getChildCount(), state.getItemCount()); } private View findLastReferenceChild(RecyclerView.Recycler recycler, RecyclerView.State state) { return findReferenceChild(recycler, state, getChildCount() - 1, -1, state.getItemCount()); } // overridden by GridLayoutManager View findReferenceChild(RecyclerView.Recycler recycler, RecyclerView.State state, int start, int end, int itemCount) { ensureLayoutState(); View invalidMatch = null; View outOfBoundsMatch = null; final int boundsStart = mOrientationHelper.getStartAfterPadding(); final int boundsEnd = mOrientationHelper.getEndAfterPadding(); final int diff = end > start ? 1 : -1; for (int i = start; i != end; i += diff) { final View view = getChildAt(i); final int position = getPosition(view); if (position >= 0 && position < itemCount) { if (((LayoutParams) view.getLayoutParams()).isItemRemoved()) { if (invalidMatch == null) { invalidMatch = view; // removed item, least preferred } } else if (mOrientationHelper.getDecoratedStart(view) >= boundsEnd || mOrientationHelper.getDecoratedEnd(view) < boundsStart) { if (outOfBoundsMatch == null) { outOfBoundsMatch = view; // item is not visible, less preferred } } else { return view; } } } return outOfBoundsMatch != null ? outOfBoundsMatch : invalidMatch; } /** * Returns the adapter position of the first visible view. This position does not include * adapter changes that were dispatched after the last layout pass. *

* Note that, this value is not affected by layout orientation or item order traversal. * ({@link #setReverseLayout(boolean)}). Views are sorted by their positions in the adapter, * not in the layout. *

* If RecyclerView has item decorators, they will be considered in calculations as well. *

* LayoutManager may pre-cache some views that are not necessarily visible. Those views * are ignored in this method. * * @return The adapter position of the first visible item or {@link RecyclerView#NO_POSITION} if * there aren't any visible items. * @see #findFirstCompletelyVisibleItemPosition() * @see #findLastVisibleItemPosition() */ public int findFirstVisibleItemPosition() { final View child = findOneVisibleChild(0, getChildCount(), false, true); return child == null ? NO_POSITION : getPosition(child); } /** * Returns the adapter position of the first fully visible view. This position does not include * adapter changes that were dispatched after the last layout pass. *

* Note that bounds check is only performed in the current orientation. That means, if * LayoutManager is horizontal, it will only check the view's left and right edges. * * @return The adapter position of the first fully visible item or * {@link RecyclerView#NO_POSITION} if there aren't any visible items. * @see #findFirstVisibleItemPosition() * @see #findLastCompletelyVisibleItemPosition() */ public int findFirstCompletelyVisibleItemPosition() { final View child = findOneVisibleChild(0, getChildCount(), true, false); return child == null ? NO_POSITION : getPosition(child); } /** * Returns the adapter position of the last visible view. This position does not include * adapter changes that were dispatched after the last layout pass. *

* Note that, this value is not affected by layout orientation or item order traversal. * ({@link #setReverseLayout(boolean)}). Views are sorted by their positions in the adapter, * not in the layout. *

* If RecyclerView has item decorators, they will be considered in calculations as well. *

* LayoutManager may pre-cache some views that are not necessarily visible. Those views * are ignored in this method. * * @return The adapter position of the last visible view or {@link RecyclerView#NO_POSITION} if * there aren't any visible items. * @see #findLastCompletelyVisibleItemPosition() * @see #findFirstVisibleItemPosition() */ public int findLastVisibleItemPosition() { final View child = findOneVisibleChild(getChildCount() - 1, -1, false, true); return child == null ? NO_POSITION : getPosition(child); } /** * Returns the adapter position of the last fully visible view. This position does not include * adapter changes that were dispatched after the last layout pass. *

* Note that bounds check is only performed in the current orientation. That means, if * LayoutManager is horizontal, it will only check the view's left and right edges. * * @return The adapter position of the last fully visible view or * {@link RecyclerView#NO_POSITION} if there aren't any visible items. * @see #findLastVisibleItemPosition() * @see #findFirstCompletelyVisibleItemPosition() */ public int findLastCompletelyVisibleItemPosition() { final View child = findOneVisibleChild(getChildCount() - 1, -1, true, false); return child == null ? NO_POSITION : getPosition(child); } View findOneVisibleChild(int fromIndex, int toIndex, boolean completelyVisible, boolean acceptPartiallyVisible) { ensureLayoutState(); final int start = mOrientationHelper.getStartAfterPadding(); final int end = mOrientationHelper.getEndAfterPadding(); final int next = toIndex > fromIndex ? 1 : -1; View partiallyVisible = null; for (int i = fromIndex; i != toIndex; i += next) { final View child = getChildAt(i); final int childStart = mOrientationHelper.getDecoratedStart(child); final int childEnd = mOrientationHelper.getDecoratedEnd(child); if (childStart < end && childEnd > start) { if (completelyVisible) { if (childStart >= start && childEnd <= end) { return child; } else if (acceptPartiallyVisible && partiallyVisible == null) { partiallyVisible = child; } } else { return child; } } } return partiallyVisible; } @Override public View onFocusSearchFailed(View focused, int focusDirection, RecyclerView.Recycler recycler, RecyclerView.State state) { resolveShouldLayoutReverse(); if (getChildCount() == 0) { return null; } final int layoutDir = convertFocusDirectionToLayoutDirection(focusDirection); if (layoutDir == LayoutState.INVALID_LAYOUT) { return null; } ensureLayoutState(); final View referenceChild; if (layoutDir == LayoutState.LAYOUT_START) { referenceChild = findReferenceChildClosestToStart(recycler, state); } else { referenceChild = findReferenceChildClosestToEnd(recycler, state); } if (referenceChild == null) { if (DEBUG) { Log.d(TAG, "Cannot find a child with a valid position to be used for focus search."); } return null; } ensureLayoutState(); final int maxScroll = (int) (MAX_SCROLL_FACTOR * mOrientationHelper.getTotalSpace()); updateLayoutState(layoutDir, maxScroll, false, state); mLayoutState.mScrollingOffset = LayoutState.SCROLLING_OFFSET_NaN; mLayoutState.mRecycle = false; fill(recycler, mLayoutState, state, true); final View nextFocus; if (layoutDir == LayoutState.LAYOUT_START) { nextFocus = getChildClosestToStart(); } else { nextFocus = getChildClosestToEnd(); } if (nextFocus == referenceChild || !nextFocus.isFocusable()) { return null; } return nextFocus; } /** * Used for debugging. * Logs the internal representation of children to default logger. */ private void logChildren() { Log.d(TAG, "internal representation of views on the screen"); for (int i = 0; i < getChildCount(); i++) { View child = getChildAt(i); Log.d(TAG, "item " + getPosition(child) + ", coord:" + mOrientationHelper.getDecoratedStart(child)); } Log.d(TAG, "=============="); } /** * Used for debugging. * Validates that child views are laid out in correct order. This is important because rest of * the algorithm relies on this constraint. * * In default layout, child 0 should be closest to screen position 0 and last child should be * closest to position WIDTH or HEIGHT. * In reverse layout, last child should be closes to screen position 0 and first child should * be closest to position WIDTH or HEIGHT */ void validateChildOrder() { Log.d(TAG, "validating child count " + getChildCount()); if (getChildCount() < 1) { return; } int lastPos = getPosition(getChildAt(0)); int lastScreenLoc = mOrientationHelper.getDecoratedStart(getChildAt(0)); if (mShouldReverseLayout) { for (int i = 1; i < getChildCount(); i++) { View child = getChildAt(i); int pos = getPosition(child); int screenLoc = mOrientationHelper.getDecoratedStart(child); if (pos < lastPos) { logChildren(); throw new RuntimeException("detected invalid position. loc invalid? " + (screenLoc < lastScreenLoc)); } if (screenLoc > lastScreenLoc) { logChildren(); throw new RuntimeException("detected invalid location"); } } } else { for (int i = 1; i < getChildCount(); i++) { View child = getChildAt(i); int pos = getPosition(child); int screenLoc = mOrientationHelper.getDecoratedStart(child); if (pos < lastPos) { logChildren(); throw new RuntimeException("detected invalid position. loc invalid? " + (screenLoc < lastScreenLoc)); } if (screenLoc < lastScreenLoc) { logChildren(); throw new RuntimeException("detected invalid location"); } } } } @Override public boolean supportsPredictiveItemAnimations() { return mPendingSavedState == null && mLastStackFromEnd == mStackFromEnd; } /** * @hide This method should be called by ItemTouchHelper only. */ @Override public void prepareForDrop(View view, View target, int x, int y) { assertNotInLayoutOrScroll("Cannot drop a view during a scroll or layout calculation"); ensureLayoutState(); resolveShouldLayoutReverse(); final int myPos = getPosition(view); final int targetPos = getPosition(target); final int dropDirection = myPos < targetPos ? LayoutState.ITEM_DIRECTION_TAIL : LayoutState.ITEM_DIRECTION_HEAD; if (mShouldReverseLayout) { if (dropDirection == LayoutState.ITEM_DIRECTION_TAIL) { scrollToPositionWithOffset(targetPos, mOrientationHelper.getEndAfterPadding() - (mOrientationHelper.getDecoratedStart(target) + mOrientationHelper.getDecoratedMeasurement(view))); } else { scrollToPositionWithOffset(targetPos, mOrientationHelper.getEndAfterPadding() - mOrientationHelper.getDecoratedEnd(target)); } } else { if (dropDirection == LayoutState.ITEM_DIRECTION_HEAD) { scrollToPositionWithOffset(targetPos, mOrientationHelper.getDecoratedStart(target)); } else { scrollToPositionWithOffset(targetPos, mOrientationHelper.getDecoratedEnd(target) - mOrientationHelper.getDecoratedMeasurement(view)); } } } /** * Helper class that keeps temporary state while {LayoutManager} is filling out the empty * space. */ static class LayoutState { static final String TAG = "LLM#LayoutState"; static final int LAYOUT_START = -1; static final int LAYOUT_END = 1; static final int INVALID_LAYOUT = Integer.MIN_VALUE; static final int ITEM_DIRECTION_HEAD = -1; static final int ITEM_DIRECTION_TAIL = 1; static final int SCROLLING_OFFSET_NaN = Integer.MIN_VALUE; /** * We may not want to recycle children in some cases (e.g. layout) */ boolean mRecycle = true; /** * Pixel offset where layout should start */ int mOffset; /** * Number of pixels that we should fill, in the layout direction. */ int mAvailable; /** * Current position on the adapter to get the next item. */ int mCurrentPosition; /** * Defines the direction in which the data adapter is traversed. * Should be {@link #ITEM_DIRECTION_HEAD} or {@link #ITEM_DIRECTION_TAIL} */ int mItemDirection; /** * Defines the direction in which the layout is filled. * Should be {@link #LAYOUT_START} or {@link #LAYOUT_END} */ int mLayoutDirection; /** * Used when LayoutState is constructed in a scrolling state. * It should be set the amount of scrolling we can make without creating a new view. * Settings this is required for efficient view recycling. */ int mScrollingOffset; /** * Used if you want to pre-layout items that are not yet visible. * The difference with {@link #mAvailable} is that, when recycling, distance laid out for * {@link #mExtra} is not considered to avoid recycling visible children. */ int mExtra = 0; /** * Equal to {@link RecyclerView.State#isPreLayout()}. When consuming scrap, if this value * is set to true, we skip removed views since they should not be laid out in post layout * step. */ boolean mIsPreLayout = false; /** * The most recent {@link #scrollBy(int, RecyclerView.Recycler, RecyclerView.State)} * amount. */ int mLastScrollDelta; /** * When LLM needs to layout particular views, it sets this list in which case, LayoutState * will only return views from this list and return null if it cannot find an item. */ List mScrapList = null; /** * Used when there is no limit in how many views can be laid out. */ boolean mInfinite; /** * @return true if there are more items in the data adapter */ boolean hasMore(RecyclerView.State state) { return mCurrentPosition >= 0 && mCurrentPosition < state.getItemCount(); } /** * Gets the view for the next element that we should layout. * Also updates current item index to the next item, based on {@link #mItemDirection} * * @return The next element that we should layout. */ View next(RecyclerView.Recycler recycler) { if (mScrapList != null) { return nextViewFromScrapList(); } final View view = recycler.getViewForPosition(mCurrentPosition); mCurrentPosition += mItemDirection; return view; } /** * Returns the next item from the scrap list. *

* Upon finding a valid VH, sets current item position to VH.itemPosition + mItemDirection * * @return View if an item in the current position or direction exists if not null. */ private View nextViewFromScrapList() { final int size = mScrapList.size(); for (int i = 0; i < size; i++) { final View view = mScrapList.get(i).itemView; final LayoutParams lp = (LayoutParams) view.getLayoutParams(); if (lp.isItemRemoved()) { continue; } if (mCurrentPosition == lp.getViewLayoutPosition()) { assignPositionFromScrapList(view); return view; } } return null; } public void assignPositionFromScrapList() { assignPositionFromScrapList(null); } public void assignPositionFromScrapList(View ignore) { final View closest = nextViewInLimitedList(ignore); if (closest == null) { mCurrentPosition = NO_POSITION; } else { mCurrentPosition = ((LayoutParams) closest.getLayoutParams()) .getViewLayoutPosition(); } } public View nextViewInLimitedList(View ignore) { int size = mScrapList.size(); View closest = null; int closestDistance = Integer.MAX_VALUE; if (DEBUG && mIsPreLayout) { throw new IllegalStateException("Scrap list cannot be used in pre layout"); } for (int i = 0; i < size; i++) { View view = mScrapList.get(i).itemView; final LayoutParams lp = (LayoutParams) view.getLayoutParams(); if (view == ignore || lp.isItemRemoved()) { continue; } final int distance = (lp.getViewLayoutPosition() - mCurrentPosition) * mItemDirection; if (distance < 0) { continue; // item is not in current direction } if (distance < closestDistance) { closest = view; closestDistance = distance; if (distance == 0) { break; } } } return closest; } void log() { Log.d(TAG, "avail:" + mAvailable + ", ind:" + mCurrentPosition + ", dir:" + mItemDirection + ", offset:" + mOffset + ", layoutDir:" + mLayoutDirection); } } /** * @hide */ public static class SavedState implements Parcelable { int mAnchorPosition; int mAnchorOffset; boolean mAnchorLayoutFromEnd; public SavedState() { } SavedState(Parcel in) { mAnchorPosition = in.readInt(); mAnchorOffset = in.readInt(); mAnchorLayoutFromEnd = in.readInt() == 1; } public SavedState(SavedState other) { mAnchorPosition = other.mAnchorPosition; mAnchorOffset = other.mAnchorOffset; mAnchorLayoutFromEnd = other.mAnchorLayoutFromEnd; } boolean hasValidAnchor() { return mAnchorPosition >= 0; } void invalidateAnchor() { mAnchorPosition = NO_POSITION; } @Override public int describeContents() { return 0; } @Override public void writeToParcel(Parcel dest, int flags) { dest.writeInt(mAnchorPosition); dest.writeInt(mAnchorOffset); dest.writeInt(mAnchorLayoutFromEnd ? 1 : 0); } public static final Parcelable.Creator CREATOR = new Parcelable.Creator() { @Override public SavedState createFromParcel(Parcel in) { return new SavedState(in); } @Override public SavedState[] newArray(int size) { return new SavedState[size]; } }; } /** * Simple data class to keep Anchor information */ class AnchorInfo { int mPosition; int mCoordinate; boolean mLayoutFromEnd; boolean mValid; AnchorInfo() { reset(); } void reset() { mPosition = NO_POSITION; mCoordinate = INVALID_OFFSET; mLayoutFromEnd = false; mValid = false; } /** * assigns anchor coordinate from the RecyclerView's padding depending on current * layoutFromEnd value */ void assignCoordinateFromPadding() { mCoordinate = mLayoutFromEnd ? mOrientationHelper.getEndAfterPadding() : mOrientationHelper.getStartAfterPadding(); } @Override public String toString() { return "AnchorInfo{" + "mPosition=" + mPosition + ", mCoordinate=" + mCoordinate + ", mLayoutFromEnd=" + mLayoutFromEnd + ", mValid=" + mValid + '}'; } boolean isViewValidAsAnchor(View child, RecyclerView.State state) { LayoutParams lp = (LayoutParams) child.getLayoutParams(); return !lp.isItemRemoved() && lp.getViewLayoutPosition() >= 0 && lp.getViewLayoutPosition() < state.getItemCount(); } public void assignFromViewAndKeepVisibleRect(View child) { final int spaceChange = mOrientationHelper.getTotalSpaceChange(); if (spaceChange >= 0) { assignFromView(child); return; } mPosition = getPosition(child); if (mLayoutFromEnd) { final int prevLayoutEnd = mOrientationHelper.getEndAfterPadding() - spaceChange; final int childEnd = mOrientationHelper.getDecoratedEnd(child); final int previousEndMargin = prevLayoutEnd - childEnd; mCoordinate = mOrientationHelper.getEndAfterPadding() - previousEndMargin; // ensure we did not push child's top out of bounds because of this if (previousEndMargin > 0) { // we have room to shift bottom if necessary final int childSize = mOrientationHelper.getDecoratedMeasurement(child); final int estimatedChildStart = mCoordinate - childSize; final int layoutStart = mOrientationHelper.getStartAfterPadding(); final int previousStartMargin = mOrientationHelper.getDecoratedStart(child) - layoutStart; final int startReference = layoutStart + Math.min(previousStartMargin, 0); final int startMargin = estimatedChildStart - startReference; if (startMargin < 0) { // offset to make top visible but not too much mCoordinate += Math.min(previousEndMargin, -startMargin); } } } else { final int childStart = mOrientationHelper.getDecoratedStart(child); final int startMargin = childStart - mOrientationHelper.getStartAfterPadding(); mCoordinate = childStart; if (startMargin > 0) { // we have room to fix end as well final int estimatedEnd = childStart + mOrientationHelper.getDecoratedMeasurement(child); final int previousLayoutEnd = mOrientationHelper.getEndAfterPadding() - spaceChange; final int previousEndMargin = previousLayoutEnd - mOrientationHelper.getDecoratedEnd(child); final int endReference = mOrientationHelper.getEndAfterPadding() - Math.min(0, previousEndMargin); final int endMargin = endReference - estimatedEnd; if (endMargin < 0) { mCoordinate -= Math.min(startMargin, -endMargin); } } } } public void assignFromView(View child) { if (mLayoutFromEnd) { mCoordinate = mOrientationHelper.getDecoratedEnd(child) + mOrientationHelper.getTotalSpaceChange(); } else { mCoordinate = mOrientationHelper.getDecoratedStart(child); } mPosition = getPosition(child); } } protected static class LayoutChunkResult { public int mConsumed; public boolean mFinished; public boolean mIgnoreConsumed; public boolean mFocusable; void resetInternal() { mConsumed = 0; mFinished = false; mIgnoreConsumed = false; mFocusable = false; } } }