LinearLayoutManager.java revision 49c83b12201dde5b93d4eca3d44478e0c967a2e6
1/*
2 * Copyright (C) 2014 The Android Open Source Project
3 *
4 * Licensed under the Apache License, Version 2.0 (the "License");
5 * you may not use this file except in compliance with the License.
6 * You may obtain a copy of the License at
7 *
8 *      http://www.apache.org/licenses/LICENSE-2.0
9 *
10 * Unless required by applicable law or agreed to in writing, software
11 * distributed under the License is distributed on an "AS IS" BASIS,
12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 * See the License for the specific languag`e governing permissions and
14 * limitations under the License.
15 */
16
17package android.support.v7.widget;
18
19import android.content.Context;
20import android.os.Parcel;
21import android.os.Parcelable;
22import android.graphics.PointF;
23import android.support.v4.view.ViewCompat;
24import android.util.Log;
25import android.view.View;
26import android.view.ViewGroup;
27
28import java.util.List;
29
30/**
31 * A {@link android.support.v7.widget.RecyclerView.LayoutManager} implementation which provides
32 * similar functionality to {@link android.widget.ListView}.
33 */
34public class LinearLayoutManager extends RecyclerView.LayoutManager {
35
36    private static final String TAG = "LinearLayoutManager";
37
38    private static final boolean DEBUG = false;
39
40    public static final int HORIZONTAL = OrientationHelper.HORIZONTAL;
41
42    public static final int VERTICAL = OrientationHelper.VERTICAL;
43
44    public static final int INVALID_OFFSET = Integer.MIN_VALUE;
45
46
47    /**
48     * While trying to find next view to focus, LinearLayoutManager will not try to scroll more
49     * than
50     * this factor times the total space of the list. If layout is vertical, total space is the
51     * height minus padding, if layout is horizontal, total space is the width minus padding.
52     */
53    private static final float MAX_SCROLL_FACTOR = 0.33f;
54
55
56    /**
57     * Current orientation. Either {@link #HORIZONTAL} or {@link #VERTICAL}
58     */
59    private int mOrientation;
60
61    /**
62     * Helper class that keeps temporary layout state.
63     * It does not keep state after layout is complete but we still keep a reference to re-use
64     * the same object.
65     */
66    private LayoutState mLayoutState;
67
68    /**
69     * Many calculations are made depending on orientation. To keep it clean, this interface
70     * helps {@link LinearLayoutManager} make those decisions.
71     * Based on {@link #mOrientation}, an implementation is lazily created in
72     * {@link #ensureLayoutState} method.
73     */
74    OrientationHelper mOrientationHelper;
75
76    /**
77     * We need to track this so that we can ignore current position when it changes.
78     */
79    private boolean mLastStackFromEnd;
80
81
82    /**
83     * Defines if layout should be calculated from end to start.
84     *
85     * @see #mShouldReverseLayout
86     */
87    private boolean mReverseLayout = false;
88
89    /**
90     * This keeps the final value for how LayoutManager shouls start laying out views.
91     * It is calculated by checking {@link #getReverseLayout()} and View's layout direction.
92     * {@link #onLayoutChildren(RecyclerView.Recycler, RecyclerView.State)} is run.
93     */
94    private boolean mShouldReverseLayout = false;
95
96    /**
97     * Works the same way as {@link android.widget.AbsListView#setStackFromBottom(boolean)} and
98     * it supports both orientations.
99     * see {@link android.widget.AbsListView#setStackFromBottom(boolean)}
100     */
101    private boolean mStackFromEnd = false;
102
103    /**
104     * When LayoutManager needs to scroll to a position, it sets this variable and requests a
105     * layout which will check this variable and re-layout accordingly.
106     */
107    private int mPendingScrollPosition = RecyclerView.NO_POSITION;
108
109    /**
110     * Used to keep the offset value when {@link #scrollToPositionWithOffset(int, int)} is
111     * called.
112     */
113    private int mPendingScrollPositionOffset = INVALID_OFFSET;
114
115    private boolean mRecycleChildrenOnDetach;
116
117    private SavedState mPendingSavedState = null;
118
119    /**
120     * Creates a vertical LinearLayoutManager
121     *
122     * @param context Current context, will be used to access resources.
123     */
124    public LinearLayoutManager(Context context) {
125        this(context, VERTICAL, false);
126    }
127
128    /**
129     * @param context       Current context, will be used to access resources.
130     * @param orientation   Layout orientation. Should be {@link #HORIZONTAL} or {@link
131     *                      #VERTICAL}.
132     * @param reverseLayout When set to true, layouts from end to start.
133     */
134    public LinearLayoutManager(Context context, int orientation, boolean reverseLayout) {
135        setOrientation(orientation);
136        setReverseLayout(reverseLayout);
137    }
138
139    /**
140     * {@inheritDoc}
141     */
142    @Override
143    public RecyclerView.LayoutParams generateDefaultLayoutParams() {
144        return new RecyclerView.LayoutParams(ViewGroup.LayoutParams.WRAP_CONTENT,
145                ViewGroup.LayoutParams.WRAP_CONTENT);
146    }
147
148    /**
149     * Returns whether LinearLayoutManager will recycle its children when it is detached from
150     * RecyclerView.
151     *
152     * @return true if LinearLayoutManager will recycle its children when it is detached from
153     * RecyclerView.
154     */
155    public boolean getRecycleChildrenOnDetach() {
156        return mRecycleChildrenOnDetach;
157    }
158
159    /**
160     * Set whether LinearLayoutManager will recycle its children when it is detached from
161     * RecyclerView.
162     * <p>
163     * If you are using a {@link RecyclerView.RecycledViewPool}, it might be a good idea to set
164     * this flag to <code>true</code> so that views will be avilable to other RecyclerViews
165     * immediately.
166     * <p>
167     * Note that, setting this flag will result in a performance drop if RecyclerView
168     * is restored.
169     *
170     * @param recycleChildrenOnDetach Whether children should be recycled in detach or not.
171     */
172    public void setRecycleChildrenOnDetach(boolean recycleChildrenOnDetach) {
173        if (mPendingSavedState != null &&
174                mPendingSavedState.mRecycleChildrenOnDetach != recycleChildrenOnDetach) {
175            // override pending state
176            mPendingSavedState.mRecycleChildrenOnDetach = recycleChildrenOnDetach;
177        }
178        mRecycleChildrenOnDetach = recycleChildrenOnDetach;
179    }
180
181    @Override
182    public void onDetachedFromWindow(RecyclerView view, RecyclerView.Recycler recycler) {
183        super.onDetachedFromWindow(view, recycler);
184        if (mRecycleChildrenOnDetach) {
185            removeAndRecycleAllViews(recycler);
186            recycler.clear();
187        }
188    }
189
190    @Override
191    public Parcelable onSaveInstanceState() {
192        if (mPendingSavedState != null) {
193            return new SavedState(mPendingSavedState);
194        }
195        SavedState state = new SavedState();
196        state.mRecycleChildrenOnDetach = mRecycleChildrenOnDetach;
197        if (getChildCount() > 0) {
198            boolean didLayoutFromEnd = mLastStackFromEnd ^ mShouldReverseLayout;
199            state.mAnchorLayoutFromEnd = didLayoutFromEnd;
200            if (didLayoutFromEnd) {
201                final View refChild = getChildClosestToEnd();
202                state.mAnchorOffset = mOrientationHelper.getEndAfterPadding() -
203                        mOrientationHelper.getDecoratedEnd(refChild);
204                state.mAnchorPosition = getPosition(refChild);
205            } else {
206                final View refChild = getChildClosestToStart();
207                state.mAnchorPosition = getPosition(refChild);
208                state.mAnchorOffset = mOrientationHelper.getDecoratedStart(refChild) -
209                        mOrientationHelper.getStartAfterPadding();
210            }
211        } else {
212            state.invalidateAnchor();
213        }
214        state.mStackFromEnd = mStackFromEnd;
215        state.mReverseLayout = mReverseLayout;
216        state.mOrientation = mOrientation;
217        return state;
218    }
219
220    @Override
221    public void onRestoreInstanceState(Parcelable state) {
222        if (state instanceof SavedState) {
223            mPendingSavedState = (SavedState) state;
224            requestLayout();
225            if (DEBUG) {
226                Log.d(TAG, "loaded saved state");
227            }
228        } else if (DEBUG) {
229            Log.d(TAG, "invalid saved state class");
230        }
231    }
232
233    /**
234     * @return true if {@link #getOrientation()} is {@link #HORIZONTAL}
235     */
236    @Override
237    public boolean canScrollHorizontally() {
238        return mOrientation == HORIZONTAL;
239    }
240
241    /**
242     * @return true if {@link #getOrientation()} is {@link #VERTICAL}
243     */
244    @Override
245    public boolean canScrollVertically() {
246        return mOrientation == VERTICAL;
247    }
248
249    /**
250     * Compatibility support for {@link android.widget.AbsListView#setStackFromBottom(boolean)}
251     */
252    public void setStackFromEnd(boolean stackFromEnd) {
253        if (mPendingSavedState != null && mPendingSavedState.mStackFromEnd != stackFromEnd) {
254            // override pending state
255            mPendingSavedState.mStackFromEnd = stackFromEnd;
256        }
257        if (mStackFromEnd == stackFromEnd) {
258            return;
259        }
260        mStackFromEnd = stackFromEnd;
261        requestLayout();
262    }
263
264    public boolean getStackFromEnd() {
265        return mStackFromEnd;
266    }
267
268    /**
269     * Returns the current orientaion of the layout.
270     *
271     * @return Current orientation.
272     * @see #mOrientation
273     * @see #setOrientation(int)
274     */
275    public int getOrientation() {
276        return mOrientation;
277    }
278
279    /**
280     * Sets the orientation of the layout. {@link android.support.v7.widget.LinearLayoutManager}
281     * will do its best to keep scroll position.
282     *
283     * @param orientation {@link #HORIZONTAL} or {@link #VERTICAL}
284     */
285    public void setOrientation(int orientation) {
286        if (orientation != HORIZONTAL && orientation != VERTICAL) {
287            throw new IllegalArgumentException("invalid orientation:" + orientation);
288        }
289        if (mPendingSavedState != null && mPendingSavedState.mOrientation != orientation) {
290            // override pending state
291            mPendingSavedState.mOrientation = orientation;
292        }
293        if (orientation == mOrientation) {
294            return;
295        }
296        mOrientation = orientation;
297        mOrientationHelper = null;
298        requestLayout();
299    }
300
301    /**
302     * Calculates the view layout order. (e.g. from end to start or start to end)
303     * RTL layout support is applied automatically. So if layout is RTL and
304     * {@link #getReverseLayout()} is {@code true}, elements will be laid out starting from left.
305     */
306    private void resolveShouldLayoutReverse() {
307        // A == B is the same result, but we rather keep it readable
308        if (mOrientation == VERTICAL || !isLayoutRTL()) {
309            mShouldReverseLayout = mReverseLayout;
310        } else {
311            mShouldReverseLayout = !mReverseLayout;
312        }
313    }
314
315    /**
316     * Returns if views are laid out from the opposite direction of the layout.
317     *
318     * @return If layout is reversed or not.
319     * @see {@link #setReverseLayout(boolean)}
320     */
321    public boolean getReverseLayout() {
322        return mReverseLayout;
323    }
324
325    /**
326     * Used to reverse item traversal and layout order.
327     * This behaves similar to the layout change for RTL views. When set to true, first item is
328     * laid out at the end of the UI, second item is laid out before it etc.
329     *
330     * For horizontal layouts, it depends on the layout direction.
331     * When set to true, If {@link android.support.v7.widget.RecyclerView} is LTR, than it will
332     * layout from RTL, if {@link android.support.v7.widget.RecyclerView}} is RTL, it will layout
333     * from LTR.
334     *
335     * If you are looking for the exact same behavior of
336     * {@link android.widget.AbsListView#setStackFromBottom(boolean)}, use
337     * {@link #setStackFromEnd(boolean)}
338     */
339    public void setReverseLayout(boolean reverseLayout) {
340        if (mPendingSavedState != null && mPendingSavedState.mReverseLayout != reverseLayout) {
341            // override pending state
342            mPendingSavedState.mReverseLayout = reverseLayout;
343        }
344        if (reverseLayout == mReverseLayout) {
345            return;
346        }
347        mReverseLayout = reverseLayout;
348        requestLayout();
349    }
350
351    /**
352     * {@inheritDoc}
353     */
354    @Override
355    public View findViewByPosition(int position) {
356        final int childCount = getChildCount();
357        if (childCount == 0) {
358            return null;
359        }
360        final int firstChild = getPosition(getChildAt(0));
361        final int viewPosition = position - firstChild;
362        if (viewPosition >= 0 && viewPosition < childCount) {
363            return getChildAt(viewPosition);
364        }
365        return null;
366    }
367
368    /**
369     * <p>Returns the amount of extra space that should be laid out by LinearLayoutManager.
370     * By default, {@link android.support.v7.widget.LinearLayoutManager} lays out 1 extra page of
371     * items while smooth scrolling and 0 otherwise. You can override this method to implement your
372     * custom layout pre-cache logic.</p>
373     * <p>Laying out invisible elements will eventually come with performance cost. On the other
374     * hand, in places like smooth scrolling to an unknown location, this extra content helps
375     * LayoutManager to calculate a much smoother scrolling; which improves user experience.</p>
376     * <p>You can also use this if you are trying to pre-layout your upcoming views.</p>
377     *
378     * @return The extra space that should be laid out (in pixels).
379     */
380    protected int getExtraLayoutSpace(RecyclerView.State state) {
381        if (state.hasTargetScrollPosition()) {
382            return mOrientationHelper.getTotalSpace();
383        } else {
384            return 0;
385        }
386    }
387
388    @Override
389    public void smoothScrollToPosition(RecyclerView recyclerView, RecyclerView.State state,
390            int position) {
391        LinearSmoothScroller linearSmoothScroller =
392                new LinearSmoothScroller(recyclerView.getContext()) {
393                    @Override
394                    public PointF computeScrollVectorForPosition(int targetPosition) {
395                        return LinearLayoutManager.this
396                                .computeScrollVectorForPosition(targetPosition);
397                    }
398                };
399        linearSmoothScroller.setTargetPosition(position);
400        startSmoothScroll(linearSmoothScroller);
401    }
402
403    public PointF computeScrollVectorForPosition(int targetPosition) {
404        if (getChildCount() == 0) {
405            return null;
406        }
407        final int firstChildPos = getPosition(getChildAt(0));
408        final int direction = targetPosition < firstChildPos != mShouldReverseLayout ? -1 : 1;
409        if (mOrientation == HORIZONTAL) {
410            return new PointF(direction, 0);
411        } else {
412            return new PointF(0, direction);
413        }
414    }
415
416    /**
417     * {@inheritDoc}
418     */
419    @Override
420    public void onLayoutChildren(RecyclerView.Recycler recycler, RecyclerView.State state) {
421        // layout algorithm:
422        // 1) by checking children and other variables, find an anchor coordinate and an anchor
423        //  item position.
424        // 2) fill towards start, stacking from bottom
425        // 3) fill towards end, stacking from top
426        // 4) scroll to fulfill requirements like stack from bottom.
427        // create layout state
428        if (DEBUG) {
429            Log.d(TAG, "is pre layout:" + state.isPreLayout());
430        }
431        if (mPendingSavedState != null) {
432            setOrientation(mPendingSavedState.mOrientation);
433            setReverseLayout(mPendingSavedState.mReverseLayout);
434            setStackFromEnd(mPendingSavedState.mStackFromEnd);
435            setRecycleChildrenOnDetach(mPendingSavedState.mRecycleChildrenOnDetach);
436            if (mPendingSavedState.hasValidAnchor()) {
437                mPendingScrollPosition = mPendingSavedState.mAnchorPosition;
438            }
439        }
440
441        ensureLayoutState();
442        // resolve layout direction
443        resolveShouldLayoutReverse();
444
445        // validate scroll position if exists
446        if (mPendingScrollPosition != RecyclerView.NO_POSITION) {
447            // validate it
448            if (mPendingScrollPosition < 0 || mPendingScrollPosition >= state.getItemCount()) {
449                mPendingScrollPosition = RecyclerView.NO_POSITION;
450                mPendingScrollPositionOffset = INVALID_OFFSET;
451                if (DEBUG) {
452                    Log.e(TAG, "ignoring invalid scroll position " + mPendingScrollPosition);
453                }
454            }
455        }
456        // this value might be updated if there is a target scroll position without an offset
457        boolean layoutFromEnd = mShouldReverseLayout ^ mStackFromEnd;
458
459        final boolean stackFromEndChanged = mLastStackFromEnd != mStackFromEnd;
460
461        int anchorCoordinate, anchorItemPosition;
462        if (mPendingScrollPosition != RecyclerView.NO_POSITION) {
463            // if child is visible, try to make it a reference child and ensure it is fully visible.
464            // if child is not visible, align it depending on its virtual position.
465            anchorItemPosition = mPendingScrollPosition;
466            if (mPendingSavedState != null && mPendingSavedState.hasValidAnchor()) {
467                // Anchor offset depends on how that child was laid out. Here, we update it
468                // according to our current view bounds
469                layoutFromEnd = mPendingSavedState.mAnchorLayoutFromEnd;
470                if (layoutFromEnd) {
471                    anchorCoordinate = mOrientationHelper.getEndAfterPadding() -
472                            mPendingSavedState.mAnchorOffset;
473                } else {
474                    anchorCoordinate = mOrientationHelper.getStartAfterPadding() +
475                            mPendingSavedState.mAnchorOffset;
476                }
477            } else if (mPendingScrollPositionOffset == INVALID_OFFSET) {
478                View child = findViewByPosition(mPendingScrollPosition);
479                if (child != null) {
480                    final int startGap = mOrientationHelper.getDecoratedStart(child)
481                            - mOrientationHelper.getStartAfterPadding();
482                    final int endGap = mOrientationHelper.getEndAfterPadding() -
483                            mOrientationHelper.getDecoratedEnd(child);
484                    final int childSize = mOrientationHelper.getDecoratedMeasurement(child);
485                    if (childSize > mOrientationHelper.getTotalSpace()) {
486                        // item does not fit. fix depending on layout direction
487                        anchorCoordinate = layoutFromEnd ? mOrientationHelper.getEndAfterPadding()
488                                : mOrientationHelper.getStartAfterPadding();
489                    } else if (startGap < 0) {
490                        anchorCoordinate = mOrientationHelper.getStartAfterPadding();
491                        layoutFromEnd = false;
492                    } else if (endGap < 0) {
493                        anchorCoordinate = mOrientationHelper.getEndAfterPadding();
494                        layoutFromEnd = true;
495                    } else {
496                        anchorCoordinate = layoutFromEnd
497                                ? mOrientationHelper.getDecoratedEnd(child)
498                                : mOrientationHelper.getDecoratedStart(child);
499                    }
500                } else { // item is not visible.
501                    if (getChildCount() > 0) {
502                        // get position of any child, does not matter
503                        int pos = getPosition(getChildAt(0));
504                        if (mPendingScrollPosition < pos == mShouldReverseLayout) {
505                            anchorCoordinate = mOrientationHelper.getEndAfterPadding();
506                            layoutFromEnd = true;
507                        } else {
508                            anchorCoordinate = mOrientationHelper.getStartAfterPadding();
509                            layoutFromEnd = false;
510                        }
511                    } else {
512                        anchorCoordinate = layoutFromEnd ? mOrientationHelper.getEndAfterPadding()
513                                : mOrientationHelper.getStartAfterPadding();
514                    }
515                }
516            } else {
517                // override layout from end values for consistency
518                if (mShouldReverseLayout) {
519                    anchorCoordinate = mOrientationHelper.getEndAfterPadding()
520                            - mPendingScrollPositionOffset;
521                    layoutFromEnd = true;
522                } else {
523                    anchorCoordinate = mOrientationHelper.getStartAfterPadding()
524                            + mPendingScrollPositionOffset;
525                    layoutFromEnd = false;
526                }
527            }
528        } else if (getChildCount() > 0 && !stackFromEndChanged) {
529            if (layoutFromEnd) {
530                View referenceChild = getChildClosestToEnd();
531                anchorCoordinate = mOrientationHelper.getDecoratedEnd(referenceChild);
532                anchorItemPosition = getPosition(referenceChild);
533            } else {
534                View referenceChild = getChildClosestToStart();
535                anchorCoordinate = mOrientationHelper.getDecoratedStart(referenceChild);
536                anchorItemPosition = getPosition(referenceChild);
537            }
538        } else {
539            anchorCoordinate = layoutFromEnd ? mOrientationHelper.getEndAfterPadding() :
540                    mOrientationHelper.getStartAfterPadding();
541            anchorItemPosition = mStackFromEnd ? state.getItemCount() - 1 : 0;
542        }
543
544        detachAndScrapAttachedViews(recycler);
545        int extraForStart;
546        int extraForEnd;
547        final int extra = getExtraLayoutSpace(state);
548        boolean before = state.getTargetScrollPosition() < anchorItemPosition;
549        if (before == mShouldReverseLayout) {
550            extraForEnd = extra;
551            extraForStart = 0;
552        } else {
553            extraForStart = extra;
554            extraForEnd = 0;
555        }
556        int startOffset;
557        int endOffset;
558        if (layoutFromEnd) {
559            // fill towards start
560            updateLayoutStateToFillStart(anchorItemPosition, anchorCoordinate);
561            mLayoutState.mExtra = extraForStart;
562            fill(recycler, mLayoutState, state, false);
563            startOffset = mLayoutState.mOffset;
564            extraForEnd += mLayoutState.mAvailable;
565            // fill towards end
566            updateLayoutStateToFillEnd(anchorItemPosition, anchorCoordinate);
567            mLayoutState.mExtra = extraForEnd;
568            mLayoutState.mCurrentPosition += mLayoutState.mItemDirection;
569            fill(recycler, mLayoutState, state, false);
570            endOffset = mLayoutState.mOffset;
571        } else {
572            // fill towards end
573            updateLayoutStateToFillEnd(anchorItemPosition, anchorCoordinate);
574            mLayoutState.mExtra = extraForEnd;
575            fill(recycler, mLayoutState, state, false);
576            endOffset = mLayoutState.mOffset;
577            extraForStart += mLayoutState.mAvailable;
578            // fill towards start
579            updateLayoutStateToFillStart(anchorItemPosition, anchorCoordinate);
580            mLayoutState.mExtra = extraForStart;
581            mLayoutState.mCurrentPosition += mLayoutState.mItemDirection;
582            fill(recycler, mLayoutState, state, false);
583            startOffset = mLayoutState.mOffset;
584        }
585
586        // changes may cause gaps on the UI, try to fix them.
587        if (getChildCount() > 0) {
588            // because layout from end may be changed by scroll to position
589            // we re-calculate it.
590            // find which side we should check for gaps.
591            if (mShouldReverseLayout ^ mStackFromEnd) {
592                int fixOffset = fixLayoutEndGap(endOffset, recycler, state, true);
593                startOffset += fixOffset;
594                endOffset += fixOffset;
595                fixOffset = fixLayoutStartGap(startOffset, recycler, state, false);
596                startOffset += fixOffset;
597                endOffset += fixOffset;
598            } else {
599                int fixOffset = fixLayoutStartGap(startOffset, recycler, state, true);
600                startOffset += fixOffset;
601                endOffset += fixOffset;
602                fixOffset = fixLayoutEndGap(endOffset, recycler, state, false);
603                startOffset += fixOffset;
604                endOffset += fixOffset;
605            }
606        }
607
608        // If there are scrap children that we did not layout, we need to find where they did go
609        // and layout them accordingly so that animations can work as expected.
610        // This case may happen if new views are added or an existing view expands and pushes
611        // another view out of bounds.
612        if (state.willRunPredictiveAnimations() &&  getChildCount() > 0 && !state.isPreLayout()) {
613            // to make the logic simpler, we calculate the size of children and call fill.
614            int scrapExtraStart = 0, scrapExtraEnd = 0;
615            final List<RecyclerView.ViewHolder> scrapList = recycler.getScrapList();
616            final int scrapSize = scrapList.size();
617            final int firstChildPos = getPosition(getChildAt(0));
618            for (int i = 0; i < scrapSize; i++) {
619                RecyclerView.ViewHolder scrap = scrapList.get(i);
620                final int position = scrap.getPosition();
621                final int direction = position < firstChildPos != mShouldReverseLayout
622                        ? LayoutState.LAYOUT_START : LayoutState.LAYOUT_END;
623                if (direction == LayoutState.LAYOUT_START) {
624                    scrapExtraStart += mOrientationHelper.getDecoratedMeasurement(scrap.itemView);
625                } else {
626                    scrapExtraEnd += mOrientationHelper.getDecoratedMeasurement(scrap.itemView);
627                }
628            }
629
630            if (DEBUG) {
631                Log.d(TAG, "for unused scrap, decided to add " + scrapExtraStart
632                        + " towards start and " + scrapExtraEnd + " towards end");
633            }
634            mLayoutState.mScrapList = scrapList;
635            if (scrapExtraStart > 0) {
636                View anchor = getChildClosestToStart();
637                updateLayoutStateToFillStart(getPosition(anchor), startOffset);
638                mLayoutState.mExtra = scrapExtraStart;
639                mLayoutState.mAvailable = 0;
640                mLayoutState.mCurrentPosition += mShouldReverseLayout ? 1 : -1;
641                fill(recycler, mLayoutState, state, false);
642            }
643
644            if (scrapExtraEnd > 0) {
645                View anchor = getChildClosestToEnd();
646                updateLayoutStateToFillEnd(getPosition(anchor), endOffset);
647                mLayoutState.mExtra = scrapExtraEnd;
648                mLayoutState.mAvailable = 0;
649                mLayoutState.mCurrentPosition += mShouldReverseLayout ? -1 : 1;
650                fill(recycler, mLayoutState, state, false);
651            }
652            mLayoutState.mScrapList = null;
653        }
654
655        mPendingScrollPosition = RecyclerView.NO_POSITION;
656        mPendingScrollPositionOffset = INVALID_OFFSET;
657        mLastStackFromEnd = mStackFromEnd;
658        mPendingSavedState = null; // we don't need this anymore
659        if (DEBUG) {
660            validateChildOrder();
661        }
662    }
663
664    /**
665     * @return The final offset amount for children
666     */
667    private int fixLayoutEndGap(int endOffset, RecyclerView.Recycler recycler,
668            RecyclerView.State state, boolean canOffsetChildren) {
669        int gap = mOrientationHelper.getEndAfterPadding() - endOffset;
670        int fixOffset = 0;
671        if (gap > 0) {
672            fixOffset = -scrollBy(-gap, recycler, state);
673        } else {
674            return 0; // nothing to fix
675        }
676        // move offset according to scroll amount
677        endOffset += fixOffset;
678        if (canOffsetChildren) {
679            // re-calculate gap, see if we could fix it
680            gap = mOrientationHelper.getEndAfterPadding() - endOffset;
681            if (gap > 0) {
682                mOrientationHelper.offsetChildren(gap);
683                return gap + fixOffset;
684            }
685        }
686        return fixOffset;
687    }
688
689    /**
690     * @return The final offset amount for children
691     */
692    private int fixLayoutStartGap(int startOffset, RecyclerView.Recycler recycler,
693            RecyclerView.State state, boolean canOffsetChildren) {
694        int gap = startOffset - mOrientationHelper.getStartAfterPadding();
695        int fixOffset = 0;
696        if (gap > 0) {
697            // check if we should fix this gap.
698            fixOffset = -scrollBy(gap, recycler, state);
699        } else {
700            return 0; // nothing to fix
701        }
702        startOffset += fixOffset;
703        if (canOffsetChildren) {
704            // re-calculate gap, see if we could fix it
705            gap = startOffset - mOrientationHelper.getStartAfterPadding();
706            if (gap > 0) {
707                mOrientationHelper.offsetChildren(-gap);
708                return fixOffset - gap;
709            }
710        }
711        return fixOffset;
712    }
713
714    private void updateLayoutStateToFillEnd(int itemPosition, int offset) {
715        mLayoutState.mAvailable = mOrientationHelper.getEndAfterPadding() - offset;
716        mLayoutState.mItemDirection = mShouldReverseLayout ? LayoutState.ITEM_DIRECTION_HEAD :
717                LayoutState.ITEM_DIRECTION_TAIL;
718        mLayoutState.mCurrentPosition = itemPosition;
719        mLayoutState.mLayoutDirection = LayoutState.LAYOUT_END;
720        mLayoutState.mOffset = offset;
721        mLayoutState.mScrollingOffset = LayoutState.SCOLLING_OFFSET_NaN;
722    }
723
724    private void updateLayoutStateToFillStart(int itemPosition, int offset) {
725        mLayoutState.mAvailable = offset - mOrientationHelper.getStartAfterPadding();
726        mLayoutState.mCurrentPosition = itemPosition;
727        mLayoutState.mItemDirection = mShouldReverseLayout ? LayoutState.ITEM_DIRECTION_TAIL :
728                LayoutState.ITEM_DIRECTION_HEAD;
729        mLayoutState.mLayoutDirection = LayoutState.LAYOUT_START;
730        mLayoutState.mOffset = offset;
731        mLayoutState.mScrollingOffset = LayoutState.SCOLLING_OFFSET_NaN;
732
733    }
734
735
736    private boolean isLayoutRTL() {
737        return getLayoutDirection() == ViewCompat.LAYOUT_DIRECTION_RTL;
738    }
739
740    void ensureLayoutState() {
741        if (mLayoutState == null) {
742            mLayoutState = new LayoutState();
743        }
744        if (mOrientationHelper == null) {
745            mOrientationHelper = OrientationHelper.createOrientationHelper(this, mOrientation);
746        }
747    }
748
749    /**
750     * <p>Scroll the RecyclerView to make the position visible.</p>
751     *
752     * <p>RecyclerView will scroll the minimum amount that is necessary to make the
753     * target position visible. If you are looking for a similar behavior to
754     * {@link android.widget.ListView#setSelection(int)} or
755     * {@link android.widget.ListView#setSelectionFromTop(int, int)}, use
756     * {@link #scrollToPositionWithOffset(int, int)}.</p>
757     *
758     * <p>Note that scroll position change will not be reflected until the next layout call.</p>
759     *
760     * @param position Scroll to this adapter position
761     * @see #scrollToPositionWithOffset(int, int)
762     */
763    @Override
764    public void scrollToPosition(int position) {
765        mPendingScrollPosition = position;
766        mPendingScrollPositionOffset = INVALID_OFFSET;
767        if (mPendingSavedState != null) {
768            mPendingSavedState.invalidateAnchor();
769        }
770        requestLayout();
771    }
772
773    /**
774     * <p>Scroll to the specified adapter position with the given offset from layout start.</p>
775     *
776     * <p>Note that scroll position change will not be reflected until the next layout call.</p>
777     *
778     * <p>If you are just trying to make a position visible, use {@link
779     * #scrollToPosition(int)}.</p>
780     *
781     * @param position Index (starting at 0) of the reference item.
782     * @param offset   The distance (in pixels) between the start edge of the item view and
783     *                 start edge of the RecyclerView.
784     * @see #setReverseLayout(boolean)
785     * @see #scrollToPosition(int)
786     */
787    public void scrollToPositionWithOffset(int position, int offset) {
788        mPendingScrollPosition = position;
789        mPendingScrollPositionOffset = offset;
790        if (mPendingSavedState != null) {
791            mPendingSavedState.invalidateAnchor();
792        }
793        requestLayout();
794    }
795
796
797    /**
798     * {@inheritDoc}
799     */
800    @Override
801    public int scrollHorizontallyBy(int dx, RecyclerView.Recycler recycler,
802            RecyclerView.State state) {
803        if (mOrientation == VERTICAL) {
804            return 0;
805        }
806        return scrollBy(dx, recycler, state);
807    }
808
809    /**
810     * {@inheritDoc}
811     */
812    @Override
813    public int scrollVerticallyBy(int dy, RecyclerView.Recycler recycler,
814            RecyclerView.State state) {
815        if (mOrientation == HORIZONTAL) {
816            return 0;
817        }
818        return scrollBy(dy, recycler, state);
819    }
820
821    @Override
822    public int computeHorizontalScrollOffset(RecyclerView.State state) {
823        if (getChildCount() == 0) {
824            return 0;
825        }
826        final int topPosition = getPosition(getChildClosestToStart());
827        return mShouldReverseLayout ? state.getItemCount() - 1 - topPosition : topPosition;
828    }
829
830    @Override
831    public int computeVerticalScrollOffset(RecyclerView.State state) {
832        if (getChildCount() == 0) {
833            return 0;
834        }
835        final int topPosition = getPosition(getChildClosestToStart());
836        return mShouldReverseLayout ? state.getItemCount() - 1 - topPosition : topPosition;
837    }
838
839    @Override
840    public int computeHorizontalScrollExtent(RecyclerView.State state) {
841        return getChildCount();
842    }
843
844    @Override
845    public int computeVerticalScrollExtent(RecyclerView.State state) {
846        return getChildCount();
847    }
848
849    @Override
850    public int computeHorizontalScrollRange(RecyclerView.State state) {
851        return state.getItemCount();
852    }
853
854    @Override
855    public int computeVerticalScrollRange(RecyclerView.State state) {
856        return state.getItemCount();
857    }
858
859    private void updateLayoutState(int layoutDirection, int requiredSpace,
860            boolean canUseExistingSpace, RecyclerView.State state) {
861        mLayoutState.mExtra = getExtraLayoutSpace(state);
862        mLayoutState.mLayoutDirection = layoutDirection;
863        int fastScrollSpace;
864        if (layoutDirection == LayoutState.LAYOUT_END) {
865            // get the first child in the direction we are going
866            final View child = getChildClosestToEnd();
867            // the direction in which we are traversing children
868            mLayoutState.mItemDirection = mShouldReverseLayout ? LayoutState.ITEM_DIRECTION_HEAD
869                    : LayoutState.ITEM_DIRECTION_TAIL;
870            mLayoutState.mCurrentPosition = getPosition(child) + mLayoutState.mItemDirection;
871            mLayoutState.mOffset = mOrientationHelper.getDecoratedEnd(child);
872            // calculate how much we can scroll without adding new children (independent of layout)
873            fastScrollSpace = mOrientationHelper.getDecoratedEnd(child)
874                    - mOrientationHelper.getEndAfterPadding();
875
876        } else {
877            final View child = getChildClosestToStart();
878            mLayoutState.mItemDirection = mShouldReverseLayout ? LayoutState.ITEM_DIRECTION_TAIL
879                    : LayoutState.ITEM_DIRECTION_HEAD;
880            mLayoutState.mCurrentPosition = getPosition(child) + mLayoutState.mItemDirection;
881            mLayoutState.mOffset = mOrientationHelper.getDecoratedStart(child);
882            fastScrollSpace = -mOrientationHelper.getDecoratedStart(child)
883                    + mOrientationHelper.getStartAfterPadding();
884        }
885        mLayoutState.mAvailable = requiredSpace;
886        if (canUseExistingSpace) {
887            mLayoutState.mAvailable -= fastScrollSpace;
888        }
889        mLayoutState.mScrollingOffset = fastScrollSpace;
890    }
891
892    private int scrollBy(int dy, RecyclerView.Recycler recycler, RecyclerView.State state) {
893        if (getChildCount() == 0 || dy == 0) {
894            return 0;
895        }
896        ensureLayoutState();
897        final int layoutDirection = dy > 0 ? LayoutState.LAYOUT_END : LayoutState.LAYOUT_START;
898        final int absDy = Math.abs(dy);
899        updateLayoutState(layoutDirection, absDy, true, state);
900        final int freeScroll = mLayoutState.mScrollingOffset;
901        final int consumed = freeScroll + fill(recycler, mLayoutState, state, false);
902        if (consumed < 0) {
903            if (DEBUG) {
904                Log.d(TAG, "Don't have any more elements to scroll");
905            }
906            return 0;
907        }
908        final int scrolled = absDy > consumed ? layoutDirection * consumed : dy;
909        mOrientationHelper.offsetChildren(-scrolled);
910        if (DEBUG) {
911            Log.d(TAG, "scroll req: " + dy + " scrolled: " + scrolled);
912        }
913        return scrolled;
914    }
915
916    /**
917     * Recycles children between given indices.
918     *
919     * @param startIndex inclusive
920     * @param endIndex   exclusive
921     */
922    private void recycleChildren(RecyclerView.Recycler recycler, int startIndex, int endIndex) {
923        if (startIndex == endIndex) {
924            return;
925        }
926        if (DEBUG) {
927            Log.d(TAG, "Recycling " + Math.abs(startIndex - endIndex) + " items");
928        }
929        if (endIndex > startIndex) {
930            for (int i = endIndex - 1; i >= startIndex; i--) {
931                removeAndRecycleViewAt(i, recycler);
932            }
933        } else {
934            for (int i = startIndex; i > endIndex; i--) {
935                removeAndRecycleViewAt(i, recycler);
936            }
937        }
938    }
939
940    /**
941     * Recycles views that went out of bounds after scrolling towards the end of the layout.
942     *
943     * @param recycler Recycler instance of {@link android.support.v7.widget.RecyclerView}
944     * @param dt       This can be used to add additional padding to the visible area. This is used
945     *                 to
946     *                 detect children that will go out of bounds after scrolling, without actually
947     *                 moving them.
948     */
949    private void recycleViewsFromStart(RecyclerView.Recycler recycler, int dt) {
950        if (dt < 0) {
951            if (DEBUG) {
952                Log.d(TAG, "Called recycle from start with a negative value. This might happen"
953                        + " during layout changes but may be sign of a bug");
954            }
955            return;
956        }
957        // ignore padding, ViewGroup may not clip children.
958        final int limit = dt;
959        final int childCount = getChildCount();
960        if (mShouldReverseLayout) {
961            for (int i = childCount - 1; i >= 0; i--) {
962                View child = getChildAt(i);
963                if (mOrientationHelper.getDecoratedEnd(child) > limit) {// stop here
964                    recycleChildren(recycler, childCount - 1, i);
965                    return;
966                }
967            }
968        } else {
969            for (int i = 0; i < childCount; i++) {
970                View child = getChildAt(i);
971                if (mOrientationHelper.getDecoratedEnd(child) > limit) {// stop here
972                    recycleChildren(recycler, 0, i);
973                    return;
974                }
975            }
976        }
977    }
978
979
980    /**
981     * Recycles views that went out of bounds after scrolling towards the start of the layout.
982     *
983     * @param recycler Recycler instance of {@link android.support.v7.widget.RecyclerView}
984     * @param dt       This can be used to add additional padding to the visible area. This is used
985     *                 to detect children that will go out of bounds after scrolling, without
986     *                 actually moving them.
987     */
988    private void recycleViewsFromEnd(RecyclerView.Recycler recycler, int dt) {
989        final int childCount = getChildCount();
990        if (dt < 0) {
991            if (DEBUG) {
992                Log.d(TAG, "Called recycle from end with a negative value. This might happen"
993                        + " during layout changes but may be sign of a bug");
994            }
995            return;
996        }
997        final int limit = mOrientationHelper.getEnd() - dt;
998        if (mShouldReverseLayout) {
999            for (int i = 0; i < childCount; i++) {
1000                View child = getChildAt(i);
1001                if (mOrientationHelper.getDecoratedStart(child) < limit) {// stop here
1002                    recycleChildren(recycler, 0, i);
1003                    return;
1004                }
1005            }
1006        } else {
1007            for (int i = childCount - 1; i >= 0; i--) {
1008                View child = getChildAt(i);
1009                if (mOrientationHelper.getDecoratedStart(child) < limit) {// stop here
1010                    recycleChildren(recycler, childCount - 1, i);
1011                    return;
1012                }
1013            }
1014        }
1015
1016    }
1017
1018    /**
1019     * Helper method to call appropriate recycle method depending on current layout direction
1020     *
1021     * @param recycler    Current recycler that is attached to RecyclerView
1022     * @param layoutState Current layout state. Right now, this object does not change but
1023     *                    we may consider moving it out of this view so passing around as a
1024     *                    parameter for now, rather than accessing {@link #mLayoutState}
1025     * @see #recycleViewsFromStart(android.support.v7.widget.RecyclerView.Recycler, int)
1026     * @see #recycleViewsFromEnd(android.support.v7.widget.RecyclerView.Recycler, int)
1027     * @see android.support.v7.widget.LinearLayoutManager.LayoutState#mLayoutDirection
1028     */
1029    private void recycleByLayoutState(RecyclerView.Recycler recycler, LayoutState layoutState) {
1030        if (layoutState.mLayoutDirection == LayoutState.LAYOUT_START) {
1031            recycleViewsFromEnd(recycler, layoutState.mScrollingOffset);
1032        } else {
1033            recycleViewsFromStart(recycler, layoutState.mScrollingOffset);
1034        }
1035    }
1036
1037    /**
1038     * The magic functions :). Fills the given layout, defined by the layoutState. This is fairly
1039     * independent from the rest of the {@link android.support.v7.widget.LinearLayoutManager}
1040     * and with little change, can be made publicly available as a helper class.
1041     *
1042     * @param recycler        Current recycler that is attached to RecyclerView
1043     * @param layoutState     Configuration on how we should fill out the available space.
1044     * @param state           Context passed by the RecyclerView to control scroll steps.
1045     * @param stopOnFocusable If true, filling stops in the first focusable new child
1046     * @return Number of pixels that it added. Useful for scoll functions.
1047     */
1048    private int fill(RecyclerView.Recycler recycler, LayoutState layoutState,
1049            RecyclerView.State state,
1050            boolean stopOnFocusable) {
1051        // max offset we should set is mFastScroll + available
1052        final int start = layoutState.mAvailable;
1053        if (layoutState.mScrollingOffset != LayoutState.SCOLLING_OFFSET_NaN) {
1054            // TODO ugly bug fix. should not happen
1055            if (layoutState.mAvailable < 0) {
1056                layoutState.mScrollingOffset += layoutState.mAvailable;
1057            }
1058            recycleByLayoutState(recycler, layoutState);
1059        }
1060        int remainingSpace = layoutState.mAvailable + layoutState.mExtra;
1061        while (remainingSpace > 0 && layoutState.hasMore(state)) {
1062            View view = layoutState.next(recycler);
1063            if (view == null) {
1064                if (DEBUG && layoutState.mScrapList == null) {
1065                    throw new RuntimeException("received null view when unexpected");
1066                }
1067                // if we are laying out views in scrap, this may return null which means there is
1068                // no more items to layout.
1069                break;
1070            }
1071            RecyclerView.LayoutParams params = (RecyclerView.LayoutParams) view.getLayoutParams();
1072            if (mLayoutState.mScrapList == null) {
1073                if (mShouldReverseLayout == (layoutState.mLayoutDirection
1074                        == LayoutState.LAYOUT_START)) {
1075                    addView(view);
1076                } else {
1077                    addView(view, 0);
1078                }
1079            } else {
1080                if (mShouldReverseLayout == (layoutState.mLayoutDirection
1081                        == LayoutState.LAYOUT_START)) {
1082                    addDisappearingView(view);
1083                } else {
1084                    addDisappearingView(view, 0);
1085                }
1086            }
1087            measureChildWithMargins(view, 0, 0);
1088            int consumed = mOrientationHelper.getDecoratedMeasurement(view);
1089            int left, top, right, bottom;
1090            if (mOrientation == VERTICAL) {
1091                if (isLayoutRTL()) {
1092                    right = getWidth() - getPaddingRight();
1093                    left = right - mOrientationHelper.getDecoratedMeasurementInOther(view);
1094                } else {
1095                    left = getPaddingLeft();
1096                    right = left + mOrientationHelper.getDecoratedMeasurementInOther(view);
1097                }
1098                if (layoutState.mLayoutDirection == LayoutState.LAYOUT_START) {
1099                    bottom = layoutState.mOffset;
1100                    top = layoutState.mOffset - consumed;
1101                } else {
1102                    top = layoutState.mOffset;
1103                    bottom = layoutState.mOffset + consumed;
1104                }
1105            } else {
1106                top = getPaddingTop();
1107                bottom = top + mOrientationHelper.getDecoratedMeasurementInOther(view);
1108
1109                if (layoutState.mLayoutDirection == LayoutState.LAYOUT_START) {
1110                    right = layoutState.mOffset;
1111                    left = layoutState.mOffset - consumed;
1112                } else {
1113                    left = layoutState.mOffset;
1114                    right = layoutState.mOffset + consumed;
1115                }
1116            }
1117            // We calculate everything with View's bounding box (which includes decor and margins)
1118            // To calculate correct layout position, we subtract margins.
1119            layoutDecorated(view, left + params.leftMargin, top + params.topMargin
1120                    , right - params.rightMargin, bottom - params.bottomMargin);
1121            if (DEBUG) {
1122                Log.d(TAG, "laid out child at position " + getPosition(view) + ", with l:"
1123                        + (left + params.leftMargin) + ", t:" + (top + params.topMargin) + ", r:"
1124                        + (right - params.rightMargin) + ", b:" + (bottom - params.bottomMargin));
1125            }
1126            layoutState.mOffset += consumed * layoutState.mLayoutDirection;
1127
1128            if (!params.isItemRemoved() || mLayoutState.mScrapList == null) {
1129                layoutState.mAvailable -= consumed;
1130                // we keep a separate remaining space because mAvailable is important for recycling
1131                remainingSpace -= consumed;
1132            }
1133
1134            if (layoutState.mScrollingOffset != LayoutState.SCOLLING_OFFSET_NaN) {
1135                layoutState.mScrollingOffset += consumed;
1136                if (layoutState.mAvailable < 0) {
1137                    layoutState.mScrollingOffset += layoutState.mAvailable;
1138                }
1139                recycleByLayoutState(recycler, layoutState);
1140            }
1141            if (stopOnFocusable && view.isFocusable()) {
1142                break;
1143            }
1144
1145            if (state != null && state.getTargetScrollPosition() == getPosition(view)) {
1146                break;
1147            }
1148        }
1149        if (DEBUG) {
1150            validateChildOrder();
1151        }
1152        return start - layoutState.mAvailable;
1153    }
1154
1155    /**
1156     * Converts a focusDirection to orientation.
1157     *
1158     * @param focusDirection One of {@link View#FOCUS_UP}, {@link View#FOCUS_DOWN},
1159     *                       {@link View#FOCUS_LEFT}, {@link View#FOCUS_RIGHT},
1160     *                       {@link View#FOCUS_BACKWARD}, {@link View#FOCUS_FORWARD}
1161     *                       or 0 for not applicable
1162     * @return {@link LayoutState#LAYOUT_START} or {@link LayoutState#LAYOUT_END} if focus direction
1163     * is applicable to current state, {@link LayoutState#INVALID_LAYOUT} otherwise.
1164     */
1165    private int convertFocusDirectionToLayoutDirection(int focusDirection) {
1166        switch (focusDirection) {
1167            case View.FOCUS_BACKWARD:
1168                return LayoutState.LAYOUT_START;
1169            case View.FOCUS_FORWARD:
1170                return LayoutState.LAYOUT_END;
1171            case View.FOCUS_UP:
1172                return mOrientation == VERTICAL ? LayoutState.LAYOUT_START
1173                        : LayoutState.INVALID_LAYOUT;
1174            case View.FOCUS_DOWN:
1175                return mOrientation == VERTICAL ? LayoutState.LAYOUT_END
1176                        : LayoutState.INVALID_LAYOUT;
1177            case View.FOCUS_LEFT:
1178                return mOrientation == HORIZONTAL ? LayoutState.LAYOUT_START
1179                        : LayoutState.INVALID_LAYOUT;
1180            case View.FOCUS_RIGHT:
1181                return mOrientation == HORIZONTAL ? LayoutState.LAYOUT_END
1182                        : LayoutState.INVALID_LAYOUT;
1183            default:
1184                if (DEBUG) {
1185                    Log.d(TAG, "Unknown focus request:" + focusDirection);
1186                }
1187                return LayoutState.INVALID_LAYOUT;
1188        }
1189
1190    }
1191
1192    /**
1193     * Convenience method to find the child closes to start. Caller should check it has enough
1194     * children.
1195     * @return The child closes to start of the layout from user's perspective.
1196     */
1197    private View getChildClosestToStart() {
1198        return getChildAt(mShouldReverseLayout ? getChildCount() - 1 : 0);
1199    }
1200
1201    /**
1202     * Convenience method to find the child closes to end. Caller should check it has enough
1203     * children.
1204     * @return The child closes to end of the layout from user's perspective.
1205     */
1206    private View getChildClosestToEnd() {
1207        return getChildAt(mShouldReverseLayout ? 0 : getChildCount() - 1);
1208    }
1209
1210    /**
1211     * Returns the adapter position of the first visible view.
1212     * <p>
1213     * Note that, this value is not affected by layout orientation or item order traversal.
1214     * ({@link #setReverseLayout(boolean)}). Views are sorted by their positions in the adapter,
1215     * not in the layout.
1216     * <p>
1217     * If RecyclerView has item decorators, they will be considered in calculations as well.
1218     * <p>
1219     * LinearLayoutManager may pre-cache some views that are not necessarily visible. Those views
1220     * are ignored in this method.
1221     *
1222     * @return The adapter position of the first visible item or {@link RecyclerView#NO_POSITION} if
1223     * there aren't any visible items.
1224     * @see #findFirstCompletelyVisibleItemPosition()
1225     * @see #findLastVisibleItemPosition()
1226     */
1227    public int findFirstVisibleItemPosition() {
1228        return findOneVisibleChild(0, getChildCount(), false);
1229    }
1230
1231    /**
1232     * Returns the adapter position of the first fully visible view.
1233     * <p>
1234     * Note that bounds check is only performed in the current orientation. That means, if
1235     * LinearLayoutManager is horizontal, it will only check the view's left and right edges.
1236     *
1237     * @return The adapter position of the first fully visible item or
1238     * {@link RecyclerView#NO_POSITION} if there aren't any visible items.
1239     * @see #findFirstVisibleItemPosition()
1240     * @see #findLastCompletelyVisibleItemPosition()
1241     */
1242    public int findFirstCompletelyVisibleItemPosition() {
1243        return findOneVisibleChild(0, getChildCount(), true);
1244    }
1245
1246    /**
1247     * Returns the adapter position of the last visible view.
1248     * <p>
1249     * Note that, this value is not affected by layout orientation or item order traversal.
1250     * ({@link #setReverseLayout(boolean)}). Views are sorted by their positions in the adapter,
1251     * not in the layout.
1252     * <p>
1253     * If RecyclerView has item decorators, they will be considered in calculations as well.
1254     * <p>
1255     * LinearLayoutManager may pre-cache some views that are not necessarily visible. Those views
1256     * are ignored in this method.
1257     *
1258     * @return The adapter position of the last visible view or {@link RecyclerView#NO_POSITION} if
1259     * there aren't any visible items.
1260     * @see #findLastCompletelyVisibleItemPosition()
1261     * @see #findFirstVisibleItemPosition()
1262     */
1263    public int findLastVisibleItemPosition() {
1264        return findOneVisibleChild(getChildCount() - 1, -1, false);
1265    }
1266
1267    /**
1268     * Returns the adapter position of the last fully visible view.
1269     * <p>
1270     * Note that bounds check is only performed in the current orientation. That means, if
1271     * LinearLayoutManager is horizontal, it will only check the view's left and right edges.
1272     *
1273     * @return The adapter position of the last fully visible view or
1274     * {@link RecyclerView#NO_POSITION} if there aren't any visible items.
1275     * @see #findLastVisibleItemPosition()
1276     * @see #findFirstCompletelyVisibleItemPosition()
1277     */
1278    public int findLastCompletelyVisibleItemPosition() {
1279        return findOneVisibleChild(getChildCount() - 1, -1, true);
1280    }
1281
1282    int findOneVisibleChild(int fromIndex, int toIndex, boolean completelyVisible) {
1283        final int start = mOrientationHelper.getStartAfterPadding();
1284        final int end = mOrientationHelper.getEndAfterPadding();
1285        final int next = toIndex > fromIndex ? 1 : -1;
1286        for (int i = fromIndex; i != toIndex; i+=next) {
1287            final View child = getChildAt(i);
1288            final int childStart = mOrientationHelper.getDecoratedStart(child);
1289            final int childEnd = mOrientationHelper.getDecoratedEnd(child);
1290            if (childStart < end && childEnd > start) {
1291                if (completelyVisible) {
1292                    if (childStart >= start && childEnd <= end) {
1293                        return getPosition(child);
1294                    }
1295                } else {
1296                    return getPosition(child);
1297                }
1298            }
1299        }
1300        return RecyclerView.NO_POSITION;
1301    }
1302
1303    @Override
1304    public View onFocusSearchFailed(View focused, int focusDirection,
1305            RecyclerView.Recycler recycler, RecyclerView.State state) {
1306        resolveShouldLayoutReverse();
1307        if (getChildCount() == 0) {
1308            return null;
1309        }
1310
1311        final int layoutDir = convertFocusDirectionToLayoutDirection(focusDirection);
1312        if (layoutDir == LayoutState.INVALID_LAYOUT) {
1313            return null;
1314        }
1315        final View referenceChild;
1316        if (layoutDir == LayoutState.LAYOUT_START) {
1317            referenceChild = getChildClosestToStart();
1318        } else {
1319            referenceChild = getChildClosestToEnd();
1320        }
1321        ensureLayoutState();
1322        final int maxScroll = (int) (MAX_SCROLL_FACTOR * (mOrientationHelper.getEndAfterPadding() -
1323                mOrientationHelper.getStartAfterPadding()));
1324        updateLayoutState(layoutDir, maxScroll, false, state);
1325        mLayoutState.mScrollingOffset = LayoutState.SCOLLING_OFFSET_NaN;
1326        fill(recycler, mLayoutState, state, true);
1327        final View nextFocus;
1328        if (layoutDir == LayoutState.LAYOUT_START) {
1329            nextFocus = getChildClosestToStart();
1330        } else {
1331            nextFocus = getChildClosestToEnd();
1332        }
1333        if (nextFocus == referenceChild || !nextFocus.isFocusable()) {
1334            return null;
1335        }
1336        return nextFocus;
1337    }
1338
1339    /**
1340     * Used for debugging.
1341     * Logs the internal representation of children to default logger.
1342     */
1343    private void logChildren() {
1344        Log.d(TAG, "internal representation of views on the screen");
1345        for (int i = 0; i < getChildCount(); i++) {
1346            View child = getChildAt(i);
1347            Log.d(TAG, "item " + getPosition(child) + ", coord:"
1348                    + mOrientationHelper.getDecoratedStart(child));
1349        }
1350        Log.d(TAG, "==============");
1351    }
1352
1353    /**
1354     * Used for debugging.
1355     * Validates that child views are laid out in correct order. This is important because rest of
1356     * the algorithm relies on this constraint.
1357     *
1358     * In default layout, child 0 should be closest to screen position 0 and last child should be
1359     * closest to position WIDTH or HEIGHT.
1360     * In reverse layout, last child should be closes to screen position 0 and first child should
1361     * be closest to position WIDTH  or HEIGHT
1362     */
1363    private void validateChildOrder() {
1364        Log.d(TAG, "validating child count " + getChildCount());
1365        if (getChildCount() < 1) {
1366            return;
1367        }
1368        int lastPos = getPosition(getChildAt(0));
1369        int lastScreenLoc = mOrientationHelper.getDecoratedStart(getChildAt(0));
1370        if (mShouldReverseLayout) {
1371            for (int i = 1; i < getChildCount(); i++) {
1372                View child = getChildAt(i);
1373                int pos = getPosition(child);
1374                int screenLoc = mOrientationHelper.getDecoratedStart(child);
1375                if (pos < lastPos) {
1376                    logChildren();
1377                    throw new RuntimeException("detected invalid position. loc invalid? " +
1378                            (screenLoc < lastScreenLoc));
1379                }
1380                if (screenLoc > lastScreenLoc) {
1381                    logChildren();
1382                    throw new RuntimeException("detected invalid location");
1383                }
1384            }
1385        } else {
1386            for (int i = 1; i < getChildCount(); i++) {
1387                View child = getChildAt(i);
1388                int pos = getPosition(child);
1389                int screenLoc = mOrientationHelper.getDecoratedStart(child);
1390                if (pos < lastPos) {
1391                    logChildren();
1392                    throw new RuntimeException("detected invalid position. loc invalid? " +
1393                            (screenLoc < lastScreenLoc));
1394                }
1395                if (screenLoc < lastScreenLoc) {
1396                    logChildren();
1397                    throw new RuntimeException("detected invalid location");
1398                }
1399            }
1400        }
1401    }
1402
1403    @Override
1404    public boolean supportsPredictiveItemAnimations() {
1405        return true;
1406    }
1407
1408    /**
1409     * Helper class that keeps temporary state while {LayoutManager} is filling out the empty
1410     * space.
1411     */
1412    private static class LayoutState {
1413
1414        final static String TAG = "LinearLayoutManager#LayoutState";
1415
1416        final static int LAYOUT_START = -1;
1417
1418        final static int LAYOUT_END = 1;
1419
1420        final static int INVALID_LAYOUT = Integer.MIN_VALUE;
1421
1422        final static int ITEM_DIRECTION_HEAD = -1;
1423
1424        final static int ITEM_DIRECTION_TAIL = 1;
1425
1426        final static int SCOLLING_OFFSET_NaN = Integer.MIN_VALUE;
1427
1428        /**
1429         * Pixel offset where layout should start
1430         */
1431        int mOffset;
1432
1433        /**
1434         * Number of pixels that we should fill, in the layout direction.
1435         */
1436        int mAvailable;
1437
1438        /**
1439         * Current position on the adapter to get the next item.
1440         */
1441        int mCurrentPosition;
1442
1443        /**
1444         * Defines the direction in which the data adapter is traversed.
1445         * Should be {@link #ITEM_DIRECTION_HEAD} or {@link #ITEM_DIRECTION_TAIL}
1446         */
1447        int mItemDirection;
1448
1449        /**
1450         * Defines the direction in which the layout is filled.
1451         * Should be {@link #LAYOUT_START} or {@link #LAYOUT_END}
1452         */
1453        int mLayoutDirection;
1454
1455        /**
1456         * Used when LayoutState is constructed in a scrolling state.
1457         * It should be set the amount of scrolling we can make without creating a new view.
1458         * Settings this is required for efficient view recycling.
1459         */
1460        int mScrollingOffset;
1461
1462        /**
1463         * Used if you want to pre-layout items that are not yet visible.
1464         * The difference with {@link #mAvailable} is that, when recycling, distance laid out for
1465         * {@link #mExtra} is not considered to avoid recycling visible children.
1466         */
1467        int mExtra = 0;
1468
1469        /**
1470         * When LLM needs to layout particular views, it sets this list in which case, LayoutState
1471         * will only return views from this list and return null if it cannot find an item.
1472         */
1473        List<RecyclerView.ViewHolder> mScrapList = null;
1474
1475        /**
1476         * @return true if there are more items in the data adapter
1477         */
1478        boolean hasMore(RecyclerView.State state) {
1479            return mCurrentPosition >= 0 && mCurrentPosition < state.getItemCount();
1480        }
1481
1482        /**
1483         * Gets the view for the next element that we should layout.
1484         * Also updates current item index to the next item, based on {@link #mItemDirection}
1485         *
1486         * @return The next element that we should layout.
1487         */
1488        View next(RecyclerView.Recycler recycler) {
1489            if (mScrapList != null) {
1490                return nextFromLimitedList();
1491            }
1492            final View view = recycler.getViewForPosition(mCurrentPosition);
1493            mCurrentPosition += mItemDirection;
1494            return view;
1495        }
1496
1497        /**
1498         * Returns next item from limited list.
1499         * <p>
1500         * Upon finding a valid VH, sets current item position to VH.itemPosition + mItemDirection
1501         *
1502         * @return View if an item in the current position or direction exists if not null.
1503         */
1504        private View nextFromLimitedList() {
1505            int size = mScrapList.size();
1506            RecyclerView.ViewHolder closest = null;
1507            int closestDistance = Integer.MAX_VALUE;
1508            for (int i = 0; i < size; i++) {
1509                RecyclerView.ViewHolder viewHolder = mScrapList.get(i);
1510                final int distance = (viewHolder.getPosition() - mCurrentPosition) * mItemDirection;
1511                if (distance < 0) {
1512                    continue; // item is not in current direction
1513                }
1514                if (distance < closestDistance) {
1515                    closest = viewHolder;
1516                    closestDistance = distance;
1517                    if (distance == 0) {
1518                        break;
1519                    }
1520                }
1521            }
1522            if (DEBUG) {
1523                Log.d(TAG, "layout from scrap. found view:?" + (closest != null));
1524            }
1525            if (closest != null) {
1526                mCurrentPosition = closest.getPosition() + mItemDirection;
1527                return closest.itemView;
1528            }
1529            return null;
1530        }
1531
1532        void log() {
1533            Log.d(TAG, "avail:" + mAvailable + ", ind:" + mCurrentPosition + ", dir:" +
1534                    mItemDirection + ", offset:" + mOffset + ", layoutDir:" + mLayoutDirection);
1535        }
1536    }
1537
1538    static class SavedState implements Parcelable {
1539
1540        int mOrientation;
1541
1542        int mAnchorPosition;
1543
1544        int mAnchorOffset;
1545
1546        boolean mReverseLayout;
1547
1548        boolean mStackFromEnd;
1549
1550        boolean mAnchorLayoutFromEnd;
1551
1552        boolean mRecycleChildrenOnDetach;
1553
1554
1555        public SavedState() {
1556
1557        }
1558
1559        SavedState(Parcel in) {
1560            mOrientation = in.readInt();
1561            mAnchorPosition = in.readInt();
1562            mAnchorOffset = in.readInt();
1563            mReverseLayout = in.readInt() == 1;
1564            mStackFromEnd = in.readInt() == 1;
1565            mAnchorLayoutFromEnd = in.readInt() == 1;
1566            mRecycleChildrenOnDetach = in.readInt() == 1;
1567        }
1568
1569        public SavedState(SavedState other) {
1570            mOrientation = other.mOrientation;
1571            mAnchorPosition = other.mAnchorPosition;
1572            mAnchorOffset = other.mAnchorOffset;
1573            mReverseLayout = other.mReverseLayout;
1574            mStackFromEnd = other.mStackFromEnd;
1575            mAnchorLayoutFromEnd = other.mAnchorLayoutFromEnd;
1576            mRecycleChildrenOnDetach = other.mRecycleChildrenOnDetach;
1577        }
1578
1579        boolean hasValidAnchor() {
1580            return mAnchorPosition >= 0;
1581        }
1582
1583        void invalidateAnchor() {
1584            mAnchorPosition = RecyclerView.NO_POSITION;
1585        }
1586
1587        @Override
1588        public int describeContents() {
1589            return 0;
1590        }
1591
1592        @Override
1593        public void writeToParcel(Parcel dest, int flags) {
1594            dest.writeInt(mOrientation);
1595            dest.writeInt(mAnchorPosition);
1596            dest.writeInt(mAnchorOffset);
1597            dest.writeInt(mReverseLayout ? 1 : 0);
1598            dest.writeInt(mStackFromEnd ? 1 : 0);
1599            dest.writeInt(mAnchorLayoutFromEnd ? 1 : 0);
1600            dest.writeInt(mRecycleChildrenOnDetach ? 1 : 0);
1601        }
1602
1603        public static final Parcelable.Creator<SavedState> CREATOR
1604                = new Parcelable.Creator<SavedState>() {
1605            @Override
1606            public SavedState createFromParcel(Parcel in) {
1607                return new SavedState(in);
1608            }
1609
1610            @Override
1611            public SavedState[] newArray(int size) {
1612                return new SavedState[size];
1613            }
1614        };
1615    }
1616}
1617