GridLayoutManager.java revision 57070ccd71477252474e3c4d35edddbc859c6179
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        appendVisibleItems();
1420        prependVisibleItems();
1421
1422        updateRowsMinMax();
1423        updateScrollMin();
1424        updateScrollMax();
1425        updateScrollSecondAxis();
1426
1427        if (mFocusScrollStrategy == BaseGridView.FOCUS_SCROLL_ALIGNED) {
1428            View focusView = getViewByPosition(mFocusPosition == NO_POSITION ? 0 : mFocusPosition);
1429            scrollToView(focusView, false);
1430        }
1431    }
1432
1433    // Lays out items based on the current scroll position
1434    @Override
1435    public void onLayoutChildren(RecyclerView.Adapter adapter, RecyclerView.Recycler recycler,
1436            boolean structureChanged, RecyclerView.State state) {
1437        if (DEBUG) {
1438            Log.v(getTag(), "layoutChildren start numRows " + mNumRows + " mScrollOffsetSecondary "
1439                    + mScrollOffsetSecondary + " mScrollOffsetPrimary " + mScrollOffsetPrimary
1440                    + " structureChanged " + structureChanged
1441                    + " mForceFullLayout " + mForceFullLayout);
1442            Log.v(getTag(), "width " + getWidth() + " height " + getHeight());
1443        }
1444
1445        if (mNumRows == 0) {
1446            // haven't done measure yet
1447            return;
1448        }
1449        final int itemCount = adapter.getItemCount();
1450        if (itemCount < 0) {
1451            return;
1452        }
1453
1454        mInLayout = true;
1455
1456        // Track the old focus view so we can adjust our system scroll position
1457        // so that any scroll animations happening now will remain valid.
1458        int delta = 0, deltaSecondary = 0;
1459        if (mFocusPosition != NO_POSITION
1460                && mFocusScrollStrategy == BaseGridView.FOCUS_SCROLL_ALIGNED) {
1461            View focusView = getViewByPosition(mFocusPosition);
1462            if (focusView != null) {
1463                delta = mWindowAlignment.mainAxis().getSystemScrollPos(
1464                        getViewCenter(focusView) + mScrollOffsetPrimary) - mScrollOffsetPrimary;
1465                deltaSecondary =
1466                    mWindowAlignment.secondAxis().getSystemScrollPos(
1467                            getViewCenterSecondary(focusView) + mScrollOffsetSecondary)
1468                    - mScrollOffsetSecondary;
1469            }
1470        }
1471
1472        boolean hasDoneFirstLayout = hasDoneFirstLayout();
1473        if (!structureChanged && !mForceFullLayout && hasDoneFirstLayout) {
1474            fastRelayout();
1475        } else {
1476            boolean hadFocus = mBaseGridView.hasFocus();
1477
1478            int newFocusPosition = init(adapter, recycler, mFocusPosition);
1479            if (DEBUG) {
1480                Log.v(getTag(), "mFocusPosition " + mFocusPosition + " newFocusPosition "
1481                    + newFocusPosition);
1482            }
1483
1484            // depending on result of init(), either recreating everything
1485            // or try to reuse the row start positions near mFocusPosition
1486            if (mGrid.getSize() == 0) {
1487                // this is a fresh creating all items, starting from
1488                // mFocusPosition with a estimated row index.
1489                mGrid.setStart(newFocusPosition, StaggeredGrid.START_DEFAULT);
1490
1491                // Can't track the old focus view
1492                delta = deltaSecondary = 0;
1493
1494            } else {
1495                // mGrid remembers Locations for the column that
1496                // contains mFocusePosition and also mRows remembers start
1497                // positions of each row.
1498                // Manually re-create child views for that column
1499                int firstIndex = mGrid.getFirstIndex();
1500                int lastIndex = mGrid.getLastIndex();
1501                for (int i = firstIndex; i <= lastIndex; i++) {
1502                    mGridProvider.createItem(i, mGrid.getLocation(i).row, true);
1503                }
1504            }
1505            // add visible views at end until reach the end of window
1506            appendVisibleItems();
1507            // add visible views at front until reach the start of window
1508            prependVisibleItems();
1509            // multiple rounds: scrollToView of first round may drag first/last child into
1510            // "visible window" and we update scrollMin/scrollMax then run second scrollToView
1511            int oldFirstVisible;
1512            int oldLastVisible;
1513            do {
1514                oldFirstVisible = mFirstVisiblePos;
1515                oldLastVisible = mLastVisiblePos;
1516                View focusView = getViewByPosition(newFocusPosition);
1517                // we need force to initialize the child view's position
1518                scrollToView(focusView, false);
1519                if (focusView != null && hadFocus) {
1520                    focusView.requestFocus();
1521                }
1522                appendVisibleItems();
1523                prependVisibleItems();
1524                removeInvisibleViewsAtFront();
1525                removeInvisibleViewsAtEnd();
1526            } while (mFirstVisiblePos != oldFirstVisible || mLastVisiblePos != oldLastVisible);
1527        }
1528        mForceFullLayout = false;
1529
1530        if (mFocusScrollStrategy == BaseGridView.FOCUS_SCROLL_ALIGNED) {
1531            scrollDirectionPrimary(-delta);
1532            scrollDirectionSecondary(-deltaSecondary);
1533        }
1534        appendVisibleItems();
1535        prependVisibleItems();
1536        removeInvisibleViewsAtFront();
1537        removeInvisibleViewsAtEnd();
1538
1539        if (DEBUG) {
1540            StringWriter sw = new StringWriter();
1541            PrintWriter pw = new PrintWriter(sw);
1542            mGrid.debugPrint(pw);
1543            Log.d(getTag(), sw.toString());
1544        }
1545
1546        removeAndRecycleScrap(recycler);
1547        attemptAnimateLayoutChild();
1548
1549        if (mRowSecondarySizeRefresh) {
1550            mRowSecondarySizeRefresh = false;
1551        } else {
1552            updateRowSecondarySizeRefresh();
1553        }
1554
1555        if (!hasDoneFirstLayout) {
1556            dispatchChildSelected();
1557        }
1558        mInLayout = false;
1559        if (DEBUG) Log.v(getTag(), "layoutChildren end");
1560    }
1561
1562    private void offsetChildrenSecondary(int increment) {
1563        final int childCount = getChildCount();
1564        if (mOrientation == HORIZONTAL) {
1565            for (int i = 0; i < childCount; i++) {
1566                getChildAt(i).offsetTopAndBottom(increment);
1567            }
1568        } else {
1569            for (int i = 0; i < childCount; i++) {
1570                getChildAt(i).offsetLeftAndRight(increment);
1571            }
1572        }
1573        mScrollOffsetSecondary -= increment;
1574    }
1575
1576    private void offsetChildrenPrimary(int increment) {
1577        final int childCount = getChildCount();
1578        if (mOrientation == VERTICAL) {
1579            for (int i = 0; i < childCount; i++) {
1580                getChildAt(i).offsetTopAndBottom(increment);
1581            }
1582        } else {
1583            for (int i = 0; i < childCount; i++) {
1584                getChildAt(i).offsetLeftAndRight(increment);
1585            }
1586        }
1587        mScrollOffsetPrimary -= increment;
1588    }
1589
1590    @Override
1591    public int scrollHorizontallyBy(int dx, Adapter adapter, Recycler recycler,
1592            RecyclerView.State state) {
1593        if (DEBUG) Log.v(TAG, "scrollHorizontallyBy " + dx);
1594
1595        if (mOrientation == HORIZONTAL) {
1596            return scrollDirectionPrimary(dx);
1597        } else {
1598            return scrollDirectionSecondary(dx);
1599        }
1600    }
1601
1602    @Override
1603    public int scrollVerticallyBy(int dy, Adapter adapter, Recycler recycler,
1604            RecyclerView.State state) {
1605        if (DEBUG) Log.v(TAG, "scrollVerticallyBy " + dy);
1606        if (mOrientation == VERTICAL) {
1607            return scrollDirectionPrimary(dy);
1608        } else {
1609            return scrollDirectionSecondary(dy);
1610        }
1611    }
1612
1613    // scroll in main direction may add/prune views
1614    private int scrollDirectionPrimary(int da) {
1615        offsetChildrenPrimary(-da);
1616        if (mInLayout) {
1617            return da;
1618        }
1619
1620        int childCount = getChildCount();
1621        boolean updated;
1622
1623        if (da > 0) {
1624            appendVisibleItems();
1625        } else if (da < 0) {
1626            prependVisibleItems();
1627        }
1628        updated = getChildCount() > childCount;
1629        childCount = getChildCount();
1630
1631        if (da > 0) {
1632            removeInvisibleViewsAtFront();
1633        } else if (da < 0) {
1634            removeInvisibleViewsAtEnd();
1635        }
1636        updated |= getChildCount() < childCount;
1637
1638        attemptAnimateLayoutChild();
1639        if (updated) {
1640            updateRowSecondarySizeRefresh();
1641        }
1642
1643        mBaseGridView.invalidate();
1644        return da;
1645    }
1646
1647    // scroll in second direction will not add/prune views
1648    private int scrollDirectionSecondary(int dy) {
1649        offsetChildrenSecondary(-dy);
1650        mBaseGridView.invalidate();
1651        return dy;
1652    }
1653
1654    private void updateScrollMax() {
1655        if (mLastVisiblePos >= 0 && mLastVisiblePos == mAdapter.getItemCount() - 1) {
1656            int maxEdge = Integer.MIN_VALUE;
1657            for (int i = 0; i < mRows.length; i++) {
1658                if (mRows[i].high > maxEdge) {
1659                    maxEdge = mRows[i].high;
1660                }
1661            }
1662            mWindowAlignment.mainAxis().setMaxEdge(maxEdge);
1663            if (DEBUG) Log.v(getTag(), "updating scroll maxEdge to " + maxEdge);
1664        }
1665    }
1666
1667    private void updateScrollMin() {
1668        if (mFirstVisiblePos == 0) {
1669            int minEdge = Integer.MAX_VALUE;
1670            for (int i = 0; i < mRows.length; i++) {
1671                if (mRows[i].low < minEdge) {
1672                    minEdge = mRows[i].low;
1673                }
1674            }
1675            mWindowAlignment.mainAxis().setMinEdge(minEdge);
1676            if (DEBUG) Log.v(getTag(), "updating scroll minEdge to " + minEdge);
1677        }
1678    }
1679
1680    private void updateScrollSecondAxis() {
1681        mWindowAlignment.secondAxis().setMinEdge(0);
1682        mWindowAlignment.secondAxis().setMaxEdge(getSizeSecondary());
1683    }
1684
1685    private void initScrollController() {
1686        mWindowAlignment.horizontal.setSize(getWidth());
1687        mWindowAlignment.horizontal.setPadding(getPaddingLeft(), getPaddingRight());
1688        mWindowAlignment.vertical.setSize(getHeight());
1689        mWindowAlignment.vertical.setPadding(getPaddingTop(), getPaddingBottom());
1690        mSizePrimary = mWindowAlignment.mainAxis().getSize();
1691        mWindowAlignment.mainAxis().invalidateScrollMin();
1692        mWindowAlignment.mainAxis().invalidateScrollMax();
1693
1694        if (DEBUG) {
1695            Log.v(getTag(), "initScrollController mSizePrimary " + mSizePrimary
1696                    + " mWindowAlignment " + mWindowAlignment);
1697        }
1698    }
1699
1700    public void setSelection(RecyclerView parent, int position) {
1701        setSelection(parent, position, false);
1702    }
1703
1704    public void setSelectionSmooth(RecyclerView parent, int position) {
1705        setSelection(parent, position, true);
1706    }
1707
1708    public int getSelection() {
1709        return mFocusPosition;
1710    }
1711
1712    public void setSelection(RecyclerView parent, int position, boolean smooth) {
1713        if (mFocusPosition == position) {
1714            return;
1715        }
1716        View view = getViewByPosition(position);
1717        if (view != null) {
1718            scrollToView(view, smooth);
1719        } else {
1720            boolean right = position > mFocusPosition;
1721            mFocusPosition = position;
1722            if (smooth) {
1723                if (!hasDoneFirstLayout()) {
1724                    Log.w(getTag(), "setSelectionSmooth should " +
1725                            "not be called before first layout pass");
1726                    return;
1727                }
1728                if (right) {
1729                    appendVisibleItems();
1730                } else {
1731                    prependVisibleItems();
1732                }
1733                scrollToView(getViewByPosition(position), smooth);
1734            } else {
1735                mForceFullLayout = true;
1736                parent.requestLayout();
1737            }
1738        }
1739    }
1740
1741    @Override
1742    public void onItemsAdded(RecyclerView recyclerView, int positionStart, int itemCount) {
1743        boolean needsLayout = false;
1744        if (itemCount != 0) {
1745            if (mFirstVisiblePos < 0) {
1746                needsLayout = true;
1747            } else if (!(positionStart > mLastVisiblePos + 1 ||
1748                    positionStart + itemCount < mFirstVisiblePos - 1)) {
1749                needsLayout = true;
1750            }
1751        }
1752        if (needsLayout) {
1753            recyclerView.requestLayout();
1754        }
1755    }
1756
1757    @Override
1758    public boolean onRequestChildFocus(RecyclerView parent, View child, View focused) {
1759        if (mFocusSearchDisabled) {
1760            return true;
1761        }
1762        if (!mInLayout) {
1763            scrollToView(child, true);
1764        }
1765        return true;
1766    }
1767
1768    @Override
1769    public boolean requestChildRectangleOnScreen(RecyclerView parent, View view, Rect rect,
1770            boolean immediate) {
1771        if (DEBUG) Log.v(getTag(), "requestChildRectangleOnScreen " + view + " " + rect);
1772        return false;
1773    }
1774
1775    int getScrollOffsetX() {
1776        return mOrientation == HORIZONTAL ? mScrollOffsetPrimary : mScrollOffsetSecondary;
1777    }
1778
1779    int getScrollOffsetY() {
1780        return mOrientation == HORIZONTAL ? mScrollOffsetSecondary : mScrollOffsetPrimary;
1781    }
1782
1783    public void getViewSelectedOffsets(View view, int[] offsets) {
1784        int scrollOffsetX = getScrollOffsetX();
1785        int scrollOffsetY = getScrollOffsetY();
1786        int viewCenterX = scrollOffsetX + getViewCenterX(view);
1787        int viewCenterY = scrollOffsetY + getViewCenterY(view);
1788        offsets[0] = mWindowAlignment.horizontal.getSystemScrollPos(viewCenterX) - scrollOffsetX;
1789        offsets[1] = mWindowAlignment.vertical.getSystemScrollPos(viewCenterY) - scrollOffsetY;
1790    }
1791
1792    /**
1793     * Scroll to a given child view and change mFocusPosition.
1794     */
1795    private void scrollToView(View view, boolean smooth) {
1796        int newFocusPosition = getPositionByView(view);
1797        if (mInLayout || newFocusPosition != mFocusPosition) {
1798            mFocusPosition = newFocusPosition;
1799            dispatchChildSelected();
1800        }
1801        if (mBaseGridView.isChildrenDrawingOrderEnabledInternal()) {
1802            mBaseGridView.invalidate();
1803        }
1804        if (view == null) {
1805            return;
1806        }
1807        if (!view.hasFocus() && mBaseGridView.hasFocus()) {
1808            // transfer focus to the child if it does not have focus yet (e.g. triggered
1809            // by setSelection())
1810            view.requestFocus();
1811        }
1812        switch (mFocusScrollStrategy) {
1813        case BaseGridView.FOCUS_SCROLL_ALIGNED:
1814        default:
1815            scrollToAlignedPosition(view, smooth);
1816            break;
1817        case BaseGridView.FOCUS_SCROLL_ITEM:
1818        case BaseGridView.FOCUS_SCROLL_PAGE:
1819            scrollItemOrPage(view, smooth);
1820            break;
1821        }
1822    }
1823
1824    private void scrollItemOrPage(View view, boolean smooth) {
1825        int pos = getPositionByView(view);
1826        int viewMin = getViewMin(view);
1827        int viewMax = getViewMax(view);
1828        // we either align "firstView" to left/top padding edge
1829        // or align "lastView" to right/bottom padding edge
1830        View firstView = null;
1831        View lastView = null;
1832        int paddingLow = mWindowAlignment.mainAxis().getPaddingLow();
1833        int clientSize = mWindowAlignment.mainAxis().getClientSize();
1834        final int row = mGrid.getLocation(pos).row;
1835        if (viewMin < paddingLow) {
1836            // view enters low padding area:
1837            firstView = view;
1838            if (mFocusScrollStrategy == BaseGridView.FOCUS_SCROLL_PAGE) {
1839                // scroll one "page" left/top,
1840                // align first visible item of the "page" at the low padding edge.
1841                while (!prependOneVisibleItem()) {
1842                    List<Integer> positions =
1843                            mGrid.getItemPositionsInRows(mFirstVisiblePos, pos)[row];
1844                    firstView = getViewByPosition(positions.get(0));
1845                    if (viewMax - getViewMin(firstView) > clientSize) {
1846                        if (positions.size() > 1) {
1847                            firstView = getViewByPosition(positions.get(1));
1848                        }
1849                        break;
1850                    }
1851                }
1852            }
1853        } else if (viewMax > clientSize + paddingLow) {
1854            // view enters high padding area:
1855            if (mFocusScrollStrategy == BaseGridView.FOCUS_SCROLL_PAGE) {
1856                // scroll whole one page right/bottom, align view at the low padding edge.
1857                firstView = view;
1858                do {
1859                    List<Integer> positions =
1860                            mGrid.getItemPositionsInRows(pos, mLastVisiblePos)[row];
1861                    lastView = getViewByPosition(positions.get(positions.size() - 1));
1862                    if (getViewMax(lastView) - viewMin > clientSize) {
1863                        lastView = null;
1864                        break;
1865                    }
1866                } while (!appendOneVisibleItem());
1867                if (lastView != null) {
1868                    // however if we reached end,  we should align last view.
1869                    firstView = null;
1870                }
1871            } else {
1872                lastView = view;
1873            }
1874        }
1875        int scrollPrimary = 0;
1876        int scrollSecondary = 0;
1877        if (firstView != null) {
1878            scrollPrimary = getViewMin(firstView) - paddingLow;
1879        } else if (lastView != null) {
1880            scrollPrimary = getViewMax(lastView) - (paddingLow + clientSize);
1881        }
1882        View secondaryAlignedView;
1883        if (firstView != null) {
1884            secondaryAlignedView = firstView;
1885        } else if (lastView != null) {
1886            secondaryAlignedView = lastView;
1887        } else {
1888            secondaryAlignedView = view;
1889        }
1890        int viewCenterSecondary = mScrollOffsetSecondary +
1891                getViewCenterSecondary(secondaryAlignedView);
1892        mWindowAlignment.secondAxis().updateScrollCenter(viewCenterSecondary);
1893        scrollSecondary = mWindowAlignment.secondAxis().getSystemScrollPos();
1894        scrollSecondary -= mScrollOffsetSecondary;
1895        scrollGrid(scrollPrimary, scrollSecondary, smooth);
1896    }
1897
1898    private void scrollToAlignedPosition(View view, boolean smooth) {
1899        int viewCenterPrimary = mScrollOffsetPrimary + getViewCenter(view);
1900        int viewCenterSecondary = mScrollOffsetSecondary + getViewCenterSecondary(view);
1901        if (DEBUG) {
1902            Log.v(getTag(), "scrollAligned smooth=" + smooth + " pos=" + mFocusPosition + " "
1903                    + viewCenterPrimary +","+viewCenterSecondary + " " + mWindowAlignment);
1904        }
1905
1906        if (mInLayout || viewCenterPrimary != mWindowAlignment.mainAxis().getScrollCenter()
1907                || viewCenterSecondary != mWindowAlignment.secondAxis().getScrollCenter()) {
1908            mWindowAlignment.mainAxis().updateScrollCenter(viewCenterPrimary);
1909            mWindowAlignment.secondAxis().updateScrollCenter(viewCenterSecondary);
1910            int scrollPrimary = mWindowAlignment.mainAxis().getSystemScrollPos();
1911            int scrollSecondary = mWindowAlignment.secondAxis().getSystemScrollPos();
1912            if (DEBUG) {
1913                Log.v(getTag(), "scrollAligned " + scrollPrimary + " " + scrollSecondary
1914                        +" " + mWindowAlignment);
1915            }
1916
1917            scrollPrimary -= mScrollOffsetPrimary;
1918            scrollSecondary -= mScrollOffsetSecondary;
1919
1920            scrollGrid(scrollPrimary, scrollSecondary, smooth);
1921        }
1922    }
1923
1924    private void scrollGrid(int scrollPrimary, int scrollSecondary, boolean smooth) {
1925        if (mInLayout) {
1926            scrollDirectionPrimary(scrollPrimary);
1927            scrollDirectionSecondary(scrollSecondary);
1928        } else {
1929            int scrollX;
1930            int scrollY;
1931            if (mOrientation == HORIZONTAL) {
1932                scrollX = scrollPrimary;
1933                scrollY = scrollSecondary;
1934            } else {
1935                scrollX = scrollSecondary;
1936                scrollY = scrollPrimary;
1937            }
1938            if (smooth) {
1939                mBaseGridView.smoothScrollBy(scrollX, scrollY);
1940            } else {
1941                mBaseGridView.scrollBy(scrollX, scrollY);
1942            }
1943        }
1944    }
1945
1946    public void setAnimateChildLayout(boolean animateChildLayout) {
1947        mAnimateChildLayout = animateChildLayout;
1948        for (int i = 0, c = getChildCount(); i < c; i++) {
1949            View v = getChildAt(i);
1950            LayoutParams p = (LayoutParams) v.getLayoutParams();
1951            if (!mAnimateChildLayout) {
1952                p.endAnimate();
1953            } else {
1954                // record initial location values
1955                p.mFirstAttached = true;
1956                p.startAnimate(this, v, 0);
1957            }
1958        }
1959    }
1960
1961    private void attemptAnimateLayoutChild() {
1962        if (!mAnimateChildLayout) {
1963            return;
1964        }
1965        for (int i = 0, c = getChildCount(); i < c; i++) {
1966            // TODO: start delay can be staggered
1967            View v = getChildAt(i);
1968            ((LayoutParams) v.getLayoutParams()).startAnimate(this, v, 0);
1969        }
1970    }
1971
1972    public boolean isChildLayoutAnimated() {
1973        return mAnimateChildLayout;
1974    }
1975
1976    public void setChildLayoutAnimationInterpolator(Interpolator interpolator) {
1977        mAnimateLayoutChildInterpolator = interpolator;
1978    }
1979
1980    public Interpolator getChildLayoutAnimationInterpolator() {
1981        return mAnimateLayoutChildInterpolator;
1982    }
1983
1984    public void setChildLayoutAnimationDuration(long duration) {
1985        mAnimateLayoutChildDuration = duration;
1986    }
1987
1988    public long getChildLayoutAnimationDuration() {
1989        return mAnimateLayoutChildDuration;
1990    }
1991
1992    private int findImmediateChildIndex(View view) {
1993        while (view != null && view != mBaseGridView) {
1994            int index = mBaseGridView.indexOfChild(view);
1995            if (index >= 0) {
1996                return index;
1997            }
1998            view = (View) view.getParent();
1999        }
2000        return NO_POSITION;
2001    }
2002
2003    void setFocusSearchDisabled(boolean disabled) {
2004        mFocusSearchDisabled = disabled;
2005    }
2006
2007    boolean isFocusSearchDisabled() {
2008        return mFocusSearchDisabled;
2009    }
2010
2011    @Override
2012    public View onInterceptFocusSearch(View focused, int direction) {
2013        if (mFocusSearchDisabled) {
2014            return focused;
2015        }
2016        return null;
2017    }
2018
2019    boolean hasNextViewInSameRow(int pos) {
2020        if (mGrid == null || pos == NO_POSITION) {
2021            return false;
2022        }
2023        final int focusedRow = mGrid.getLocation(pos).row;
2024        for (int i = 0, count = getChildCount(); i < count; i++) {
2025            int position = getPositionByIndex(i);
2026            StaggeredGrid.Location loc = mGrid.getLocation(position);
2027            if (loc != null && loc.row == focusedRow) {
2028                if (position > pos) {
2029                    return true;
2030                }
2031            }
2032        }
2033        return false;
2034    }
2035
2036    boolean hasPreviousViewInSameRow(int pos) {
2037        if (mGrid == null || pos == NO_POSITION) {
2038            return false;
2039        }
2040        final int focusedRow = mGrid.getLocation(pos).row;
2041        for (int i = getChildCount() - 1; i >= 0; i--) {
2042            int position = getPositionByIndex(i);
2043            StaggeredGrid.Location loc = mGrid.getLocation(position);
2044            if (loc != null && loc.row == focusedRow) {
2045                if (position < pos) {
2046                    return true;
2047                }
2048            }
2049        }
2050        return false;
2051    }
2052
2053    @Override
2054    public boolean onAddFocusables(RecyclerView recyclerView,
2055            ArrayList<View> views, int direction, int focusableMode) {
2056        if (mFocusSearchDisabled) {
2057            return true;
2058        }
2059        // If this viewgroup or one of its children currently has focus then we
2060        // consider our children for focus searching in main direction on the same row.
2061        // If this viewgroup has no focus and using focus align, we want the system
2062        // to ignore our children and pass focus to the viewgroup, which will pass
2063        // focus on to its children appropriately.
2064        // If this viewgroup has no focus and not using focus align, we want to
2065        // consider the child that does not overlap with padding area.
2066        if (recyclerView.hasFocus()) {
2067            final int movement = getMovement(direction);
2068            if (movement != PREV_ITEM && movement != NEXT_ITEM) {
2069                // Move on secondary direction uses default addFocusables().
2070                return false;
2071            }
2072            final View focused = recyclerView.findFocus();
2073            final int focusedPos = getPositionByIndex(findImmediateChildIndex(focused));
2074            // Add focusables of focused item.
2075            if (focusedPos != NO_POSITION) {
2076                getViewByPosition(focusedPos).addFocusables(views,  direction, focusableMode);
2077            }
2078            final int focusedRow = mGrid != null && focusedPos != NO_POSITION ?
2079                    mGrid.getLocation(focusedPos).row : NO_POSITION;
2080            // Add focusables of next neighbor of same row on the focus search direction.
2081            if (mGrid != null) {
2082                final int focusableCount = views.size();
2083                for (int i = 0, count = getChildCount(); i < count; i++) {
2084                    int index = movement == NEXT_ITEM ? i : count - 1 - i;
2085                    final View child = getChildAt(index);
2086                    if (child.getVisibility() != View.VISIBLE) {
2087                        continue;
2088                    }
2089                    int position = getPositionByIndex(index);
2090                    StaggeredGrid.Location loc = mGrid.getLocation(position);
2091                    if (focusedRow == NO_POSITION || (loc != null && loc.row == focusedRow)) {
2092                        if (focusedPos == NO_POSITION ||
2093                                (movement == NEXT_ITEM && position > focusedPos)
2094                                || (movement == PREV_ITEM && position < focusedPos)) {
2095                            child.addFocusables(views,  direction, focusableMode);
2096                            if (views.size() > focusableCount) {
2097                                break;
2098                            }
2099                        }
2100                    }
2101                }
2102            }
2103        } else {
2104            if (mFocusScrollStrategy != BaseGridView.FOCUS_SCROLL_ALIGNED) {
2105                // adding views not overlapping padding area to avoid scrolling in gaining focus
2106                int left = mWindowAlignment.mainAxis().getPaddingLow();
2107                int right = mWindowAlignment.mainAxis().getClientSize() + left;
2108                int focusableCount = views.size();
2109                for (int i = 0, count = getChildCount(); i < count; i++) {
2110                    View child = getChildAt(i);
2111                    if (child.getVisibility() == View.VISIBLE) {
2112                        if (getViewMin(child) >= left && getViewMax(child) <= right) {
2113                            child.addFocusables(views, direction, focusableMode);
2114                        }
2115                    }
2116                }
2117                // if we cannot find any, then just add all children.
2118                if (views.size() == focusableCount) {
2119                    for (int i = 0, count = getChildCount(); i < count; i++) {
2120                        View child = getChildAt(i);
2121                        if (child.getVisibility() == View.VISIBLE) {
2122                            child.addFocusables(views, direction, focusableMode);
2123                        }
2124                    }
2125                    if (views.size() != focusableCount) {
2126                        return true;
2127                    }
2128                } else {
2129                    return true;
2130                }
2131                // if still cannot find any, fall through and add itself
2132            }
2133            if (recyclerView.isFocusable()) {
2134                views.add(recyclerView);
2135            }
2136        }
2137        return true;
2138    }
2139
2140    @Override
2141    public View onFocusSearchFailed(View focused, int direction, Adapter adapter,
2142            Recycler recycler) {
2143        if (DEBUG) Log.v(getTag(), "onFocusSearchFailed direction " + direction);
2144
2145        View view = null;
2146        int movement = getMovement(direction);
2147        final FocusFinder ff = FocusFinder.getInstance();
2148        if (movement == NEXT_ITEM) {
2149            while (view == null && !appendOneVisibleItem()) {
2150                view = ff.findNextFocus(mBaseGridView, focused, direction);
2151            }
2152        } else if (movement == PREV_ITEM){
2153            while (view == null && !prependOneVisibleItem()) {
2154                view = ff.findNextFocus(mBaseGridView, focused, direction);
2155            }
2156        }
2157        if (view == null) {
2158            // returning the same view to prevent focus lost when scrolling past the end of the list
2159            if (movement == PREV_ITEM) {
2160                view = mFocusOutFront ? null : focused;
2161            } else if (movement == NEXT_ITEM){
2162                view = mFocusOutEnd ? null : focused;
2163            }
2164        }
2165        if (DEBUG) Log.v(getTag(), "returning view " + view);
2166        return view;
2167    }
2168
2169    boolean gridOnRequestFocusInDescendants(RecyclerView recyclerView, int direction,
2170            Rect previouslyFocusedRect) {
2171        switch (mFocusScrollStrategy) {
2172        case BaseGridView.FOCUS_SCROLL_ALIGNED:
2173        default:
2174            return gridOnRequestFocusInDescendantsAligned(recyclerView,
2175                    direction, previouslyFocusedRect);
2176        case BaseGridView.FOCUS_SCROLL_PAGE:
2177        case BaseGridView.FOCUS_SCROLL_ITEM:
2178            return gridOnRequestFocusInDescendantsUnaligned(recyclerView,
2179                    direction, previouslyFocusedRect);
2180        }
2181    }
2182
2183    private boolean gridOnRequestFocusInDescendantsAligned(RecyclerView recyclerView,
2184            int direction, Rect previouslyFocusedRect) {
2185        View view = getViewByPosition(mFocusPosition);
2186        if (view != null) {
2187            boolean result = view.requestFocus(direction, previouslyFocusedRect);
2188            if (!result && DEBUG) {
2189                Log.w(getTag(), "failed to request focus on " + view);
2190            }
2191            return result;
2192        }
2193        return false;
2194    }
2195
2196    private boolean gridOnRequestFocusInDescendantsUnaligned(RecyclerView recyclerView,
2197            int direction, Rect previouslyFocusedRect) {
2198        // focus to view not overlapping padding area to avoid scrolling in gaining focus
2199        int index;
2200        int increment;
2201        int end;
2202        int count = getChildCount();
2203        if ((direction & View.FOCUS_FORWARD) != 0) {
2204            index = 0;
2205            increment = 1;
2206            end = count;
2207        } else {
2208            index = count - 1;
2209            increment = -1;
2210            end = -1;
2211        }
2212        int left = mWindowAlignment.mainAxis().getPaddingLow();
2213        int right = mWindowAlignment.mainAxis().getClientSize() + left;
2214        for (int i = index; i != end; i += increment) {
2215            View child = getChildAt(i);
2216            if (child.getVisibility() == View.VISIBLE) {
2217                if (getViewMin(child) >= left && getViewMax(child) <= right) {
2218                    if (child.requestFocus(direction, previouslyFocusedRect)) {
2219                        return true;
2220                    }
2221                }
2222            }
2223        }
2224        return false;
2225    }
2226
2227    private final static int PREV_ITEM = 0;
2228    private final static int NEXT_ITEM = 1;
2229    private final static int PREV_ROW = 2;
2230    private final static int NEXT_ROW = 3;
2231
2232    private int getMovement(int direction) {
2233        int movement = View.FOCUS_LEFT;
2234
2235        if (mOrientation == HORIZONTAL) {
2236            switch(direction) {
2237                case View.FOCUS_LEFT:
2238                    movement = PREV_ITEM;
2239                    break;
2240                case View.FOCUS_RIGHT:
2241                    movement = NEXT_ITEM;
2242                    break;
2243                case View.FOCUS_UP:
2244                    movement = PREV_ROW;
2245                    break;
2246                case View.FOCUS_DOWN:
2247                    movement = NEXT_ROW;
2248                    break;
2249            }
2250         } else if (mOrientation == VERTICAL) {
2251             switch(direction) {
2252                 case View.FOCUS_LEFT:
2253                     movement = PREV_ROW;
2254                     break;
2255                 case View.FOCUS_RIGHT:
2256                     movement = NEXT_ROW;
2257                     break;
2258                 case View.FOCUS_UP:
2259                     movement = PREV_ITEM;
2260                     break;
2261                 case View.FOCUS_DOWN:
2262                     movement = NEXT_ITEM;
2263                     break;
2264             }
2265         }
2266
2267        return movement;
2268    }
2269
2270    int getChildDrawingOrder(RecyclerView recyclerView, int childCount, int i) {
2271        int focusIndex = getIndexByPosition(mFocusPosition);
2272        if (focusIndex == NO_POSITION) {
2273            return i;
2274        }
2275        // supposely 0 1 2 3 4 5 6 7 8 9, 4 is the center item
2276        // drawing order is 0 1 2 3 9 8 7 6 5 4
2277        if (i < focusIndex) {
2278            return i;
2279        } else if (i < childCount - 1) {
2280            return focusIndex + childCount - 1 - i;
2281        } else {
2282            return focusIndex;
2283        }
2284    }
2285
2286    @Override
2287    public void onAdapterChanged(RecyclerView.Adapter oldAdapter,
2288            RecyclerView.Adapter newAdapter) {
2289        mGrid = null;
2290        mRows = null;
2291        mRowSizeSecondary = null;
2292        mRowSecondarySizeRefresh = false;
2293        super.onAdapterChanged(oldAdapter, newAdapter);
2294    }
2295}
2296