GridLayoutManager.java revision 3a23ad69eef2b92be38f9704e064600066b7dfe9
1/*
2 * Copyright (C) 2014 The Android Open Source Project
3 *
4 * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except
5 * in compliance with the License. You may obtain a copy of the License at
6 *
7 * http://www.apache.org/licenses/LICENSE-2.0
8 *
9 * Unless required by applicable law or agreed to in writing, software distributed under the License
10 * is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express
11 * or implied. See the License for the specific language governing permissions and limitations under
12 * the License.
13 */
14package android.support.v17.leanback.widget;
15
16import android.content.Context;
17import android.graphics.PointF;
18import android.graphics.Rect;
19import android.os.Bundle;
20import android.os.Parcel;
21import android.os.Parcelable;
22import android.support.v4.util.CircularIntArray;
23import android.support.v4.view.ViewCompat;
24import android.support.v7.widget.LinearSmoothScroller;
25import android.support.v7.widget.RecyclerView;
26import android.support.v7.widget.RecyclerView.Recycler;
27import android.support.v7.widget.RecyclerView.State;
28import android.support.v17.leanback.os.TraceHelper;
29
30import static android.support.v7.widget.RecyclerView.NO_ID;
31import static android.support.v7.widget.RecyclerView.NO_POSITION;
32import static android.support.v7.widget.RecyclerView.HORIZONTAL;
33import static android.support.v7.widget.RecyclerView.VERTICAL;
34
35import android.util.AttributeSet;
36import android.util.Log;
37import android.view.FocusFinder;
38import android.view.Gravity;
39import android.view.View;
40import android.view.ViewParent;
41import android.view.View.MeasureSpec;
42import android.view.ViewGroup.MarginLayoutParams;
43import android.view.ViewGroup;
44
45import java.io.PrintWriter;
46import java.io.StringWriter;
47import java.util.ArrayList;
48import java.util.List;
49
50final class GridLayoutManager extends RecyclerView.LayoutManager {
51
52     /*
53      * LayoutParams for {@link HorizontalGridView} and {@link VerticalGridView}.
54      * The class currently does two internal jobs:
55      * - Saves optical bounds insets.
56      * - Caches focus align view center.
57      */
58    static class LayoutParams extends RecyclerView.LayoutParams {
59
60        // The view is saved only during animation.
61        private View mView;
62
63        // For placement
64        private int mLeftInset;
65        private int mTopInset;
66        private int mRightInset;
67        private int mBottomInset;
68
69        // For alignment
70        private int mAlignX;
71        private int mAlignY;
72
73        public LayoutParams(Context c, AttributeSet attrs) {
74            super(c, attrs);
75        }
76
77        public LayoutParams(int width, int height) {
78            super(width, height);
79        }
80
81        public LayoutParams(MarginLayoutParams source) {
82            super(source);
83        }
84
85        public LayoutParams(ViewGroup.LayoutParams source) {
86            super(source);
87        }
88
89        public LayoutParams(RecyclerView.LayoutParams source) {
90            super(source);
91        }
92
93        public LayoutParams(LayoutParams source) {
94            super(source);
95        }
96
97        int getAlignX() {
98            return mAlignX;
99        }
100
101        int getAlignY() {
102            return mAlignY;
103        }
104
105        int getOpticalLeft(View view) {
106            return view.getLeft() + mLeftInset;
107        }
108
109        int getOpticalTop(View view) {
110            return view.getTop() + mTopInset;
111        }
112
113        int getOpticalRight(View view) {
114            return view.getRight() - mRightInset;
115        }
116
117        int getOpticalBottom(View view) {
118            return view.getBottom() - mBottomInset;
119        }
120
121        int getOpticalWidth(View view) {
122            return view.getWidth() - mLeftInset - mRightInset;
123        }
124
125        int getOpticalHeight(View view) {
126            return view.getHeight() - mTopInset - mBottomInset;
127        }
128
129        int getOpticalLeftInset() {
130            return mLeftInset;
131        }
132
133        int getOpticalRightInset() {
134            return mRightInset;
135        }
136
137        int getOpticalTopInset() {
138            return mTopInset;
139        }
140
141        int getOpticalBottomInset() {
142            return mBottomInset;
143        }
144
145        void setAlignX(int alignX) {
146            mAlignX = alignX;
147        }
148
149        void setAlignY(int alignY) {
150            mAlignY = alignY;
151        }
152
153        void setOpticalInsets(int leftInset, int topInset, int rightInset, int bottomInset) {
154            mLeftInset = leftInset;
155            mTopInset = topInset;
156            mRightInset = rightInset;
157            mBottomInset = bottomInset;
158        }
159
160        private void invalidateItemDecoration() {
161            ViewParent parent = mView.getParent();
162            if (parent instanceof RecyclerView) {
163                // TODO: we only need invalidate parent if it has ItemDecoration
164                ((RecyclerView) parent).invalidate();
165            }
166        }
167    }
168
169    private static final String TAG = "GridLayoutManager";
170    private static final boolean DEBUG = false;
171    private static final boolean TRACE = false;
172
173    private String getTag() {
174        return TAG + ":" + mBaseGridView.getId();
175    }
176
177    private final BaseGridView mBaseGridView;
178
179    /**
180     * Note on conventions in the presence of RTL layout directions:
181     * Many properties and method names reference entities related to the
182     * beginnings and ends of things.  In the presence of RTL flows,
183     * it may not be clear whether this is intended to reference a
184     * quantity that changes direction in RTL cases, or a quantity that
185     * does not.  Here are the conventions in use:
186     *
187     * start/end: coordinate quantities - do reverse
188     * (optical) left/right: coordinate quantities - do not reverse
189     * low/high: coordinate quantities - do not reverse
190     * min/max: coordinate quantities - do not reverse
191     * scroll offset - coordinate quantities - do not reverse
192     * first/last: positional indices - do not reverse
193     * front/end: positional indices - do not reverse
194     * prepend/append: related to positional indices - do not reverse
195     *
196     * Note that although quantities do not reverse in RTL flows, their
197     * relationship does.  In LTR flows, the first positional index is
198     * leftmost; in RTL flows, it is rightmost.  Thus, anywhere that
199     * positional quantities are mapped onto coordinate quantities,
200     * the flow must be checked and the logic reversed.
201     */
202
203    /**
204     * The orientation of a "row".
205     */
206    private int mOrientation = HORIZONTAL;
207
208    private RecyclerView.State mState;
209    private RecyclerView.Recycler mRecycler;
210
211    private boolean mInLayout = false;
212    private boolean mInSelection = false;
213
214    private OnChildSelectedListener mChildSelectedListener = null;
215
216    /**
217     * The focused position, it's not the currently visually aligned position
218     * but it is the final position that we intend to focus on. If there are
219     * multiple setSelection() called, mFocusPosition saves last value.
220     */
221    private int mFocusPosition = NO_POSITION;
222
223    /**
224     * The offset to be applied to mFocusPosition, due to adapter change, on the next
225     * layout.  Set to Integer.MIN_VALUE means item was removed.
226     * TODO:  This is somewhat duplication of RecyclerView getOldPosition() which is
227     * unfortunately cleared after prelayout.
228     */
229    private int mFocusPositionOffset = 0;
230
231    /**
232     * Force a full layout under certain situations.  E.g. Rows change, jump to invisible child.
233     */
234    private boolean mForceFullLayout;
235
236    /**
237     * True if layout is enabled.
238     */
239    private boolean mLayoutEnabled = true;
240
241    /**
242     * override child visibility
243     */
244    private int mChildVisibility = -1;
245
246    /**
247     * The scroll offsets of the viewport relative to the entire view.
248     */
249    private int mScrollOffsetPrimary;
250    private int mScrollOffsetSecondary;
251
252    /**
253     * User-specified row height/column width.  Can be WRAP_CONTENT.
254     */
255    private int mRowSizeSecondaryRequested;
256
257    /**
258     * The fixed size of each grid item in the secondary direction. This corresponds to
259     * the row height, equal for all rows. Grid items may have variable length
260     * in the primary direction.
261     */
262    private int mFixedRowSizeSecondary;
263
264    /**
265     * Tracks the secondary size of each row.
266     */
267    private int[] mRowSizeSecondary;
268
269    /**
270     * Flag controlling whether the current/next layout should
271     * be updating the secondary size of rows.
272     */
273    private boolean mRowSecondarySizeRefresh;
274
275    /**
276     * The maximum measured size of the view.
277     */
278    private int mMaxSizeSecondary;
279
280    /**
281     * Margin between items.
282     */
283    private int mHorizontalMargin;
284    /**
285     * Margin between items vertically.
286     */
287    private int mVerticalMargin;
288    /**
289     * Margin in main direction.
290     */
291    private int mMarginPrimary;
292    /**
293     * Margin in second direction.
294     */
295    private int mMarginSecondary;
296    /**
297     * How to position child in secondary direction.
298     */
299    private int mGravity = Gravity.START | Gravity.TOP;
300    /**
301     * The number of rows in the grid.
302     */
303    private int mNumRows;
304    /**
305     * Number of rows requested, can be 0 to be determined by parent size and
306     * rowHeight.
307     */
308    private int mNumRowsRequested = 1;
309
310    /**
311     * Saves grid information of each view.
312     */
313    private Grid mGrid;
314
315    /**
316     * Focus Scroll strategy.
317     */
318    private int mFocusScrollStrategy = BaseGridView.FOCUS_SCROLL_ALIGNED;
319    /**
320     * Defines how item view is aligned in the window.
321     */
322    private final WindowAlignment mWindowAlignment = new WindowAlignment();
323
324    /**
325     * Defines how item view is aligned.
326     */
327    private final ItemAlignment mItemAlignment = new ItemAlignment();
328
329    /**
330     * Dimensions of the view, width or height depending on orientation.
331     */
332    private int mSizePrimary;
333
334    /**
335     *  Allow DPAD key to navigate out at the front of the View (where position = 0),
336     *  default is false.
337     */
338    private boolean mFocusOutFront;
339
340    /**
341     * Allow DPAD key to navigate out at the end of the view, default is false.
342     */
343    private boolean mFocusOutEnd;
344
345    /**
346     * True if focus search is disabled.
347     */
348    private boolean mFocusSearchDisabled;
349
350    /**
351     * True if prune child,  might be disabled during transition.
352     */
353    private boolean mPruneChild = true;
354
355    /**
356     * True if scroll content,  might be disabled during transition.
357     */
358    private boolean mScrollEnabled = true;
359
360    /**
361     * Temporary variable: an int array of length=2.
362     */
363    private static int[] sTwoInts = new int[2];
364
365    /**
366     * Set to true for RTL layout in horizontal orientation
367     */
368    private boolean mReverseFlowPrimary = false;
369
370    /**
371     * Set to true for RTL layout in vertical orientation
372     */
373    private boolean mReverseFlowSecondary = false;
374
375    /**
376     * Temporaries used for measuring.
377     */
378    private int[] mMeasuredDimension = new int[2];
379
380    final ViewsStateBundle mChildrenStates = new ViewsStateBundle();
381
382    public GridLayoutManager(BaseGridView baseGridView) {
383        mBaseGridView = baseGridView;
384    }
385
386    public void setOrientation(int orientation) {
387        if (orientation != HORIZONTAL && orientation != VERTICAL) {
388            if (DEBUG) Log.v(getTag(), "invalid orientation: " + orientation);
389            return;
390        }
391
392        mOrientation = orientation;
393        mWindowAlignment.setOrientation(orientation);
394        mItemAlignment.setOrientation(orientation);
395        mForceFullLayout = true;
396    }
397
398    public void onRtlPropertiesChanged(int layoutDirection) {
399        if (mOrientation == HORIZONTAL) {
400            mReverseFlowPrimary = layoutDirection == View.LAYOUT_DIRECTION_RTL;
401            mReverseFlowSecondary = false;
402        } else {
403            mReverseFlowSecondary = layoutDirection == View.LAYOUT_DIRECTION_RTL;
404            mReverseFlowPrimary = false;
405        }
406        mWindowAlignment.horizontal.setReversedFlow(layoutDirection == View.LAYOUT_DIRECTION_RTL);
407    }
408
409    public int getFocusScrollStrategy() {
410        return mFocusScrollStrategy;
411    }
412
413    public void setFocusScrollStrategy(int focusScrollStrategy) {
414        mFocusScrollStrategy = focusScrollStrategy;
415    }
416
417    public void setWindowAlignment(int windowAlignment) {
418        mWindowAlignment.mainAxis().setWindowAlignment(windowAlignment);
419    }
420
421    public int getWindowAlignment() {
422        return mWindowAlignment.mainAxis().getWindowAlignment();
423    }
424
425    public void setWindowAlignmentOffset(int alignmentOffset) {
426        mWindowAlignment.mainAxis().setWindowAlignmentOffset(alignmentOffset);
427    }
428
429    public int getWindowAlignmentOffset() {
430        return mWindowAlignment.mainAxis().getWindowAlignmentOffset();
431    }
432
433    public void setWindowAlignmentOffsetPercent(float offsetPercent) {
434        mWindowAlignment.mainAxis().setWindowAlignmentOffsetPercent(offsetPercent);
435    }
436
437    public float getWindowAlignmentOffsetPercent() {
438        return mWindowAlignment.mainAxis().getWindowAlignmentOffsetPercent();
439    }
440
441    public void setItemAlignmentOffset(int alignmentOffset) {
442        mItemAlignment.mainAxis().setItemAlignmentOffset(alignmentOffset);
443        updateChildAlignments();
444    }
445
446    public int getItemAlignmentOffset() {
447        return mItemAlignment.mainAxis().getItemAlignmentOffset();
448    }
449
450    public void setItemAlignmentOffsetWithPadding(boolean withPadding) {
451        mItemAlignment.mainAxis().setItemAlignmentOffsetWithPadding(withPadding);
452        updateChildAlignments();
453    }
454
455    public boolean isItemAlignmentOffsetWithPadding() {
456        return mItemAlignment.mainAxis().isItemAlignmentOffsetWithPadding();
457    }
458
459    public void setItemAlignmentOffsetPercent(float offsetPercent) {
460        mItemAlignment.mainAxis().setItemAlignmentOffsetPercent(offsetPercent);
461        updateChildAlignments();
462    }
463
464    public float getItemAlignmentOffsetPercent() {
465        return mItemAlignment.mainAxis().getItemAlignmentOffsetPercent();
466    }
467
468    public void setItemAlignmentViewId(int viewId) {
469        mItemAlignment.mainAxis().setItemAlignmentViewId(viewId);
470        updateChildAlignments();
471    }
472
473    public int getItemAlignmentViewId() {
474        return mItemAlignment.mainAxis().getItemAlignmentViewId();
475    }
476
477    public void setFocusOutAllowed(boolean throughFront, boolean throughEnd) {
478        mFocusOutFront = throughFront;
479        mFocusOutEnd = throughEnd;
480    }
481
482    public void setNumRows(int numRows) {
483        if (numRows < 0) throw new IllegalArgumentException();
484        mNumRowsRequested = numRows;
485        mForceFullLayout = true;
486    }
487
488    /**
489     * Set the row height. May be WRAP_CONTENT, or a size in pixels.
490     */
491    public void setRowHeight(int height) {
492        if (height >= 0 || height == ViewGroup.LayoutParams.WRAP_CONTENT) {
493            mRowSizeSecondaryRequested = height;
494        } else {
495            throw new IllegalArgumentException("Invalid row height: " + height);
496        }
497    }
498
499    public void setItemMargin(int margin) {
500        mVerticalMargin = mHorizontalMargin = margin;
501        mMarginPrimary = mMarginSecondary = margin;
502    }
503
504    public void setVerticalMargin(int margin) {
505        if (mOrientation == HORIZONTAL) {
506            mMarginSecondary = mVerticalMargin = margin;
507        } else {
508            mMarginPrimary = mVerticalMargin = margin;
509        }
510    }
511
512    public void setHorizontalMargin(int margin) {
513        if (mOrientation == HORIZONTAL) {
514            mMarginPrimary = mHorizontalMargin = margin;
515        } else {
516            mMarginSecondary = mHorizontalMargin = margin;
517        }
518    }
519
520    public int getVerticalMargin() {
521        return mVerticalMargin;
522    }
523
524    public int getHorizontalMargin() {
525        return mHorizontalMargin;
526    }
527
528    public void setGravity(int gravity) {
529        mGravity = gravity;
530    }
531
532    protected boolean hasDoneFirstLayout() {
533        return mGrid != null;
534    }
535
536    public void setOnChildSelectedListener(OnChildSelectedListener listener) {
537        mChildSelectedListener = listener;
538    }
539
540    private int getPositionByView(View view) {
541        if (view == null) {
542            return NO_POSITION;
543        }
544        LayoutParams params = (LayoutParams) view.getLayoutParams();
545        if (params == null || params.isItemRemoved()) {
546            // when item is removed, the position value can be any value.
547            return NO_POSITION;
548        }
549        return params.getViewPosition();
550    }
551
552    private int getPositionByIndex(int index) {
553        return getPositionByView(getChildAt(index));
554    }
555
556    private void dispatchChildSelected() {
557        if (mChildSelectedListener == null) {
558            return;
559        }
560
561        if (TRACE) TraceHelper.beginSection("onChildSelected");
562        View view = mFocusPosition == NO_POSITION ? null : findViewByPosition(mFocusPosition);
563        if (view != null) {
564            RecyclerView.ViewHolder vh = mBaseGridView.getChildViewHolder(view);
565            mChildSelectedListener.onChildSelected(mBaseGridView, view, mFocusPosition,
566                    vh == null? NO_ID: vh.getItemId());
567        } else {
568            mChildSelectedListener.onChildSelected(mBaseGridView, null, NO_POSITION, NO_ID);
569        }
570        if (TRACE) TraceHelper.endSection();
571
572        // Children may request layout when a child selection event occurs (such as a change of
573        // padding on the current and previously selected rows).
574        // If in layout, a child requesting layout may have been laid out before the selection
575        // callback.
576        // If it was not, the child will be laid out after the selection callback.
577        // If so, the layout request will be honoured though the view system will emit a double-
578        // layout warning.
579        // If not in layout, we may be scrolling in which case the child layout request will be
580        // eaten by recyclerview.  Post a requestLayout.
581        if (!mInLayout && !mBaseGridView.isLayoutRequested()) {
582            int childCount = getChildCount();
583            for (int i = 0; i < childCount; i++) {
584                if (getChildAt(i).isLayoutRequested()) {
585                    forceRequestLayout();
586                    break;
587                }
588            }
589        }
590    }
591
592    @Override
593    public boolean canScrollHorizontally() {
594        // We can scroll horizontally if we have horizontal orientation, or if
595        // we are vertical and have more than one column.
596        return mOrientation == HORIZONTAL || mNumRows > 1;
597    }
598
599    @Override
600    public boolean canScrollVertically() {
601        // We can scroll vertically if we have vertical orientation, or if we
602        // are horizontal and have more than one row.
603        return mOrientation == VERTICAL || mNumRows > 1;
604    }
605
606    @Override
607    public RecyclerView.LayoutParams generateDefaultLayoutParams() {
608        return new LayoutParams(ViewGroup.LayoutParams.WRAP_CONTENT,
609                ViewGroup.LayoutParams.WRAP_CONTENT);
610    }
611
612    @Override
613    public RecyclerView.LayoutParams generateLayoutParams(Context context, AttributeSet attrs) {
614        return new LayoutParams(context, attrs);
615    }
616
617    @Override
618    public RecyclerView.LayoutParams generateLayoutParams(ViewGroup.LayoutParams lp) {
619        if (lp instanceof LayoutParams) {
620            return new LayoutParams((LayoutParams) lp);
621        } else if (lp instanceof RecyclerView.LayoutParams) {
622            return new LayoutParams((RecyclerView.LayoutParams) lp);
623        } else if (lp instanceof MarginLayoutParams) {
624            return new LayoutParams((MarginLayoutParams) lp);
625        } else {
626            return new LayoutParams(lp);
627        }
628    }
629
630    protected View getViewForPosition(int position) {
631        return mRecycler.getViewForPosition(position);
632    }
633
634    final int getOpticalLeft(View v) {
635        return ((LayoutParams) v.getLayoutParams()).getOpticalLeft(v);
636    }
637
638    final int getOpticalRight(View v) {
639        return ((LayoutParams) v.getLayoutParams()).getOpticalRight(v);
640    }
641
642    final int getOpticalTop(View v) {
643        return ((LayoutParams) v.getLayoutParams()).getOpticalTop(v);
644    }
645
646    final int getOpticalBottom(View v) {
647        return ((LayoutParams) v.getLayoutParams()).getOpticalBottom(v);
648    }
649
650    private int getViewMin(View v) {
651        return (mOrientation == HORIZONTAL) ? getOpticalLeft(v) : getOpticalTop(v);
652    }
653
654    private int getViewMax(View v) {
655        return (mOrientation == HORIZONTAL) ? getOpticalRight(v) : getOpticalBottom(v);
656    }
657
658    private int getViewCenter(View view) {
659        return (mOrientation == HORIZONTAL) ? getViewCenterX(view) : getViewCenterY(view);
660    }
661
662    private int getViewCenterSecondary(View view) {
663        return (mOrientation == HORIZONTAL) ? getViewCenterY(view) : getViewCenterX(view);
664    }
665
666    private int getViewCenterX(View v) {
667        LayoutParams p = (LayoutParams) v.getLayoutParams();
668        return p.getOpticalLeft(v) + p.getAlignX();
669    }
670
671    private int getViewCenterY(View v) {
672        LayoutParams p = (LayoutParams) v.getLayoutParams();
673        return p.getOpticalTop(v) + p.getAlignY();
674    }
675
676    /**
677     * Save Recycler and State for convenience.  Must be paired with leaveContext().
678     */
679    private void saveContext(Recycler recycler, State state) {
680        if (mRecycler != null || mState != null) {
681            Log.e(TAG, "Recycler information was not released, bug!");
682        }
683        mRecycler = recycler;
684        mState = state;
685    }
686
687    /**
688     * Discard saved Recycler and State.
689     */
690    private void leaveContext() {
691        mRecycler = null;
692        mState = null;
693    }
694
695    /**
696     * Re-initialize data structures for a data change or handling invisible
697     * selection. The method tries its best to preserve position information so
698     * that staggered grid looks same before and after re-initialize.
699     * @return true if can fastRelayout()
700     */
701    private boolean layoutInit() {
702        if (!mState.didStructureChange() && !mForceFullLayout && mGrid != null) {
703            updateScrollController();
704            updateScrollSecondAxis();
705            mGrid.setMargin(mMarginPrimary);
706            return true;
707        } else {
708            mForceFullLayout = false;
709            boolean focusViewWasInTree = mGrid != null && mFocusPosition >= 0
710                    && mFocusPosition >= mGrid.getFirstVisibleIndex()
711                    && mFocusPosition <= mGrid.getLastVisibleIndex();
712            int firstVisibleIndex = focusViewWasInTree ? mGrid.getFirstVisibleIndex() : 0;
713            final int newItemCount = mState.getItemCount();
714            if (newItemCount == 0) {
715                mFocusPosition = NO_POSITION;
716            } else if (mFocusPosition >= newItemCount) {
717                mFocusPosition = newItemCount - 1;
718            } else if (mFocusPosition == NO_POSITION && newItemCount > 0) {
719                // if focus position is never set before,  initialize it to 0
720                mFocusPosition = 0;
721            }
722
723            if (mGrid == null || mNumRows != mGrid.getNumRows() ||
724                    mReverseFlowPrimary != mGrid.isReversedFlow()) {
725                mGrid = Grid.createStaggeredMultipleRows(mNumRows);
726                mGrid.setProvider(mGridProvider);
727                mGrid.setReversedFlow(mReverseFlowPrimary);
728            }
729            initScrollController();
730            updateScrollSecondAxis();
731            mGrid.setMargin(mMarginPrimary);
732            detachAndScrapAttachedViews(mRecycler);
733            mGrid.resetVisibleIndex();
734            if (mFocusPosition == NO_POSITION) {
735                mBaseGridView.clearFocus();
736            }
737            mWindowAlignment.mainAxis().invalidateScrollMin();
738            mWindowAlignment.mainAxis().invalidateScrollMax();
739            if (focusViewWasInTree && firstVisibleIndex <= mFocusPosition) {
740                // if focusView was in tree, we will add item from first visible item
741                mGrid.setStart(firstVisibleIndex);
742            } else {
743                // if focusView was not in tree, it's probably because focus position jumped
744                // far away from visible range,  so use mFocusPosition as start
745                mGrid.setStart(mFocusPosition);
746            }
747            return false;
748        }
749    }
750
751    private int getRowSizeSecondary(int rowIndex) {
752        if (mFixedRowSizeSecondary != 0) {
753            return mFixedRowSizeSecondary;
754        }
755        if (mRowSizeSecondary == null) {
756            return 0;
757        }
758        return mRowSizeSecondary[rowIndex];
759    }
760
761    private int getRowStartSecondary(int rowIndex) {
762        int start = 0;
763        // Iterate from left to right, which is a different index traversal
764        // in RTL flow
765        if (mReverseFlowSecondary) {
766            for (int i = mNumRows-1; i > rowIndex; i--) {
767                start += getRowSizeSecondary(i) + mMarginSecondary;
768            }
769        } else {
770            for (int i = 0; i < rowIndex; i++) {
771                start += getRowSizeSecondary(i) + mMarginSecondary;
772            }
773        }
774        return start;
775    }
776
777    private int getSizeSecondary() {
778        int rightmostIndex = mReverseFlowSecondary ? 0 : mNumRows - 1;
779        return getRowStartSecondary(rightmostIndex) + getRowSizeSecondary(rightmostIndex);
780    }
781
782    private void measureScrapChild(int position, int widthSpec, int heightSpec,
783            int[] measuredDimension) {
784        View view = mRecycler.getViewForPosition(position);
785        if (view != null) {
786            LayoutParams p = (LayoutParams) view.getLayoutParams();
787            int childWidthSpec = ViewGroup.getChildMeasureSpec(widthSpec,
788                    getPaddingLeft() + getPaddingRight(), p.width);
789            int childHeightSpec = ViewGroup.getChildMeasureSpec(heightSpec,
790                    getPaddingTop() + getPaddingBottom(), p.height);
791            view.measure(childWidthSpec, childHeightSpec);
792            measuredDimension[0] = view.getMeasuredWidth();
793            measuredDimension[1] = view.getMeasuredHeight();
794            mRecycler.recycleView(view);
795        }
796    }
797
798    private boolean processRowSizeSecondary(boolean measure) {
799        if (mFixedRowSizeSecondary != 0) {
800            return false;
801        }
802
803        if (TRACE) TraceHelper.beginSection("processRowSizeSecondary");
804        CircularIntArray[] rows = mGrid == null ? null : mGrid.getItemPositionsInRows();
805        boolean changed = false;
806        int scrapChildWidth = -1;
807        int scrapChildHeight = -1;
808
809        for (int rowIndex = 0; rowIndex < mNumRows; rowIndex++) {
810            CircularIntArray row = rows == null ? null : rows[rowIndex];
811            final int rowItemsPairCount = row == null ? 0 : row.size();
812            int rowSize = -1;
813            for (int rowItemPairIndex = 0; rowItemPairIndex < rowItemsPairCount;
814                    rowItemPairIndex += 2) {
815                final int rowIndexStart = row.get(rowItemPairIndex);
816                final int rowIndexEnd = row.get(rowItemPairIndex + 1);
817                for (int i = rowIndexStart; i <= rowIndexEnd; i++) {
818                    final View view = findViewByPosition(i);
819                    if (view == null) {
820                        continue;
821                    }
822                    if (measure && view.isLayoutRequested()) {
823                        measureChild(view);
824                    }
825                    final int secondarySize = mOrientation == HORIZONTAL ?
826                            view.getMeasuredHeight() : view.getMeasuredWidth();
827                    if (secondarySize > rowSize) {
828                        rowSize = secondarySize;
829                    }
830                }
831            }
832
833            final int itemCount = mState.getItemCount();
834            if (measure && rowSize < 0 && itemCount > 0) {
835                if (scrapChildWidth < 0 && scrapChildHeight < 0) {
836                    int position;
837                    if (mFocusPosition == NO_POSITION) {
838                        position = 0;
839                    } else if (mFocusPosition >= itemCount) {
840                        position = itemCount - 1;
841                    } else {
842                        position = mFocusPosition;
843                    }
844                    measureScrapChild(position,
845                            MeasureSpec.makeMeasureSpec(0, MeasureSpec.UNSPECIFIED),
846                            MeasureSpec.makeMeasureSpec(0, MeasureSpec.UNSPECIFIED),
847                            mMeasuredDimension);
848                    scrapChildWidth = mMeasuredDimension[0];
849                    scrapChildHeight = mMeasuredDimension[1];
850                    if (DEBUG) Log.v(TAG, "measured scrap child: " + scrapChildWidth +
851                            " " + scrapChildHeight);
852                }
853                rowSize = mOrientation == HORIZONTAL ? scrapChildHeight : scrapChildWidth;
854            }
855            if (rowSize < 0) {
856                rowSize = 0;
857            }
858            if (mRowSizeSecondary[rowIndex] != rowSize) {
859                if (DEBUG) Log.v(getTag(), "row size secondary changed: " + mRowSizeSecondary[rowIndex] +
860                        ", " + rowSize);
861                mRowSizeSecondary[rowIndex] = rowSize;
862                changed = true;
863            }
864        }
865
866        if (TRACE) TraceHelper.endSection();
867        return changed;
868    }
869
870    /**
871     * Checks if we need to update row secondary sizes.
872     */
873    private void updateRowSecondarySizeRefresh() {
874        mRowSecondarySizeRefresh = processRowSizeSecondary(false);
875        if (mRowSecondarySizeRefresh) {
876            if (DEBUG) Log.v(getTag(), "mRowSecondarySizeRefresh now set");
877            forceRequestLayout();
878        }
879    }
880
881    private void forceRequestLayout() {
882        if (DEBUG) Log.v(getTag(), "forceRequestLayout");
883        // RecyclerView prevents us from requesting layout in many cases
884        // (during layout, during scroll, etc.)
885        // For secondary row size wrap_content support we currently need a
886        // second layout pass to update the measured size after having measured
887        // and added child views in layoutChildren.
888        // Force the second layout by posting a delayed runnable.
889        // TODO: investigate allowing a second layout pass,
890        // or move child add/measure logic to the measure phase.
891        ViewCompat.postOnAnimation(mBaseGridView, mRequestLayoutRunnable);
892    }
893
894    private final Runnable mRequestLayoutRunnable = new Runnable() {
895        @Override
896        public void run() {
897            if (DEBUG) Log.v(getTag(), "request Layout from runnable");
898            requestLayout();
899        }
900     };
901
902    @Override
903    public void onMeasure(Recycler recycler, State state, int widthSpec, int heightSpec) {
904        saveContext(recycler, state);
905
906        int sizePrimary, sizeSecondary, modeSecondary, paddingSecondary;
907        int measuredSizeSecondary;
908        if (mOrientation == HORIZONTAL) {
909            sizePrimary = MeasureSpec.getSize(widthSpec);
910            sizeSecondary = MeasureSpec.getSize(heightSpec);
911            modeSecondary = MeasureSpec.getMode(heightSpec);
912            paddingSecondary = getPaddingTop() + getPaddingBottom();
913        } else {
914            sizeSecondary = MeasureSpec.getSize(widthSpec);
915            sizePrimary = MeasureSpec.getSize(heightSpec);
916            modeSecondary = MeasureSpec.getMode(widthSpec);
917            paddingSecondary = getPaddingLeft() + getPaddingRight();
918        }
919        if (DEBUG) Log.v(getTag(), "onMeasure widthSpec " + Integer.toHexString(widthSpec) +
920                " heightSpec " + Integer.toHexString(heightSpec) +
921                " modeSecondary " + Integer.toHexString(modeSecondary) +
922                " sizeSecondary " + sizeSecondary + " " + this);
923
924        mMaxSizeSecondary = sizeSecondary;
925
926        if (mRowSizeSecondaryRequested == ViewGroup.LayoutParams.WRAP_CONTENT) {
927            mNumRows = mNumRowsRequested == 0 ? 1 : mNumRowsRequested;
928            mFixedRowSizeSecondary = 0;
929
930            if (mRowSizeSecondary == null || mRowSizeSecondary.length != mNumRows) {
931                mRowSizeSecondary = new int[mNumRows];
932            }
933
934            // Measure all current children and update cached row heights
935            processRowSizeSecondary(true);
936
937            switch (modeSecondary) {
938            case MeasureSpec.UNSPECIFIED:
939                measuredSizeSecondary = getSizeSecondary() + paddingSecondary;
940                break;
941            case MeasureSpec.AT_MOST:
942                measuredSizeSecondary = Math.min(getSizeSecondary() + paddingSecondary,
943                        mMaxSizeSecondary);
944                break;
945            case MeasureSpec.EXACTLY:
946                measuredSizeSecondary = mMaxSizeSecondary;
947                break;
948            default:
949                throw new IllegalStateException("wrong spec");
950            }
951
952        } else {
953            switch (modeSecondary) {
954            case MeasureSpec.UNSPECIFIED:
955                if (mRowSizeSecondaryRequested == 0) {
956                    if (mOrientation == HORIZONTAL) {
957                        throw new IllegalStateException("Must specify rowHeight or view height");
958                    } else {
959                        throw new IllegalStateException("Must specify columnWidth or view width");
960                    }
961                }
962                mFixedRowSizeSecondary = mRowSizeSecondaryRequested;
963                mNumRows = mNumRowsRequested == 0 ? 1 : mNumRowsRequested;
964                measuredSizeSecondary = mFixedRowSizeSecondary * mNumRows + mMarginSecondary
965                    * (mNumRows - 1) + paddingSecondary;
966                break;
967            case MeasureSpec.AT_MOST:
968            case MeasureSpec.EXACTLY:
969                if (mNumRowsRequested == 0 && mRowSizeSecondaryRequested == 0) {
970                    mNumRows = 1;
971                    mFixedRowSizeSecondary = sizeSecondary - paddingSecondary;
972                } else if (mNumRowsRequested == 0) {
973                    mFixedRowSizeSecondary = mRowSizeSecondaryRequested;
974                    mNumRows = (sizeSecondary + mMarginSecondary)
975                        / (mRowSizeSecondaryRequested + mMarginSecondary);
976                } else if (mRowSizeSecondaryRequested == 0) {
977                    mNumRows = mNumRowsRequested;
978                    mFixedRowSizeSecondary = (sizeSecondary - paddingSecondary - mMarginSecondary
979                            * (mNumRows - 1)) / mNumRows;
980                } else {
981                    mNumRows = mNumRowsRequested;
982                    mFixedRowSizeSecondary = mRowSizeSecondaryRequested;
983                }
984                measuredSizeSecondary = sizeSecondary;
985                if (modeSecondary == MeasureSpec.AT_MOST) {
986                    int childrenSize = mFixedRowSizeSecondary * mNumRows + mMarginSecondary
987                        * (mNumRows - 1) + paddingSecondary;
988                    if (childrenSize < measuredSizeSecondary) {
989                        measuredSizeSecondary = childrenSize;
990                    }
991                }
992                break;
993            default:
994                throw new IllegalStateException("wrong spec");
995            }
996        }
997        if (mOrientation == HORIZONTAL) {
998            setMeasuredDimension(sizePrimary, measuredSizeSecondary);
999        } else {
1000            setMeasuredDimension(measuredSizeSecondary, sizePrimary);
1001        }
1002        if (DEBUG) {
1003            Log.v(getTag(), "onMeasure sizePrimary " + sizePrimary +
1004                    " measuredSizeSecondary " + measuredSizeSecondary +
1005                    " mFixedRowSizeSecondary " + mFixedRowSizeSecondary +
1006                    " mNumRows " + mNumRows);
1007        }
1008        leaveContext();
1009    }
1010
1011    private void measureChild(View child) {
1012        if (TRACE) TraceHelper.beginSection("measureChild");
1013        final ViewGroup.LayoutParams lp = child.getLayoutParams();
1014        final int secondarySpec = (mRowSizeSecondaryRequested == ViewGroup.LayoutParams.WRAP_CONTENT) ?
1015                MeasureSpec.makeMeasureSpec(0, MeasureSpec.UNSPECIFIED) :
1016                MeasureSpec.makeMeasureSpec(mFixedRowSizeSecondary, MeasureSpec.EXACTLY);
1017        int widthSpec, heightSpec;
1018
1019        if (mOrientation == HORIZONTAL) {
1020            widthSpec = ViewGroup.getChildMeasureSpec(
1021                    MeasureSpec.makeMeasureSpec(0, MeasureSpec.UNSPECIFIED),
1022                    0, lp.width);
1023            heightSpec = ViewGroup.getChildMeasureSpec(secondarySpec, 0, lp.height);
1024        } else {
1025            heightSpec = ViewGroup.getChildMeasureSpec(
1026                    MeasureSpec.makeMeasureSpec(0, MeasureSpec.UNSPECIFIED),
1027                    0, lp.height);
1028            widthSpec = ViewGroup.getChildMeasureSpec(secondarySpec, 0, lp.width);
1029        }
1030        child.measure(widthSpec, heightSpec);
1031        if (DEBUG) Log.v(getTag(), "measureChild secondarySpec " + Integer.toHexString(secondarySpec) +
1032                " widthSpec " + Integer.toHexString(widthSpec) +
1033                " heightSpec " + Integer.toHexString(heightSpec) +
1034                " measuredWidth " + child.getMeasuredWidth() +
1035                " measuredHeight " + child.getMeasuredHeight());
1036        if (DEBUG) Log.v(getTag(), "child lp width " + lp.width + " height " + lp.height);
1037        if (TRACE) TraceHelper.endSection();
1038    }
1039
1040    private Grid.Provider mGridProvider = new Grid.Provider() {
1041
1042        @Override
1043        public int getCount() {
1044            return mState.getItemCount();
1045        }
1046
1047        @Override
1048        public int createItem(int index, boolean append, Object[] item) {
1049            if (TRACE) TraceHelper.beginSection("createItem");
1050            if (TRACE) TraceHelper.beginSection("getview");
1051            View v = getViewForPosition(index);
1052            if (TRACE) TraceHelper.endSection();
1053            // See recyclerView docs:  we don't need re-add scraped view if it was removed.
1054            if (!((RecyclerView.LayoutParams) v.getLayoutParams()).isItemRemoved()) {
1055                if (TRACE) TraceHelper.beginSection("addView");
1056                if (append) {
1057                    addView(v);
1058                } else {
1059                    addView(v, 0);
1060                }
1061                if (TRACE) TraceHelper.endSection();
1062                if (mChildVisibility != -1) {
1063                    v.setVisibility(mChildVisibility);
1064                }
1065
1066                // View is added first or it won't be found by dispatchChildSelected.
1067                if (mInLayout && index == mFocusPosition) {
1068                    dispatchChildSelected();
1069                }
1070                measureChild(v);
1071            }
1072            item[0] = v;
1073            return mOrientation == HORIZONTAL ? v.getMeasuredWidth() : v.getMeasuredHeight();
1074        }
1075
1076        @Override
1077        public void addItem(Object item, int index, int length, int rowIndex, int edge) {
1078            View v = (View) item;
1079            int start, end;
1080            if (edge == Integer.MIN_VALUE || edge == Integer.MAX_VALUE) {
1081                edge = !mGrid.isReversedFlow() ? mWindowAlignment.mainAxis().getPaddingLow()
1082                        : mWindowAlignment.mainAxis().getSize()
1083                                - mWindowAlignment.mainAxis().getPaddingHigh();
1084            }
1085            boolean edgeIsMin = !mGrid.isReversedFlow();
1086            if (edgeIsMin) {
1087                start = edge;
1088                end = edge + length;
1089            } else {
1090                start = edge - length;
1091                end = edge;
1092            }
1093            int startSecondary = getRowStartSecondary(rowIndex) - mScrollOffsetSecondary;
1094            mChildrenStates.loadView(v, index);
1095            layoutChild(rowIndex, v, start, end, startSecondary);
1096            if (DEBUG) {
1097                Log.d(getTag(), "addView " + index + " " + v);
1098            }
1099            if (TRACE) TraceHelper.endSection();
1100
1101            if (index == mGrid.getFirstVisibleIndex()) {
1102                if (!mGrid.isReversedFlow()) {
1103                    updateScrollMin();
1104                } else {
1105                    updateScrollMax();
1106                }
1107            }
1108            if (index == mGrid.getLastVisibleIndex()) {
1109                if (!mGrid.isReversedFlow()) {
1110                    updateScrollMax();
1111                } else {
1112                    updateScrollMin();
1113                }
1114            }
1115        }
1116
1117        @Override
1118        public void removeItem(int index) {
1119            if (TRACE) TraceHelper.beginSection("removeItem");
1120            View v = findViewByPosition(index);
1121            if (mInLayout) {
1122                detachAndScrapView(v, mRecycler);
1123            } else {
1124                removeAndRecycleView(v, mRecycler);
1125            }
1126            if (TRACE) TraceHelper.endSection();
1127        }
1128
1129        @Override
1130        public int getEdge(int index) {
1131            if (mReverseFlowPrimary) {
1132                return getViewMax(findViewByPosition(index));
1133            } else {
1134                return getViewMin(findViewByPosition(index));
1135            }
1136        }
1137
1138        @Override
1139        public int getSize(int index) {
1140            final View v = findViewByPosition(index);
1141            return mOrientation == HORIZONTAL ? v.getMeasuredWidth() : v.getMeasuredHeight();
1142        }
1143    };
1144
1145    private void layoutChild(int rowIndex, View v, int start, int end, int startSecondary) {
1146        if (TRACE) TraceHelper.beginSection("layoutChild");
1147        int sizeSecondary = mOrientation == HORIZONTAL ? v.getMeasuredHeight()
1148                : v.getMeasuredWidth();
1149        if (mFixedRowSizeSecondary > 0) {
1150            sizeSecondary = Math.min(sizeSecondary, mFixedRowSizeSecondary);
1151        }
1152        final int verticalGravity = mGravity & Gravity.VERTICAL_GRAVITY_MASK;
1153        final int horizontalGravity = (mReverseFlowPrimary || mReverseFlowSecondary) ?
1154                Gravity.getAbsoluteGravity(mGravity & Gravity.RELATIVE_HORIZONTAL_GRAVITY_MASK, View.LAYOUT_DIRECTION_RTL) :
1155                mGravity & Gravity.HORIZONTAL_GRAVITY_MASK;
1156        if (mOrientation == HORIZONTAL && verticalGravity == Gravity.TOP
1157                || mOrientation == VERTICAL && horizontalGravity == Gravity.LEFT) {
1158            // do nothing
1159        } else if (mOrientation == HORIZONTAL && verticalGravity == Gravity.BOTTOM
1160                || mOrientation == VERTICAL && horizontalGravity == Gravity.RIGHT) {
1161            startSecondary += getRowSizeSecondary(rowIndex) - sizeSecondary;
1162        } else if (mOrientation == HORIZONTAL && verticalGravity == Gravity.CENTER_VERTICAL
1163                || mOrientation == VERTICAL && horizontalGravity == Gravity.CENTER_HORIZONTAL) {
1164            startSecondary += (getRowSizeSecondary(rowIndex) - sizeSecondary) / 2;
1165        }
1166        int left, top, right, bottom;
1167        if (mOrientation == HORIZONTAL) {
1168            left = start;
1169            top = startSecondary;
1170            right = end;
1171            bottom = startSecondary + sizeSecondary;
1172        } else {
1173            top = start;
1174            left = startSecondary;
1175            bottom = end;
1176            right = startSecondary + sizeSecondary;
1177        }
1178        v.layout(left, top, right, bottom);
1179        updateChildOpticalInsets(v, left, top, right, bottom);
1180        updateChildAlignments(v);
1181        if (TRACE) TraceHelper.endSection();
1182    }
1183
1184    private void updateChildOpticalInsets(View v, int left, int top, int right, int bottom) {
1185        LayoutParams p = (LayoutParams) v.getLayoutParams();
1186        p.setOpticalInsets(left - v.getLeft(), top - v.getTop(),
1187                v.getRight() - right, v.getBottom() - bottom);
1188    }
1189
1190    private void updateChildAlignments(View v) {
1191        LayoutParams p = (LayoutParams) v.getLayoutParams();
1192        p.setAlignX(mItemAlignment.horizontal.getAlignmentPosition(v));
1193        p.setAlignY(mItemAlignment.vertical.getAlignmentPosition(v));
1194    }
1195
1196    private void updateChildAlignments() {
1197        for (int i = 0, c = getChildCount(); i < c; i++) {
1198            updateChildAlignments(getChildAt(i));
1199        }
1200    }
1201
1202    private void removeInvisibleViewsAtEnd() {
1203        if (mPruneChild) {
1204            mGrid.removeInvisibleItemsAtEnd(mFocusPosition,
1205                    mReverseFlowPrimary ? 0 : mSizePrimary);
1206        }
1207    }
1208
1209    private void removeInvisibleViewsAtFront() {
1210        if (mPruneChild) {
1211            mGrid.removeInvisibleItemsAtFront(mFocusPosition,
1212                    mReverseFlowPrimary ? mSizePrimary : 0);
1213        }
1214    }
1215
1216    private boolean appendOneColumnVisibleItems() {
1217        return mGrid.appendOneColumnVisibleItems();
1218    }
1219
1220    private boolean prependOneColumnVisibleItems() {
1221        return mGrid.prependOneColumnVisibleItems();
1222    }
1223
1224    private void appendVisibleItems() {
1225        mGrid.appendVisibleItems(mReverseFlowPrimary ? 0 : mSizePrimary);
1226    }
1227
1228    private void prependVisibleItems() {
1229        mGrid.prependVisibleItems(mReverseFlowPrimary ? mSizePrimary : 0);
1230    }
1231
1232    /**
1233     * Fast layout when there is no structure change, adapter change, etc.
1234     * It will layout all views was layout requested or updated, until hit a view
1235     * with different size,  then it break and detachAndScrap all views after that.
1236     */
1237    private void fastRelayout() {
1238        boolean invalidateAfter = false;
1239        final int childCount = getChildCount();
1240        int position = -1;
1241        for (int index = 0; index < childCount; index++) {
1242            View view = getChildAt(index);
1243            position = getPositionByIndex(index);
1244            Grid.Location location = mGrid.getLocation(position);
1245            if (location == null) {
1246                if (DEBUG) Log.w(getTag(), "fastRelayout(): no Location at " + position);
1247                invalidateAfter = true;
1248                break;
1249            }
1250
1251            int startSecondary = getRowStartSecondary(location.row) - mScrollOffsetSecondary;
1252            int primarySize, end;
1253            int start = getViewMin(view);
1254            int oldPrimarySize = (mOrientation == HORIZONTAL) ?
1255                    view.getMeasuredWidth() :
1256                    view.getMeasuredHeight();
1257
1258            LayoutParams lp = (LayoutParams) view.getLayoutParams();
1259            if (lp.viewNeedsUpdate()) {
1260                int viewIndex = mBaseGridView.indexOfChild(view);
1261                detachAndScrapView(view, mRecycler);
1262                view = getViewForPosition(position);
1263                addView(view, viewIndex);
1264            }
1265
1266            if (view.isLayoutRequested()) {
1267                measureChild(view);
1268            }
1269            if (mOrientation == HORIZONTAL) {
1270                primarySize = view.getMeasuredWidth();
1271                end = start + primarySize;
1272            } else {
1273                primarySize = view.getMeasuredHeight();
1274                end = start + primarySize;
1275            }
1276            layoutChild(location.row, view, start, end, startSecondary);
1277            if (oldPrimarySize != primarySize) {
1278                // size changed invalidate remaining Locations
1279                if (DEBUG) Log.d(getTag(), "fastRelayout: view size changed at " + position);
1280                invalidateAfter = true;
1281                break;
1282            }
1283        }
1284        if (invalidateAfter) {
1285            final int savedLastPos = mGrid.getLastVisibleIndex();
1286            mGrid.invalidateItemsAfter(position);
1287            if (mPruneChild) {
1288                // in regular prune child mode, we just append items up to edge limit
1289                appendVisibleItems();
1290            } else {
1291                // prune disabled(e.g. in RowsFragment transition): append all removed items
1292                while (mGrid.appendOneColumnVisibleItems()
1293                        && mGrid.getLastVisibleIndex() < savedLastPos);
1294            }
1295        }
1296        updateScrollMin();
1297        updateScrollMax();
1298        updateScrollSecondAxis();
1299    }
1300
1301    public void removeAndRecycleAllViews(RecyclerView.Recycler recycler) {
1302        if (TRACE) TraceHelper.beginSection("removeAndRecycleAllViews");
1303        if (DEBUG) Log.v(TAG, "removeAndRecycleAllViews " + getChildCount());
1304        for (int i = getChildCount() - 1; i >= 0; i--) {
1305            removeAndRecycleViewAt(i, recycler);
1306        }
1307        if (TRACE) TraceHelper.endSection();
1308    }
1309
1310    // Lays out items based on the current scroll position
1311    @Override
1312    public void onLayoutChildren(RecyclerView.Recycler recycler, RecyclerView.State state) {
1313        if (DEBUG) {
1314            Log.v(getTag(), "layoutChildren start numRows " + mNumRows + " mScrollOffsetSecondary "
1315                    + mScrollOffsetSecondary + " mScrollOffsetPrimary " + mScrollOffsetPrimary
1316                    + " inPreLayout " + state.isPreLayout()
1317                    + " didStructureChange " + state.didStructureChange()
1318                    + " mForceFullLayout " + mForceFullLayout);
1319            Log.v(getTag(), "width " + getWidth() + " height " + getHeight());
1320        }
1321
1322        if (mNumRows == 0) {
1323            // haven't done measure yet
1324            return;
1325        }
1326        final int itemCount = state.getItemCount();
1327        if (itemCount < 0) {
1328            return;
1329        }
1330
1331        if (!mLayoutEnabled) {
1332            discardLayoutInfo();
1333            removeAndRecycleAllViews(recycler);
1334            return;
1335        }
1336        mInLayout = true;
1337
1338        final boolean scrollToFocus = !isSmoothScrolling()
1339                && mFocusScrollStrategy == BaseGridView.FOCUS_SCROLL_ALIGNED;
1340        if (mFocusPosition != NO_POSITION && mFocusPositionOffset != Integer.MIN_VALUE) {
1341            mFocusPosition = mFocusPosition + mFocusPositionOffset;
1342            mFocusPositionOffset = 0;
1343        }
1344        saveContext(recycler, state);
1345
1346        // Track the old focus view so we can adjust our system scroll position
1347        // so that any scroll animations happening now will remain valid.
1348        // We must use same delta in Pre Layout (if prelayout exists) and second layout.
1349        // So we cache the deltas in PreLayout and use it in second layout.
1350        int delta = 0, deltaSecondary = 0;
1351        if (mFocusPosition != NO_POSITION && scrollToFocus
1352                && mBaseGridView.getScrollState() != RecyclerView.SCROLL_STATE_IDLE) {
1353            // FIXME: we should get the remaining scroll animation offset from RecyclerView
1354            View focusView = findViewByPosition(mFocusPosition);
1355            if (focusView != null) {
1356                delta = mWindowAlignment.mainAxis().getSystemScrollPos(mScrollOffsetPrimary
1357                        + getViewCenter(focusView), false, false) - mScrollOffsetPrimary;
1358                deltaSecondary = mWindowAlignment.secondAxis().getSystemScrollPos(
1359                        mScrollOffsetSecondary + getViewCenterSecondary(focusView),
1360                        false, false) - mScrollOffsetSecondary;
1361            }
1362        }
1363
1364        boolean hadFocus = mBaseGridView.hasFocus();
1365        int savedFocusPos = mFocusPosition;
1366        boolean fastRelayout;
1367        if (fastRelayout = layoutInit()) {
1368            fastRelayout();
1369            View focusView = findViewByPosition(mFocusPosition);
1370            if (scrollToFocus) {
1371                scrollToView(focusView, false);
1372            }
1373            if (focusView != null && hadFocus) {
1374                focusView.requestFocus();
1375            }
1376        } else {
1377            if (mFocusPosition != NO_POSITION) {
1378                // appends items till focus position.
1379                while (appendOneColumnVisibleItems()
1380                        && findViewByPosition(mFocusPosition) == null) ;
1381            }
1382            // multiple rounds: scrollToView of first round may drag first/last child into
1383            // "visible window" and we update scrollMin/scrollMax then run second scrollToView
1384            int oldFirstVisible;
1385            int oldLastVisible;
1386            do {
1387                updateScrollMin();
1388                updateScrollMax();
1389                oldFirstVisible = mGrid.getFirstVisibleIndex();
1390                oldLastVisible = mGrid.getLastVisibleIndex();
1391                View focusView = findViewByPosition(mFocusPosition);
1392                // we need force to initialize the child view's position
1393                scrollToView(focusView, false);
1394                if (focusView != null && hadFocus) {
1395                    focusView.requestFocus();
1396                }
1397                appendVisibleItems();
1398                prependVisibleItems();
1399                removeInvisibleViewsAtFront();
1400                removeInvisibleViewsAtEnd();
1401            } while (mGrid.getFirstVisibleIndex() != oldFirstVisible ||
1402                    mGrid.getLastVisibleIndex() != oldLastVisible);
1403        }
1404
1405        if (scrollToFocus) {
1406            scrollDirectionPrimary(-delta);
1407            scrollDirectionSecondary(-deltaSecondary);
1408        }
1409        appendVisibleItems();
1410        prependVisibleItems();
1411        removeInvisibleViewsAtFront();
1412        removeInvisibleViewsAtEnd();
1413
1414        if (DEBUG) {
1415            StringWriter sw = new StringWriter();
1416            PrintWriter pw = new PrintWriter(sw);
1417            mGrid.debugPrint(pw);
1418            Log.d(getTag(), sw.toString());
1419        }
1420
1421        if (mRowSecondarySizeRefresh) {
1422            mRowSecondarySizeRefresh = false;
1423        } else {
1424            updateRowSecondarySizeRefresh();
1425        }
1426
1427        if (fastRelayout && mFocusPosition != savedFocusPos) {
1428            dispatchChildSelected();
1429        }
1430
1431        mInLayout = false;
1432        leaveContext();
1433        if (DEBUG) Log.v(getTag(), "layoutChildren end");
1434    }
1435
1436    private void offsetChildrenSecondary(int increment) {
1437        final int childCount = getChildCount();
1438        if (mOrientation == HORIZONTAL) {
1439            for (int i = 0; i < childCount; i++) {
1440                getChildAt(i).offsetTopAndBottom(increment);
1441            }
1442        } else {
1443            for (int i = 0; i < childCount; i++) {
1444                getChildAt(i).offsetLeftAndRight(increment);
1445            }
1446        }
1447    }
1448
1449    private void offsetChildrenPrimary(int increment) {
1450        final int childCount = getChildCount();
1451        if (mOrientation == VERTICAL) {
1452            for (int i = 0; i < childCount; i++) {
1453                getChildAt(i).offsetTopAndBottom(increment);
1454            }
1455        } else {
1456            for (int i = 0; i < childCount; i++) {
1457                getChildAt(i).offsetLeftAndRight(increment);
1458            }
1459        }
1460    }
1461
1462    @Override
1463    public int scrollHorizontallyBy(int dx, Recycler recycler, RecyclerView.State state) {
1464        if (DEBUG) Log.v(getTag(), "scrollHorizontallyBy " + dx);
1465        if (!mLayoutEnabled || !hasDoneFirstLayout()) {
1466            return 0;
1467        }
1468        saveContext(recycler, state);
1469        int result;
1470        if (mOrientation == HORIZONTAL) {
1471            result = scrollDirectionPrimary(dx);
1472        } else {
1473            result = scrollDirectionSecondary(dx);
1474        }
1475        leaveContext();
1476        return result;
1477    }
1478
1479    @Override
1480    public int scrollVerticallyBy(int dy, Recycler recycler, RecyclerView.State state) {
1481        if (DEBUG) Log.v(getTag(), "scrollVerticallyBy " + dy);
1482        if (!mLayoutEnabled || !hasDoneFirstLayout()) {
1483            return 0;
1484        }
1485        saveContext(recycler, state);
1486        int result;
1487        if (mOrientation == VERTICAL) {
1488            result = scrollDirectionPrimary(dy);
1489        } else {
1490            result = scrollDirectionSecondary(dy);
1491        }
1492        leaveContext();
1493        return result;
1494    }
1495
1496    // scroll in main direction may add/prune views
1497    private int scrollDirectionPrimary(int da) {
1498        if (TRACE) TraceHelper.beginSection("scrollPrimary");
1499        boolean isMaxUnknown = false, isMinUnknown = false;
1500        int minScroll = 0, maxScroll = 0;
1501        if (da > 0) {
1502            isMaxUnknown = mWindowAlignment.mainAxis().isMaxUnknown();
1503            if (!isMaxUnknown) {
1504                maxScroll = mWindowAlignment.mainAxis().getMaxScroll();
1505                if (mScrollOffsetPrimary + da > maxScroll) {
1506                    da = maxScroll - mScrollOffsetPrimary;
1507                }
1508            }
1509        } else if (da < 0) {
1510            isMinUnknown = mWindowAlignment.mainAxis().isMinUnknown();
1511            if (!isMinUnknown) {
1512                minScroll = mWindowAlignment.mainAxis().getMinScroll();
1513                if (mScrollOffsetPrimary + da < minScroll) {
1514                    da = minScroll - mScrollOffsetPrimary;
1515                }
1516            }
1517        }
1518        if (da == 0) {
1519            if (TRACE) TraceHelper.endSection();
1520            return 0;
1521        }
1522        offsetChildrenPrimary(-da);
1523        mScrollOffsetPrimary += da;
1524        if (mInLayout) {
1525            if (TRACE) TraceHelper.endSection();
1526            return da;
1527        }
1528
1529        int childCount = getChildCount();
1530        boolean updated;
1531
1532        if (mReverseFlowPrimary ? da > 0 : da < 0) {
1533            prependVisibleItems();
1534        } else {
1535            appendVisibleItems();
1536        }
1537        updated = getChildCount() > childCount;
1538        childCount = getChildCount();
1539
1540        if (TRACE) TraceHelper.beginSection("remove");
1541        if (mReverseFlowPrimary ? da > 0 : da < 0) {
1542            removeInvisibleViewsAtEnd();
1543        } else {
1544            removeInvisibleViewsAtFront();
1545        }
1546        if (TRACE) TraceHelper.endSection();
1547        updated |= getChildCount() < childCount;
1548        if (updated) {
1549            updateRowSecondarySizeRefresh();
1550        }
1551
1552        mBaseGridView.invalidate();
1553        if (TRACE) TraceHelper.endSection();
1554        return da;
1555    }
1556
1557    // scroll in second direction will not add/prune views
1558    private int scrollDirectionSecondary(int dy) {
1559        if (dy == 0) {
1560            return 0;
1561        }
1562        offsetChildrenSecondary(-dy);
1563        mScrollOffsetSecondary += dy;
1564        mBaseGridView.invalidate();
1565        return dy;
1566    }
1567
1568    private void updateScrollMax() {
1569        int highVisiblePos = (!mReverseFlowPrimary) ? mGrid.getLastVisibleIndex()
1570                : mGrid.getFirstVisibleIndex();
1571        int highMaxPos = (!mReverseFlowPrimary) ? mState.getItemCount() - 1 : 0;
1572        if (highVisiblePos < 0) {
1573            return;
1574        }
1575        final boolean highAvailable = highVisiblePos == highMaxPos;
1576        final boolean maxUnknown = mWindowAlignment.mainAxis().isMaxUnknown();
1577        if (!highAvailable && maxUnknown) {
1578            return;
1579        }
1580        int maxEdge = mGrid.findRowMax(true, sTwoInts) + mScrollOffsetPrimary;
1581        int rowIndex = sTwoInts[0];
1582        int pos = sTwoInts[1];
1583        int savedMaxEdge = mWindowAlignment.mainAxis().getMaxEdge();
1584        mWindowAlignment.mainAxis().setMaxEdge(maxEdge);
1585        int maxScroll = getPrimarySystemScrollPosition(findViewByPosition(pos));
1586        mWindowAlignment.mainAxis().setMaxEdge(savedMaxEdge);
1587
1588        if (highAvailable) {
1589            mWindowAlignment.mainAxis().setMaxEdge(maxEdge);
1590            mWindowAlignment.mainAxis().setMaxScroll(maxScroll);
1591            if (DEBUG) Log.v(getTag(), "updating scroll maxEdge to " + maxEdge +
1592                    " scrollMax to " + maxScroll);
1593        } else {
1594            // the maxScroll for currently last visible item is larger,
1595            // so we must invalidate the max scroll value.
1596            if (maxScroll > mWindowAlignment.mainAxis().getMaxScroll()) {
1597                mWindowAlignment.mainAxis().invalidateScrollMax();
1598                if (DEBUG) Log.v(getTag(), "Invalidate scrollMax since it should be "
1599                        + "greater than " + maxScroll);
1600            }
1601        }
1602    }
1603
1604    private void updateScrollMin() {
1605        int lowVisiblePos = (!mReverseFlowPrimary) ? mGrid.getFirstVisibleIndex()
1606                : mGrid.getLastVisibleIndex();
1607        int lowMinPos = (!mReverseFlowPrimary) ? 0 : mState.getItemCount() - 1;
1608        if (lowVisiblePos < 0) {
1609            return;
1610        }
1611        final boolean lowAvailable = lowVisiblePos == lowMinPos;
1612        final boolean minUnknown = mWindowAlignment.mainAxis().isMinUnknown();
1613        if (!lowAvailable && minUnknown) {
1614            return;
1615        }
1616        int minEdge = mGrid.findRowMin(false, sTwoInts) + mScrollOffsetPrimary;
1617        int rowIndex = sTwoInts[0];
1618        int pos = sTwoInts[1];
1619        int savedMinEdge = mWindowAlignment.mainAxis().getMinEdge();
1620        mWindowAlignment.mainAxis().setMinEdge(minEdge);
1621        int minScroll = getPrimarySystemScrollPosition(findViewByPosition(pos));
1622        mWindowAlignment.mainAxis().setMinEdge(savedMinEdge);
1623
1624        if (lowAvailable) {
1625            mWindowAlignment.mainAxis().setMinEdge(minEdge);
1626            mWindowAlignment.mainAxis().setMinScroll(minScroll);
1627            if (DEBUG) Log.v(getTag(), "updating scroll minEdge to " + minEdge +
1628                    " scrollMin to " + minScroll);
1629        } else {
1630            // the minScroll for currently first visible item is smaller,
1631            // so we must invalidate the min scroll value.
1632            if (minScroll < mWindowAlignment.mainAxis().getMinScroll()) {
1633                mWindowAlignment.mainAxis().invalidateScrollMin();
1634                if (DEBUG) Log.v(getTag(), "Invalidate scrollMin, since it should be "
1635                        + "less than " + minScroll);
1636            }
1637        }
1638    }
1639
1640    private void updateScrollSecondAxis() {
1641        mWindowAlignment.secondAxis().setMinEdge(0);
1642        mWindowAlignment.secondAxis().setMaxEdge(getSizeSecondary());
1643    }
1644
1645    private void initScrollController() {
1646        mWindowAlignment.reset();
1647        mWindowAlignment.horizontal.setSize(getWidth());
1648        mWindowAlignment.vertical.setSize(getHeight());
1649        mWindowAlignment.horizontal.setPadding(getPaddingLeft(), getPaddingRight());
1650        mWindowAlignment.vertical.setPadding(getPaddingTop(), getPaddingBottom());
1651        mSizePrimary = mWindowAlignment.mainAxis().getSize();
1652        mScrollOffsetPrimary = -mWindowAlignment.mainAxis().getPaddingLow();
1653        mScrollOffsetSecondary = -mWindowAlignment.secondAxis().getPaddingLow();
1654
1655        if (DEBUG) {
1656            Log.v(getTag(), "initScrollController mSizePrimary " + mSizePrimary
1657                    + " mWindowAlignment " + mWindowAlignment
1658                    + " mScrollOffsetPrimary " + mScrollOffsetPrimary);
1659        }
1660    }
1661
1662    private void updateScrollController() {
1663        // mScrollOffsetPrimary and mScrollOffsetSecondary includes the padding.
1664        // e.g. when topPadding is 16 for horizontal grid view,  the initial
1665        // mScrollOffsetSecondary is -16.  fastRelayout() put views based on offsets(not padding),
1666        // when padding changes to 20,  we also need update mScrollOffsetSecondary to -20 before
1667        // fastRelayout() is performed
1668        int paddingPrimaryDiff, paddingSecondaryDiff;
1669        if (mOrientation == HORIZONTAL) {
1670            paddingPrimaryDiff = getPaddingLeft() - mWindowAlignment.horizontal.getPaddingLow();
1671            paddingSecondaryDiff = getPaddingTop() - mWindowAlignment.vertical.getPaddingLow();
1672        } else {
1673            paddingPrimaryDiff = getPaddingTop() - mWindowAlignment.vertical.getPaddingLow();
1674            paddingSecondaryDiff = getPaddingLeft() - mWindowAlignment.horizontal.getPaddingLow();
1675        }
1676        mScrollOffsetPrimary -= paddingPrimaryDiff;
1677        mScrollOffsetSecondary -= paddingSecondaryDiff;
1678
1679        mWindowAlignment.horizontal.setSize(getWidth());
1680        mWindowAlignment.vertical.setSize(getHeight());
1681        mWindowAlignment.horizontal.setPadding(getPaddingLeft(), getPaddingRight());
1682        mWindowAlignment.vertical.setPadding(getPaddingTop(), getPaddingBottom());
1683        mSizePrimary = mWindowAlignment.mainAxis().getSize();
1684
1685        if (DEBUG) {
1686            Log.v(getTag(), "updateScrollController mSizePrimary " + mSizePrimary
1687                    + " mWindowAlignment " + mWindowAlignment
1688                    + " mScrollOffsetPrimary " + mScrollOffsetPrimary);
1689        }
1690    }
1691
1692    public void setSelection(RecyclerView parent, int position) {
1693        setSelection(parent, position, false);
1694    }
1695
1696    public void setSelectionSmooth(RecyclerView parent, int position) {
1697        setSelection(parent, position, true);
1698    }
1699
1700    public int getSelection() {
1701        return mFocusPosition;
1702    }
1703
1704    public void setSelection(RecyclerView parent, int position, boolean smooth) {
1705        if (mFocusPosition != position && position != NO_POSITION) {
1706            scrollToSelection(parent, position, smooth);
1707        }
1708    }
1709
1710    private void scrollToSelection(RecyclerView parent, int position, boolean smooth) {
1711        if (TRACE) TraceHelper.beginSection("scrollToSelection");
1712        View view = findViewByPosition(position);
1713        if (view != null) {
1714            mInSelection = true;
1715            scrollToView(view, smooth);
1716            mInSelection = false;
1717        } else {
1718            mFocusPosition = position;
1719            mFocusPositionOffset = 0;
1720            if (!mLayoutEnabled) {
1721                return;
1722            }
1723            if (smooth) {
1724                if (!hasDoneFirstLayout()) {
1725                    Log.w(getTag(), "setSelectionSmooth should " +
1726                            "not be called before first layout pass");
1727                    return;
1728                }
1729                LinearSmoothScroller linearSmoothScroller =
1730                        new LinearSmoothScroller(parent.getContext()) {
1731                    @Override
1732                    public PointF computeScrollVectorForPosition(int targetPosition) {
1733                        if (getChildCount() == 0) {
1734                            return null;
1735                        }
1736                        final int firstChildPos = getPosition(getChildAt(0));
1737                        // TODO We should be able to deduce direction from bounds of current and target focus,
1738                        // rather than making assumptions about positions and directionality
1739                        final boolean isStart = mReverseFlowPrimary ? targetPosition > firstChildPos : targetPosition < firstChildPos;
1740                        final int direction = isStart ? -1 : 1;
1741                        if (mOrientation == HORIZONTAL) {
1742                            return new PointF(direction, 0);
1743                        } else {
1744                            return new PointF(0, direction);
1745                        }
1746                    }
1747
1748                    @Override
1749                    protected void onStop() {
1750                        // onTargetFound() may not be called if we hit the "wall" first.
1751                        View targetView = findViewByPosition(getTargetPosition());
1752                        if (hasFocus() && targetView != null) {
1753                            targetView.requestFocus();
1754                        }
1755                        dispatchChildSelected();
1756                        super.onStop();
1757                    }
1758
1759                    @Override
1760                    protected void onTargetFound(View targetView,
1761                            RecyclerView.State state, Action action) {
1762                        if (getScrollPosition(targetView, sTwoInts)) {
1763                            int dx, dy;
1764                            if (mOrientation == HORIZONTAL) {
1765                                dx = sTwoInts[0];
1766                                dy = sTwoInts[1];
1767                            } else {
1768                                dx = sTwoInts[1];
1769                                dy = sTwoInts[0];
1770                            }
1771                            final int distance = (int) Math.sqrt(dx * dx + dy * dy);
1772                            final int time = calculateTimeForDeceleration(distance);
1773                            action.update(dx, dy, time, mDecelerateInterpolator);
1774                        }
1775                    }
1776                };
1777                linearSmoothScroller.setTargetPosition(position);
1778                startSmoothScroll(linearSmoothScroller);
1779            } else {
1780                mForceFullLayout = true;
1781                parent.requestLayout();
1782            }
1783        }
1784        if (TRACE) TraceHelper.endSection();
1785    }
1786
1787    @Override
1788    public void onItemsAdded(RecyclerView recyclerView, int positionStart, int itemCount) {
1789        if (DEBUG) Log.v(getTag(), "onItemsAdded positionStart "
1790                + positionStart + " itemCount " + itemCount);
1791        if (mFocusPosition != NO_POSITION && mFocusPositionOffset != Integer.MIN_VALUE) {
1792            int pos = mFocusPosition + mFocusPositionOffset;
1793            if (positionStart <= pos) {
1794                mFocusPositionOffset += itemCount;
1795            }
1796        }
1797        mChildrenStates.clear();
1798    }
1799
1800    @Override
1801    public void onItemsChanged(RecyclerView recyclerView) {
1802        if (DEBUG) Log.v(getTag(), "onItemsChanged");
1803        mFocusPositionOffset = 0;
1804        mChildrenStates.clear();
1805    }
1806
1807    @Override
1808    public void onItemsRemoved(RecyclerView recyclerView, int positionStart, int itemCount) {
1809        if (DEBUG) Log.v(getTag(), "onItemsRemoved positionStart "
1810                + positionStart + " itemCount " + itemCount);
1811        if (mFocusPosition != NO_POSITION && mFocusPositionOffset != Integer.MIN_VALUE) {
1812            int pos = mFocusPosition + mFocusPositionOffset;
1813            if (positionStart <= pos) {
1814                if (positionStart + itemCount > pos) {
1815                    // stop updating offset after the focus item was removed
1816                    mFocusPositionOffset = Integer.MIN_VALUE;
1817                } else {
1818                    mFocusPositionOffset -= itemCount;
1819                }
1820            }
1821        }
1822        mChildrenStates.clear();
1823    }
1824
1825    @Override
1826    public void onItemsMoved(RecyclerView recyclerView, int fromPosition, int toPosition,
1827            int itemCount) {
1828        if (DEBUG) Log.v(getTag(), "onItemsMoved fromPosition "
1829                + fromPosition + " toPosition " + toPosition);
1830        if (mFocusPosition != NO_POSITION && mFocusPositionOffset != Integer.MIN_VALUE) {
1831            int pos = mFocusPosition + mFocusPositionOffset;
1832            if (fromPosition <= pos && pos < fromPosition + itemCount) {
1833                // moved items include focused position
1834                mFocusPositionOffset += toPosition - fromPosition;
1835            } else if (fromPosition < pos && toPosition > pos - itemCount) {
1836                // move items before focus position to after focused position
1837                mFocusPositionOffset -= itemCount;
1838            } else if (fromPosition > pos && toPosition < pos) {
1839                // move items after focus position to before focused position
1840                mFocusPositionOffset += itemCount;
1841            }
1842        }
1843        mChildrenStates.clear();
1844    }
1845
1846    @Override
1847    public void onItemsUpdated(RecyclerView recyclerView, int positionStart, int itemCount) {
1848        if (DEBUG) Log.v(getTag(), "onItemsUpdated positionStart "
1849                + positionStart + " itemCount " + itemCount);
1850        for (int i = positionStart, end = positionStart + itemCount; i < end; i++) {
1851            mChildrenStates.remove(i);
1852        }
1853    }
1854
1855    @Override
1856    public boolean onRequestChildFocus(RecyclerView parent, View child, View focused) {
1857        if (mFocusSearchDisabled) {
1858            return true;
1859        }
1860        if (getPositionByView(child) == NO_POSITION) {
1861            // This shouldn't happen, but in case it does be sure not to attempt a
1862            // scroll to a view whose item has been removed.
1863            return true;
1864        }
1865        if (!mInLayout && !mInSelection) {
1866            scrollToView(child, true);
1867        }
1868        return true;
1869    }
1870
1871    @Override
1872    public boolean requestChildRectangleOnScreen(RecyclerView parent, View view, Rect rect,
1873            boolean immediate) {
1874        if (DEBUG) Log.v(getTag(), "requestChildRectangleOnScreen " + view + " " + rect);
1875        return false;
1876    }
1877
1878    int getScrollOffsetX() {
1879        return mOrientation == HORIZONTAL ? mScrollOffsetPrimary : mScrollOffsetSecondary;
1880    }
1881
1882    int getScrollOffsetY() {
1883        return mOrientation == HORIZONTAL ? mScrollOffsetSecondary : mScrollOffsetPrimary;
1884    }
1885
1886    public void getViewSelectedOffsets(View view, int[] offsets) {
1887        if (mOrientation == HORIZONTAL) {
1888            offsets[0] = getPrimarySystemScrollPosition(view) - mScrollOffsetPrimary;
1889            offsets[1] = getSecondarySystemScrollPosition(view) - mScrollOffsetSecondary;
1890        } else {
1891            offsets[1] = getPrimarySystemScrollPosition(view) - mScrollOffsetPrimary;
1892            offsets[0] = getSecondarySystemScrollPosition(view) - mScrollOffsetSecondary;
1893        }
1894    }
1895
1896    private int getPrimarySystemScrollPosition(View view) {
1897        final int viewCenterPrimary = mScrollOffsetPrimary + getViewCenter(view);
1898        final int viewMin = getViewMin(view);
1899        final int viewMax = getViewMax(view);
1900        // TODO: change to use State object in onRequestChildFocus()
1901        boolean isMin, isMax;
1902        if (!mReverseFlowPrimary) {
1903            isMin = mGrid.getFirstVisibleIndex() == 0;
1904            isMax = mGrid.getLastVisibleIndex() == (mState == null ?
1905                    getItemCount() : mState.getItemCount()) - 1;
1906        } else {
1907            isMax = mGrid.getFirstVisibleIndex() == 0;
1908            isMin = mGrid.getLastVisibleIndex() == (mState == null ?
1909                    getItemCount() : mState.getItemCount()) - 1;
1910        }
1911        for (int i = getChildCount() - 1; (isMin || isMax) && i >= 0; i--) {
1912            View v = getChildAt(i);
1913            if (v == view || v == null) {
1914                continue;
1915            }
1916            if (isMin && getViewMin(v) < viewMin) {
1917                isMin = false;
1918            }
1919            if (isMax && getViewMax(v) > viewMax) {
1920                isMax = false;
1921            }
1922        }
1923        return mWindowAlignment.mainAxis().getSystemScrollPos(viewCenterPrimary, isMin, isMax);
1924    }
1925
1926    private int getSecondarySystemScrollPosition(View view) {
1927        int viewCenterSecondary = mScrollOffsetSecondary + getViewCenterSecondary(view);
1928        int pos = getPositionByView(view);
1929        Grid.Location location = mGrid.getLocation(pos);
1930        final int row = location.row;
1931        final boolean isMin, isMax;
1932        if (!mReverseFlowSecondary) {
1933            isMin = row == 0;
1934            isMax = row == mGrid.getNumRows() - 1;
1935        } else {
1936            isMax = row == 0;
1937            isMin = row == mGrid.getNumRows() - 1;
1938        }
1939        return mWindowAlignment.secondAxis().getSystemScrollPos(viewCenterSecondary, isMin, isMax);
1940    }
1941
1942    /**
1943     * Scroll to a given child view and change mFocusPosition.
1944     */
1945    private void scrollToView(View view, boolean smooth) {
1946        int newFocusPosition = getPositionByView(view);
1947        if (newFocusPosition != mFocusPosition) {
1948            mFocusPosition = newFocusPosition;
1949            mFocusPositionOffset = 0;
1950            if (!mInLayout) {
1951                dispatchChildSelected();
1952            }
1953        }
1954        if (mBaseGridView.isChildrenDrawingOrderEnabledInternal()) {
1955            mBaseGridView.invalidate();
1956        }
1957        if (view == null) {
1958            return;
1959        }
1960        if (!view.hasFocus() && mBaseGridView.hasFocus()) {
1961            // transfer focus to the child if it does not have focus yet (e.g. triggered
1962            // by setSelection())
1963            view.requestFocus();
1964        }
1965        if (!mScrollEnabled && smooth) {
1966            return;
1967        }
1968        if (getScrollPosition(view, sTwoInts)) {
1969            scrollGrid(sTwoInts[0], sTwoInts[1], smooth);
1970        }
1971    }
1972
1973    private boolean getScrollPosition(View view, int[] deltas) {
1974        switch (mFocusScrollStrategy) {
1975        case BaseGridView.FOCUS_SCROLL_ALIGNED:
1976        default:
1977            return getAlignedPosition(view, deltas);
1978        case BaseGridView.FOCUS_SCROLL_ITEM:
1979        case BaseGridView.FOCUS_SCROLL_PAGE:
1980            return getNoneAlignedPosition(view, deltas);
1981        }
1982    }
1983
1984    private boolean getNoneAlignedPosition(View view, int[] deltas) {
1985        int pos = getPositionByView(view);
1986        int viewMin = getViewMin(view);
1987        int viewMax = getViewMax(view);
1988        // we either align "firstView" to left/top padding edge
1989        // or align "lastView" to right/bottom padding edge
1990        View firstView = null;
1991        View lastView = null;
1992        int paddingLow = mWindowAlignment.mainAxis().getPaddingLow();
1993        int clientSize = mWindowAlignment.mainAxis().getClientSize();
1994        final int row = mGrid.getRowIndex(pos);
1995        if (viewMin < paddingLow) {
1996            // view enters low padding area:
1997            firstView = view;
1998            if (mFocusScrollStrategy == BaseGridView.FOCUS_SCROLL_PAGE) {
1999                // scroll one "page" left/top,
2000                // align first visible item of the "page" at the low padding edge.
2001                while (prependOneColumnVisibleItems()) {
2002                    CircularIntArray positions =
2003                            mGrid.getItemPositionsInRows(mGrid.getFirstVisibleIndex(), pos)[row];
2004                    firstView = findViewByPosition(positions.get(0));
2005                    if (viewMax - getViewMin(firstView) > clientSize) {
2006                        if (positions.size() > 2) {
2007                            firstView = findViewByPosition(positions.get(2));
2008                        }
2009                        break;
2010                    }
2011                }
2012            }
2013        } else if (viewMax > clientSize + paddingLow) {
2014            // view enters high padding area:
2015            if (mFocusScrollStrategy == BaseGridView.FOCUS_SCROLL_PAGE) {
2016                // scroll whole one page right/bottom, align view at the low padding edge.
2017                firstView = view;
2018                do {
2019                    CircularIntArray positions =
2020                            mGrid.getItemPositionsInRows(pos, mGrid.getLastVisibleIndex())[row];
2021                    lastView = findViewByPosition(positions.get(positions.size() - 1));
2022                    if (getViewMax(lastView) - viewMin > clientSize) {
2023                        lastView = null;
2024                        break;
2025                    }
2026                } while (appendOneColumnVisibleItems());
2027                if (lastView != null) {
2028                    // however if we reached end,  we should align last view.
2029                    firstView = null;
2030                }
2031            } else {
2032                lastView = view;
2033            }
2034        }
2035        int scrollPrimary = 0;
2036        int scrollSecondary = 0;
2037        if (firstView != null) {
2038            scrollPrimary = getViewMin(firstView) - paddingLow;
2039        } else if (lastView != null) {
2040            scrollPrimary = getViewMax(lastView) - (paddingLow + clientSize);
2041        }
2042        View secondaryAlignedView;
2043        if (firstView != null) {
2044            secondaryAlignedView = firstView;
2045        } else if (lastView != null) {
2046            secondaryAlignedView = lastView;
2047        } else {
2048            secondaryAlignedView = view;
2049        }
2050        scrollSecondary = getSecondarySystemScrollPosition(secondaryAlignedView);
2051        scrollSecondary -= mScrollOffsetSecondary;
2052        if (scrollPrimary != 0 || scrollSecondary != 0) {
2053            deltas[0] = scrollPrimary;
2054            deltas[1] = scrollSecondary;
2055            return true;
2056        }
2057        return false;
2058    }
2059
2060    private boolean getAlignedPosition(View view, int[] deltas) {
2061        int scrollPrimary = getPrimarySystemScrollPosition(view);
2062        int scrollSecondary = getSecondarySystemScrollPosition(view);
2063        if (DEBUG) {
2064            Log.v(getTag(), "getAlignedPosition " + scrollPrimary + " " + scrollSecondary
2065                    + " " + mWindowAlignment);
2066            Log.v(getTag(), "getAlignedPosition " + mScrollOffsetPrimary + " " + mScrollOffsetSecondary);
2067        }
2068        scrollPrimary -= mScrollOffsetPrimary;
2069        scrollSecondary -= mScrollOffsetSecondary;
2070        if (scrollPrimary != 0 || scrollSecondary != 0) {
2071            deltas[0] = scrollPrimary;
2072            deltas[1] = scrollSecondary;
2073            return true;
2074        }
2075        return false;
2076    }
2077
2078    private void scrollGrid(int scrollPrimary, int scrollSecondary, boolean smooth) {
2079        if (mInLayout) {
2080            scrollDirectionPrimary(scrollPrimary);
2081            scrollDirectionSecondary(scrollSecondary);
2082        } else {
2083            int scrollX;
2084            int scrollY;
2085            if (mOrientation == HORIZONTAL) {
2086                scrollX = scrollPrimary;
2087                scrollY = scrollSecondary;
2088            } else {
2089                scrollX = scrollSecondary;
2090                scrollY = scrollPrimary;
2091            }
2092            if (smooth) {
2093                mBaseGridView.smoothScrollBy(scrollX, scrollY);
2094            } else {
2095                mBaseGridView.scrollBy(scrollX, scrollY);
2096            }
2097        }
2098    }
2099
2100    public void setPruneChild(boolean pruneChild) {
2101        if (mPruneChild != pruneChild) {
2102            mPruneChild = pruneChild;
2103            if (mPruneChild) {
2104                requestLayout();
2105            }
2106        }
2107    }
2108
2109    public boolean getPruneChild() {
2110        return mPruneChild;
2111    }
2112
2113    public void setScrollEnabled(boolean scrollEnabled) {
2114        if (mScrollEnabled != scrollEnabled) {
2115            mScrollEnabled = scrollEnabled;
2116            if (mScrollEnabled && mFocusScrollStrategy == BaseGridView.FOCUS_SCROLL_ALIGNED
2117                    && mFocusPosition != NO_POSITION) {
2118                scrollToSelection(mBaseGridView, mFocusPosition, true);
2119            }
2120        }
2121    }
2122
2123    public boolean isScrollEnabled() {
2124        return mScrollEnabled;
2125    }
2126
2127    private int findImmediateChildIndex(View view) {
2128        while (view != null && view != mBaseGridView) {
2129            int index = mBaseGridView.indexOfChild(view);
2130            if (index >= 0) {
2131                return index;
2132            }
2133            view = (View) view.getParent();
2134        }
2135        return NO_POSITION;
2136    }
2137
2138    void setFocusSearchDisabled(boolean disabled) {
2139        mFocusSearchDisabled = disabled;
2140    }
2141
2142    boolean isFocusSearchDisabled() {
2143        return mFocusSearchDisabled;
2144    }
2145
2146    @Override
2147    public View onInterceptFocusSearch(View focused, int direction) {
2148        if (mFocusSearchDisabled) {
2149            return focused;
2150        }
2151        return null;
2152    }
2153
2154    boolean hasPreviousViewInSameRow(int pos) {
2155        if (mGrid == null || pos == NO_POSITION || mGrid.getFirstVisibleIndex() < 0) {
2156            return false;
2157        }
2158        if (mGrid.getFirstVisibleIndex() > 0) {
2159            return true;
2160        }
2161        final int focusedRow = mGrid.getLocation(pos).row;
2162        for (int i = getChildCount() - 1; i >= 0; i--) {
2163            int position = getPositionByIndex(i);
2164            Grid.Location loc = mGrid.getLocation(position);
2165            if (loc != null && loc.row == focusedRow) {
2166                if (position < pos) {
2167                    return true;
2168                }
2169            }
2170        }
2171        return false;
2172    }
2173
2174    @Override
2175    public boolean onAddFocusables(RecyclerView recyclerView,
2176            ArrayList<View> views, int direction, int focusableMode) {
2177        if (mFocusSearchDisabled) {
2178            return true;
2179        }
2180        // If this viewgroup or one of its children currently has focus then we
2181        // consider our children for focus searching in main direction on the same row.
2182        // If this viewgroup has no focus and using focus align, we want the system
2183        // to ignore our children and pass focus to the viewgroup, which will pass
2184        // focus on to its children appropriately.
2185        // If this viewgroup has no focus and not using focus align, we want to
2186        // consider the child that does not overlap with padding area.
2187        if (recyclerView.hasFocus()) {
2188            final int movement = getMovement(direction);
2189            if (movement != PREV_ITEM && movement != NEXT_ITEM) {
2190                // Move on secondary direction uses default addFocusables().
2191                return false;
2192            }
2193            final View focused = recyclerView.findFocus();
2194            final int focusedPos = getPositionByIndex(findImmediateChildIndex(focused));
2195            // Add focusables of focused item.
2196            if (focusedPos != NO_POSITION) {
2197                findViewByPosition(focusedPos).addFocusables(views,  direction, focusableMode);
2198            }
2199            final int focusedRow = mGrid != null && focusedPos != NO_POSITION ?
2200                    mGrid.getLocation(focusedPos).row : NO_POSITION;
2201            // Add focusables of next neighbor of same row on the focus search direction.
2202            if (mGrid != null) {
2203                final int focusableCount = views.size();
2204                for (int i = 0, count = getChildCount(); i < count; i++) {
2205                    int index = movement == NEXT_ITEM ? i : count - 1 - i;
2206                    final View child = getChildAt(index);
2207                    if (child.getVisibility() != View.VISIBLE) {
2208                        continue;
2209                    }
2210                    int position = getPositionByIndex(index);
2211                    Grid.Location loc = mGrid.getLocation(position);
2212                    if (focusedRow == NO_POSITION || (loc != null && loc.row == focusedRow)) {
2213                        if (focusedPos == NO_POSITION ||
2214                                (movement == NEXT_ITEM && position > focusedPos)
2215                                || (movement == PREV_ITEM && position < focusedPos)) {
2216                            child.addFocusables(views,  direction, focusableMode);
2217                            if (views.size() > focusableCount) {
2218                                break;
2219                            }
2220                        }
2221                    }
2222                }
2223            }
2224        } else {
2225            if (mFocusScrollStrategy != BaseGridView.FOCUS_SCROLL_ALIGNED) {
2226                // adding views not overlapping padding area to avoid scrolling in gaining focus
2227                int left = mWindowAlignment.mainAxis().getPaddingLow();
2228                int right = mWindowAlignment.mainAxis().getClientSize() + left;
2229                int focusableCount = views.size();
2230                for (int i = 0, count = getChildCount(); i < count; i++) {
2231                    View child = getChildAt(i);
2232                    if (child.getVisibility() == View.VISIBLE) {
2233                        if (getViewMin(child) >= left && getViewMax(child) <= right) {
2234                            child.addFocusables(views, direction, focusableMode);
2235                        }
2236                    }
2237                }
2238                // if we cannot find any, then just add all children.
2239                if (views.size() == focusableCount) {
2240                    for (int i = 0, count = getChildCount(); i < count; i++) {
2241                        View child = getChildAt(i);
2242                        if (child.getVisibility() == View.VISIBLE) {
2243                            child.addFocusables(views, direction, focusableMode);
2244                        }
2245                    }
2246                    if (views.size() != focusableCount) {
2247                        return true;
2248                    }
2249                } else {
2250                    return true;
2251                }
2252                // if still cannot find any, fall through and add itself
2253            }
2254            if (recyclerView.isFocusable()) {
2255                views.add(recyclerView);
2256            }
2257        }
2258        return true;
2259    }
2260
2261    @Override
2262    public View onFocusSearchFailed(View focused, int direction, Recycler recycler,
2263            RecyclerView.State state) {
2264        if (DEBUG) Log.v(getTag(), "onFocusSearchFailed direction " + direction);
2265
2266        View view = null;
2267        int movement = getMovement(direction);
2268        final boolean isScroll = mBaseGridView.getScrollState() != RecyclerView.SCROLL_STATE_IDLE;
2269        if (mNumRows == 1) {
2270            // for simple row, use LinearSmoothScroller to smooth animation.
2271            // It will stay at a fixed cap speed in continuous scroll.
2272            if (movement == NEXT_ITEM) {
2273                int newPos = mFocusPosition + mNumRows;
2274                if (newPos < getItemCount() && mScrollEnabled) {
2275                    setSelectionSmooth(mBaseGridView, newPos);
2276                    view = focused;
2277                } else {
2278                    if (isScroll || !mFocusOutEnd) {
2279                        view = focused;
2280                    }
2281                }
2282            } else if (movement == PREV_ITEM){
2283                int newPos = mFocusPosition - mNumRows;
2284                if (newPos >= 0 && mScrollEnabled) {
2285                    setSelectionSmooth(mBaseGridView, newPos);
2286                    view = focused;
2287                } else {
2288                    if (isScroll || !mFocusOutFront) {
2289                        view = focused;
2290                    }
2291                }
2292            }
2293        } else if (mNumRows > 1) {
2294            // for possible staggered grid,  we need guarantee focus to same row/column.
2295            // TODO: we may also use LinearSmoothScroller.
2296            saveContext(recycler, state);
2297            final FocusFinder ff = FocusFinder.getInstance();
2298            if (movement == NEXT_ITEM) {
2299                while (view == null && appendOneColumnVisibleItems()) {
2300                    view = ff.findNextFocus(mBaseGridView, focused, direction);
2301                }
2302            } else if (movement == PREV_ITEM){
2303                while (view == null && prependOneColumnVisibleItems()) {
2304                    view = ff.findNextFocus(mBaseGridView, focused, direction);
2305                }
2306            }
2307            if (view == null) {
2308                // returning the same view to prevent focus lost when scrolling past the end of the list
2309                if (movement == PREV_ITEM) {
2310                    view = mFocusOutFront && !isScroll ? null : focused;
2311                } else if (movement == NEXT_ITEM){
2312                    view = mFocusOutEnd && !isScroll ? null : focused;
2313                }
2314            }
2315            leaveContext();
2316        }
2317        if (DEBUG) Log.v(getTag(), "returning view " + view);
2318        return view;
2319    }
2320
2321    boolean gridOnRequestFocusInDescendants(RecyclerView recyclerView, int direction,
2322            Rect previouslyFocusedRect) {
2323        switch (mFocusScrollStrategy) {
2324        case BaseGridView.FOCUS_SCROLL_ALIGNED:
2325        default:
2326            return gridOnRequestFocusInDescendantsAligned(recyclerView,
2327                    direction, previouslyFocusedRect);
2328        case BaseGridView.FOCUS_SCROLL_PAGE:
2329        case BaseGridView.FOCUS_SCROLL_ITEM:
2330            return gridOnRequestFocusInDescendantsUnaligned(recyclerView,
2331                    direction, previouslyFocusedRect);
2332        }
2333    }
2334
2335    private boolean gridOnRequestFocusInDescendantsAligned(RecyclerView recyclerView,
2336            int direction, Rect previouslyFocusedRect) {
2337        View view = findViewByPosition(mFocusPosition);
2338        if (view != null) {
2339            boolean result = view.requestFocus(direction, previouslyFocusedRect);
2340            if (!result && DEBUG) {
2341                Log.w(getTag(), "failed to request focus on " + view);
2342            }
2343            return result;
2344        }
2345        return false;
2346    }
2347
2348    private boolean gridOnRequestFocusInDescendantsUnaligned(RecyclerView recyclerView,
2349            int direction, Rect previouslyFocusedRect) {
2350        // focus to view not overlapping padding area to avoid scrolling in gaining focus
2351        int index;
2352        int increment;
2353        int end;
2354        int count = getChildCount();
2355        if ((direction & View.FOCUS_FORWARD) != 0) {
2356            index = 0;
2357            increment = 1;
2358            end = count;
2359        } else {
2360            index = count - 1;
2361            increment = -1;
2362            end = -1;
2363        }
2364        int left = mWindowAlignment.mainAxis().getPaddingLow();
2365        int right = mWindowAlignment.mainAxis().getClientSize() + left;
2366        for (int i = index; i != end; i += increment) {
2367            View child = getChildAt(i);
2368            if (child.getVisibility() == View.VISIBLE) {
2369                if (getViewMin(child) >= left && getViewMax(child) <= right) {
2370                    if (child.requestFocus(direction, previouslyFocusedRect)) {
2371                        return true;
2372                    }
2373                }
2374            }
2375        }
2376        return false;
2377    }
2378
2379    private final static int PREV_ITEM = 0;
2380    private final static int NEXT_ITEM = 1;
2381    private final static int PREV_ROW = 2;
2382    private final static int NEXT_ROW = 3;
2383
2384    private int getMovement(int direction) {
2385        int movement = View.FOCUS_LEFT;
2386
2387        if (mOrientation == HORIZONTAL) {
2388            switch(direction) {
2389                case View.FOCUS_LEFT:
2390                    movement = (!mReverseFlowPrimary) ? PREV_ITEM : NEXT_ITEM;
2391                    break;
2392                case View.FOCUS_RIGHT:
2393                    movement = (!mReverseFlowPrimary) ? NEXT_ITEM : PREV_ITEM;
2394                    break;
2395                case View.FOCUS_UP:
2396                    movement = PREV_ROW;
2397                    break;
2398                case View.FOCUS_DOWN:
2399                    movement = NEXT_ROW;
2400                    break;
2401            }
2402         } else if (mOrientation == VERTICAL) {
2403             switch(direction) {
2404                 case View.FOCUS_LEFT:
2405                     movement = (!mReverseFlowPrimary) ? PREV_ROW : NEXT_ROW;
2406                     break;
2407                 case View.FOCUS_RIGHT:
2408                     movement = (!mReverseFlowPrimary) ? NEXT_ROW : PREV_ROW;
2409                     break;
2410                 case View.FOCUS_UP:
2411                     movement = PREV_ITEM;
2412                     break;
2413                 case View.FOCUS_DOWN:
2414                     movement = NEXT_ITEM;
2415                     break;
2416             }
2417         }
2418
2419        return movement;
2420    }
2421
2422    int getChildDrawingOrder(RecyclerView recyclerView, int childCount, int i) {
2423        View view = findViewByPosition(mFocusPosition);
2424        if (view == null) {
2425            return i;
2426        }
2427        int focusIndex = recyclerView.indexOfChild(view);
2428        // supposely 0 1 2 3 4 5 6 7 8 9, 4 is the center item
2429        // drawing order is 0 1 2 3 9 8 7 6 5 4
2430        if (i < focusIndex) {
2431            return i;
2432        } else if (i < childCount - 1) {
2433            return focusIndex + childCount - 1 - i;
2434        } else {
2435            return focusIndex;
2436        }
2437    }
2438
2439    @Override
2440    public void onAdapterChanged(RecyclerView.Adapter oldAdapter,
2441            RecyclerView.Adapter newAdapter) {
2442        if (DEBUG) Log.v(getTag(), "onAdapterChanged to " + newAdapter);
2443        if (oldAdapter != null) {
2444            discardLayoutInfo();
2445            mFocusPosition = NO_POSITION;
2446            mFocusPositionOffset = 0;
2447            mChildrenStates.clear();
2448        }
2449        super.onAdapterChanged(oldAdapter, newAdapter);
2450    }
2451
2452    private void discardLayoutInfo() {
2453        mGrid = null;
2454        mRowSizeSecondary = null;
2455        mRowSecondarySizeRefresh = false;
2456    }
2457
2458    public void setLayoutEnabled(boolean layoutEnabled) {
2459        if (mLayoutEnabled != layoutEnabled) {
2460            mLayoutEnabled = layoutEnabled;
2461            requestLayout();
2462        }
2463    }
2464
2465    void setChildrenVisibility(int visiblity) {
2466        mChildVisibility = visiblity;
2467        if (mChildVisibility != -1) {
2468            int count = getChildCount();
2469            for (int i= 0; i < count; i++) {
2470                getChildAt(i).setVisibility(mChildVisibility);
2471            }
2472        }
2473    }
2474
2475    final static class SavedState implements Parcelable {
2476
2477        int index; // index inside adapter of the current view
2478        Bundle childStates = Bundle.EMPTY;
2479
2480        @Override
2481        public void writeToParcel(Parcel out, int flags) {
2482            out.writeInt(index);
2483            out.writeBundle(childStates);
2484        }
2485
2486        @SuppressWarnings("hiding")
2487        public static final Parcelable.Creator<SavedState> CREATOR =
2488                new Parcelable.Creator<SavedState>() {
2489                    @Override
2490                    public SavedState createFromParcel(Parcel in) {
2491                        return new SavedState(in);
2492                    }
2493
2494                    @Override
2495                    public SavedState[] newArray(int size) {
2496                        return new SavedState[size];
2497                    }
2498                };
2499
2500        @Override
2501        public int describeContents() {
2502            return 0;
2503        }
2504
2505        SavedState(Parcel in) {
2506            index = in.readInt();
2507            childStates = in.readBundle(GridLayoutManager.class.getClassLoader());
2508        }
2509
2510        SavedState() {
2511        }
2512    }
2513
2514    @Override
2515    public Parcelable onSaveInstanceState() {
2516        if (DEBUG) Log.v(getTag(), "onSaveInstanceState getSelection() " + getSelection());
2517        SavedState ss = new SavedState();
2518        for (int i = 0, count = getChildCount(); i < count; i++) {
2519            View view = getChildAt(i);
2520            int position = getPositionByView(view);
2521            if (position != NO_POSITION) {
2522                mChildrenStates.saveOnScreenView(view, position);
2523            }
2524        }
2525        ss.index = getSelection();
2526        ss.childStates = mChildrenStates.saveAsBundle();
2527        return ss;
2528    }
2529
2530    void onChildDetachedFromWindow(RecyclerView.ViewHolder holder) {
2531     // FIXME: use getAdapterPosition() once RV can return position after view is detached.
2532        final int position = holder.getLayoutPosition();
2533        if (position != NO_POSITION) {
2534            mChildrenStates.saveOffscreenView(holder.itemView, position);
2535        }
2536    }
2537
2538    @Override
2539    public void onRestoreInstanceState(Parcelable state) {
2540        if (!(state instanceof SavedState)) {
2541            return;
2542        }
2543        SavedState loadingState = (SavedState)state;
2544        mFocusPosition = loadingState.index;
2545        mFocusPositionOffset = 0;
2546        mChildrenStates.loadFromBundle(loadingState.childStates);
2547        mForceFullLayout = true;
2548        requestLayout();
2549        if (DEBUG) Log.v(getTag(), "onRestoreInstanceState mFocusPosition " + mFocusPosition);
2550    }
2551}
2552