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