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