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