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