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