GridLayoutManager.java revision 9c0d20dbcdf7afc18f429ad54a2250f82bd85841
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, Recycler recycler, RecyclerView.State state) {
1642        if (DEBUG) Log.v(TAG, "scrollHorizontallyBy " + dx);
1643
1644        if (mOrientation == HORIZONTAL) {
1645            return scrollDirectionPrimary(dx);
1646        } else {
1647            return scrollDirectionSecondary(dx);
1648        }
1649    }
1650
1651    @Override
1652    public int scrollVerticallyBy(int dy, Recycler recycler, RecyclerView.State state) {
1653        if (DEBUG) Log.v(TAG, "scrollVerticallyBy " + dy);
1654        if (mOrientation == VERTICAL) {
1655            return scrollDirectionPrimary(dy);
1656        } else {
1657            return scrollDirectionSecondary(dy);
1658        }
1659    }
1660
1661    // scroll in main direction may add/prune views
1662    private int scrollDirectionPrimary(int da) {
1663        offsetChildrenPrimary(-da);
1664        if (mInLayout) {
1665            return da;
1666        }
1667
1668        int childCount = getChildCount();
1669        boolean updated;
1670
1671        if (da > 0) {
1672            appendVisibleItems();
1673        } else if (da < 0) {
1674            prependVisibleItems();
1675        }
1676        updated = getChildCount() > childCount;
1677        childCount = getChildCount();
1678
1679        if (da > 0) {
1680            removeInvisibleViewsAtFront();
1681        } else if (da < 0) {
1682            removeInvisibleViewsAtEnd();
1683        }
1684        updated |= getChildCount() < childCount;
1685
1686        if (updated) {
1687            updateRowSecondarySizeRefresh();
1688        }
1689
1690        mBaseGridView.invalidate();
1691        return da;
1692    }
1693
1694    // scroll in second direction will not add/prune views
1695    private int scrollDirectionSecondary(int dy) {
1696        offsetChildrenSecondary(-dy);
1697        mBaseGridView.invalidate();
1698        return dy;
1699    }
1700
1701    private void updateScrollMax() {
1702        if (mLastVisiblePos >= 0 && mLastVisiblePos == mAdapter.getItemCount() - 1) {
1703            int maxEdge = Integer.MIN_VALUE;
1704            for (int i = 0; i < mRows.length; i++) {
1705                if (mRows[i].high > maxEdge) {
1706                    maxEdge = mRows[i].high;
1707                }
1708            }
1709            mWindowAlignment.mainAxis().setMaxEdge(maxEdge);
1710            if (DEBUG) Log.v(getTag(), "updating scroll maxEdge to " + maxEdge);
1711        }
1712    }
1713
1714    private void updateScrollMin() {
1715        if (mFirstVisiblePos == 0) {
1716            int minEdge = Integer.MAX_VALUE;
1717            for (int i = 0; i < mRows.length; i++) {
1718                if (mRows[i].low < minEdge) {
1719                    minEdge = mRows[i].low;
1720                }
1721            }
1722            mWindowAlignment.mainAxis().setMinEdge(minEdge);
1723            if (DEBUG) Log.v(getTag(), "updating scroll minEdge to " + minEdge);
1724        }
1725    }
1726
1727    private void updateScrollSecondAxis() {
1728        mWindowAlignment.secondAxis().setMinEdge(0);
1729        mWindowAlignment.secondAxis().setMaxEdge(getSizeSecondary());
1730    }
1731
1732    private void initScrollController() {
1733        mWindowAlignment.horizontal.setSize(getWidth());
1734        mWindowAlignment.horizontal.setPadding(getPaddingLeft(), getPaddingRight());
1735        mWindowAlignment.vertical.setSize(getHeight());
1736        mWindowAlignment.vertical.setPadding(getPaddingTop(), getPaddingBottom());
1737        mSizePrimary = mWindowAlignment.mainAxis().getSize();
1738        mWindowAlignment.mainAxis().invalidateScrollMin();
1739        mWindowAlignment.mainAxis().invalidateScrollMax();
1740
1741        if (DEBUG) {
1742            Log.v(getTag(), "initScrollController mSizePrimary " + mSizePrimary
1743                    + " mWindowAlignment " + mWindowAlignment);
1744        }
1745    }
1746
1747    public void setSelection(RecyclerView parent, int position) {
1748        setSelection(parent, position, false);
1749    }
1750
1751    public void setSelectionSmooth(RecyclerView parent, int position) {
1752        setSelection(parent, position, true);
1753    }
1754
1755    public int getSelection() {
1756        return mFocusPosition;
1757    }
1758
1759    public void setSelection(RecyclerView parent, int position, boolean smooth) {
1760        if (mFocusPosition == position) {
1761            return;
1762        }
1763        View view = getViewByPosition(position);
1764        if (view != null) {
1765            scrollToView(view, smooth);
1766        } else {
1767            boolean right = position > mFocusPosition;
1768            mFocusPosition = position;
1769            if (!mLayoutEnabled) {
1770                return;
1771            }
1772            if (smooth) {
1773                if (!hasDoneFirstLayout()) {
1774                    Log.w(getTag(), "setSelectionSmooth should " +
1775                            "not be called before first layout pass");
1776                    return;
1777                }
1778                if (right) {
1779                    appendVisibleItems();
1780                } else {
1781                    prependVisibleItems();
1782                }
1783                scrollToView(getViewByPosition(position), smooth);
1784            } else {
1785                mForceFullLayout = true;
1786                parent.requestLayout();
1787            }
1788        }
1789    }
1790
1791    @Override
1792    public void onItemsAdded(RecyclerView recyclerView, int positionStart, int itemCount) {
1793        boolean needsLayout = false;
1794        if (itemCount != 0) {
1795            if (mFirstVisiblePos < 0) {
1796                needsLayout = true;
1797            } else if (!(positionStart > mLastVisiblePos + 1 ||
1798                    positionStart + itemCount < mFirstVisiblePos - 1)) {
1799                needsLayout = true;
1800            }
1801        }
1802        if (needsLayout) {
1803            recyclerView.requestLayout();
1804        }
1805    }
1806
1807    @Override
1808    public boolean onRequestChildFocus(RecyclerView parent, View child, View focused) {
1809        if (mFocusSearchDisabled) {
1810            return true;
1811        }
1812        if (!mInLayout) {
1813            scrollToView(child, true);
1814        }
1815        return true;
1816    }
1817
1818    @Override
1819    public boolean requestChildRectangleOnScreen(RecyclerView parent, View view, Rect rect,
1820            boolean immediate) {
1821        if (DEBUG) Log.v(getTag(), "requestChildRectangleOnScreen " + view + " " + rect);
1822        return false;
1823    }
1824
1825    int getScrollOffsetX() {
1826        return mOrientation == HORIZONTAL ? mScrollOffsetPrimary : mScrollOffsetSecondary;
1827    }
1828
1829    int getScrollOffsetY() {
1830        return mOrientation == HORIZONTAL ? mScrollOffsetSecondary : mScrollOffsetPrimary;
1831    }
1832
1833    public void getViewSelectedOffsets(View view, int[] offsets) {
1834        int scrollOffsetX = getScrollOffsetX();
1835        int scrollOffsetY = getScrollOffsetY();
1836        int viewCenterX = scrollOffsetX + getViewCenterX(view);
1837        int viewCenterY = scrollOffsetY + getViewCenterY(view);
1838        offsets[0] = mWindowAlignment.horizontal.getSystemScrollPos(viewCenterX) - scrollOffsetX;
1839        offsets[1] = mWindowAlignment.vertical.getSystemScrollPos(viewCenterY) - scrollOffsetY;
1840    }
1841
1842    /**
1843     * Scroll to a given child view and change mFocusPosition.
1844     */
1845    private void scrollToView(View view, boolean smooth) {
1846        int newFocusPosition = getPositionByView(view);
1847        if (mInLayout || newFocusPosition != mFocusPosition) {
1848            mFocusPosition = newFocusPosition;
1849            dispatchChildSelected();
1850        }
1851        if (mBaseGridView.isChildrenDrawingOrderEnabledInternal()) {
1852            mBaseGridView.invalidate();
1853        }
1854        if (view == null) {
1855            return;
1856        }
1857        if (!view.hasFocus() && mBaseGridView.hasFocus()) {
1858            // transfer focus to the child if it does not have focus yet (e.g. triggered
1859            // by setSelection())
1860            view.requestFocus();
1861        }
1862        switch (mFocusScrollStrategy) {
1863        case BaseGridView.FOCUS_SCROLL_ALIGNED:
1864        default:
1865            scrollToAlignedPosition(view, smooth);
1866            break;
1867        case BaseGridView.FOCUS_SCROLL_ITEM:
1868        case BaseGridView.FOCUS_SCROLL_PAGE:
1869            scrollItemOrPage(view, smooth);
1870            break;
1871        }
1872    }
1873
1874    private void scrollItemOrPage(View view, boolean smooth) {
1875        int pos = getPositionByView(view);
1876        int viewMin = getViewMin(view);
1877        int viewMax = getViewMax(view);
1878        // we either align "firstView" to left/top padding edge
1879        // or align "lastView" to right/bottom padding edge
1880        View firstView = null;
1881        View lastView = null;
1882        int paddingLow = mWindowAlignment.mainAxis().getPaddingLow();
1883        int clientSize = mWindowAlignment.mainAxis().getClientSize();
1884        final int row = mGrid.getLocation(pos).row;
1885        if (viewMin < paddingLow) {
1886            // view enters low padding area:
1887            firstView = view;
1888            if (mFocusScrollStrategy == BaseGridView.FOCUS_SCROLL_PAGE) {
1889                // scroll one "page" left/top,
1890                // align first visible item of the "page" at the low padding edge.
1891                while (!prependOneVisibleItem()) {
1892                    List<Integer> positions =
1893                            mGrid.getItemPositionsInRows(mFirstVisiblePos, pos)[row];
1894                    firstView = getViewByPosition(positions.get(0));
1895                    if (viewMax - getViewMin(firstView) > clientSize) {
1896                        if (positions.size() > 1) {
1897                            firstView = getViewByPosition(positions.get(1));
1898                        }
1899                        break;
1900                    }
1901                }
1902            }
1903        } else if (viewMax > clientSize + paddingLow) {
1904            // view enters high padding area:
1905            if (mFocusScrollStrategy == BaseGridView.FOCUS_SCROLL_PAGE) {
1906                // scroll whole one page right/bottom, align view at the low padding edge.
1907                firstView = view;
1908                do {
1909                    List<Integer> positions =
1910                            mGrid.getItemPositionsInRows(pos, mLastVisiblePos)[row];
1911                    lastView = getViewByPosition(positions.get(positions.size() - 1));
1912                    if (getViewMax(lastView) - viewMin > clientSize) {
1913                        lastView = null;
1914                        break;
1915                    }
1916                } while (!appendOneVisibleItem());
1917                if (lastView != null) {
1918                    // however if we reached end,  we should align last view.
1919                    firstView = null;
1920                }
1921            } else {
1922                lastView = view;
1923            }
1924        }
1925        int scrollPrimary = 0;
1926        int scrollSecondary = 0;
1927        if (firstView != null) {
1928            scrollPrimary = getViewMin(firstView) - paddingLow;
1929        } else if (lastView != null) {
1930            scrollPrimary = getViewMax(lastView) - (paddingLow + clientSize);
1931        }
1932        View secondaryAlignedView;
1933        if (firstView != null) {
1934            secondaryAlignedView = firstView;
1935        } else if (lastView != null) {
1936            secondaryAlignedView = lastView;
1937        } else {
1938            secondaryAlignedView = view;
1939        }
1940        int viewCenterSecondary = mScrollOffsetSecondary +
1941                getViewCenterSecondary(secondaryAlignedView);
1942        mWindowAlignment.secondAxis().updateScrollCenter(viewCenterSecondary);
1943        scrollSecondary = mWindowAlignment.secondAxis().getSystemScrollPos();
1944        scrollSecondary -= mScrollOffsetSecondary;
1945        scrollGrid(scrollPrimary, scrollSecondary, smooth);
1946    }
1947
1948    private void scrollToAlignedPosition(View view, boolean smooth) {
1949        int viewCenterPrimary = mScrollOffsetPrimary + getViewCenter(view);
1950        int viewCenterSecondary = mScrollOffsetSecondary + getViewCenterSecondary(view);
1951        if (DEBUG) {
1952            Log.v(getTag(), "scrollAligned smooth=" + smooth + " pos=" + mFocusPosition + " "
1953                    + viewCenterPrimary +","+viewCenterSecondary + " " + mWindowAlignment);
1954        }
1955
1956        if (mInLayout || viewCenterPrimary != mWindowAlignment.mainAxis().getScrollCenter()
1957                || viewCenterSecondary != mWindowAlignment.secondAxis().getScrollCenter()) {
1958            mWindowAlignment.mainAxis().updateScrollCenter(viewCenterPrimary);
1959            mWindowAlignment.secondAxis().updateScrollCenter(viewCenterSecondary);
1960            int scrollPrimary = mWindowAlignment.mainAxis().getSystemScrollPos();
1961            int scrollSecondary = mWindowAlignment.secondAxis().getSystemScrollPos();
1962            if (DEBUG) {
1963                Log.v(getTag(), "scrollAligned " + scrollPrimary + " " + scrollSecondary
1964                        +" " + mWindowAlignment);
1965            }
1966
1967            scrollPrimary -= mScrollOffsetPrimary;
1968            scrollSecondary -= mScrollOffsetSecondary;
1969
1970            scrollGrid(scrollPrimary, scrollSecondary, smooth);
1971        }
1972    }
1973
1974    private void scrollGrid(int scrollPrimary, int scrollSecondary, boolean smooth) {
1975        if (mInLayout) {
1976            scrollDirectionPrimary(scrollPrimary);
1977            scrollDirectionSecondary(scrollSecondary);
1978        } else {
1979            int scrollX;
1980            int scrollY;
1981            if (mOrientation == HORIZONTAL) {
1982                scrollX = scrollPrimary;
1983                scrollY = scrollSecondary;
1984            } else {
1985                scrollX = scrollSecondary;
1986                scrollY = scrollPrimary;
1987            }
1988            if (smooth) {
1989                mBaseGridView.smoothScrollBy(scrollX, scrollY);
1990            } else {
1991                mBaseGridView.scrollBy(scrollX, scrollY);
1992            }
1993        }
1994    }
1995
1996    public void setAnimateChildLayout(boolean animateChildLayout) {
1997        mAnimateChildLayout = animateChildLayout;
1998        for (int i = 0, c = getChildCount(); i < c; i++) {
1999            View v = getChildAt(i);
2000            LayoutParams p = (LayoutParams) v.getLayoutParams();
2001            if (!mAnimateChildLayout) {
2002                p.endAnimate();
2003            }
2004        }
2005    }
2006
2007    private void attemptRecordChildLayout() {
2008        if (!mAnimateChildLayout) {
2009            return;
2010        }
2011        for (int i = 0, c = getChildCount(); i < c; i++) {
2012            View v = getChildAt(i);
2013            ((LayoutParams) v.getLayoutParams()).recordStart(mOrientation, v);
2014        }
2015    }
2016
2017    private void attemptAnimateLayoutChild() {
2018        if (!mAnimateChildLayout) {
2019            return;
2020        }
2021        for (int i = 0, c = getChildCount(); i < c; i++) {
2022            // TODO: start delay can be staggered
2023            View v = getChildAt(i);
2024            ((LayoutParams) v.getLayoutParams()).startAnimate(v,
2025                    getChildLayoutAnimationDuration(), 0, getChildLayoutAnimationInterpolator());
2026        }
2027    }
2028
2029    public boolean isChildLayoutAnimated() {
2030        return mAnimateChildLayout;
2031    }
2032
2033    public void setPruneChild(boolean pruneChild) {
2034        if (mPruneChild != pruneChild) {
2035            mPruneChild = pruneChild;
2036            if (mPruneChild) {
2037                removeInvisibleViewsAtEnd();
2038                removeInvisibleViewsAtFront();
2039            }
2040        }
2041    }
2042
2043    public boolean getPruneChild() {
2044        return mPruneChild;
2045    }
2046
2047    public void setChildLayoutAnimationInterpolator(Interpolator interpolator) {
2048        mAnimateLayoutChildInterpolator = interpolator;
2049    }
2050
2051    public Interpolator getChildLayoutAnimationInterpolator() {
2052        return mAnimateLayoutChildInterpolator;
2053    }
2054
2055    public void setChildLayoutAnimationDuration(long duration) {
2056        mAnimateLayoutChildDuration = duration;
2057    }
2058
2059    public long getChildLayoutAnimationDuration() {
2060        return mAnimateLayoutChildDuration;
2061    }
2062
2063    private int findImmediateChildIndex(View view) {
2064        while (view != null && view != mBaseGridView) {
2065            int index = mBaseGridView.indexOfChild(view);
2066            if (index >= 0) {
2067                return index;
2068            }
2069            view = (View) view.getParent();
2070        }
2071        return NO_POSITION;
2072    }
2073
2074    void setFocusSearchDisabled(boolean disabled) {
2075        mFocusSearchDisabled = disabled;
2076    }
2077
2078    boolean isFocusSearchDisabled() {
2079        return mFocusSearchDisabled;
2080    }
2081
2082    @Override
2083    public View onInterceptFocusSearch(View focused, int direction) {
2084        if (mFocusSearchDisabled) {
2085            return focused;
2086        }
2087        return null;
2088    }
2089
2090    boolean hasNextViewInSameRow(int pos) {
2091        if (mGrid == null || pos == NO_POSITION) {
2092            return false;
2093        }
2094        final int focusedRow = mGrid.getLocation(pos).row;
2095        for (int i = 0, count = getChildCount(); i < count; i++) {
2096            int position = getPositionByIndex(i);
2097            StaggeredGrid.Location loc = mGrid.getLocation(position);
2098            if (loc != null && loc.row == focusedRow) {
2099                if (position > pos) {
2100                    return true;
2101                }
2102            }
2103        }
2104        return false;
2105    }
2106
2107    boolean hasPreviousViewInSameRow(int pos) {
2108        if (mGrid == null || pos == NO_POSITION) {
2109            return false;
2110        }
2111        final int focusedRow = mGrid.getLocation(pos).row;
2112        for (int i = getChildCount() - 1; i >= 0; i--) {
2113            int position = getPositionByIndex(i);
2114            StaggeredGrid.Location loc = mGrid.getLocation(position);
2115            if (loc != null && loc.row == focusedRow) {
2116                if (position < pos) {
2117                    return true;
2118                }
2119            }
2120        }
2121        return false;
2122    }
2123
2124    @Override
2125    public boolean onAddFocusables(RecyclerView recyclerView,
2126            ArrayList<View> views, int direction, int focusableMode) {
2127        if (mFocusSearchDisabled) {
2128            return true;
2129        }
2130        // If this viewgroup or one of its children currently has focus then we
2131        // consider our children for focus searching in main direction on the same row.
2132        // If this viewgroup has no focus and using focus align, we want the system
2133        // to ignore our children and pass focus to the viewgroup, which will pass
2134        // focus on to its children appropriately.
2135        // If this viewgroup has no focus and not using focus align, we want to
2136        // consider the child that does not overlap with padding area.
2137        if (recyclerView.hasFocus()) {
2138            final int movement = getMovement(direction);
2139            if (movement != PREV_ITEM && movement != NEXT_ITEM) {
2140                // Move on secondary direction uses default addFocusables().
2141                return false;
2142            }
2143            final View focused = recyclerView.findFocus();
2144            final int focusedPos = getPositionByIndex(findImmediateChildIndex(focused));
2145            // Add focusables of focused item.
2146            if (focusedPos != NO_POSITION) {
2147                getViewByPosition(focusedPos).addFocusables(views,  direction, focusableMode);
2148            }
2149            final int focusedRow = mGrid != null && focusedPos != NO_POSITION ?
2150                    mGrid.getLocation(focusedPos).row : NO_POSITION;
2151            // Add focusables of next neighbor of same row on the focus search direction.
2152            if (mGrid != null) {
2153                final int focusableCount = views.size();
2154                for (int i = 0, count = getChildCount(); i < count; i++) {
2155                    int index = movement == NEXT_ITEM ? i : count - 1 - i;
2156                    final View child = getChildAt(index);
2157                    if (child.getVisibility() != View.VISIBLE) {
2158                        continue;
2159                    }
2160                    int position = getPositionByIndex(index);
2161                    StaggeredGrid.Location loc = mGrid.getLocation(position);
2162                    if (focusedRow == NO_POSITION || (loc != null && loc.row == focusedRow)) {
2163                        if (focusedPos == NO_POSITION ||
2164                                (movement == NEXT_ITEM && position > focusedPos)
2165                                || (movement == PREV_ITEM && position < focusedPos)) {
2166                            child.addFocusables(views,  direction, focusableMode);
2167                            if (views.size() > focusableCount) {
2168                                break;
2169                            }
2170                        }
2171                    }
2172                }
2173            }
2174        } else {
2175            if (mFocusScrollStrategy != BaseGridView.FOCUS_SCROLL_ALIGNED) {
2176                // adding views not overlapping padding area to avoid scrolling in gaining focus
2177                int left = mWindowAlignment.mainAxis().getPaddingLow();
2178                int right = mWindowAlignment.mainAxis().getClientSize() + left;
2179                int focusableCount = views.size();
2180                for (int i = 0, count = getChildCount(); i < count; i++) {
2181                    View child = getChildAt(i);
2182                    if (child.getVisibility() == View.VISIBLE) {
2183                        if (getViewMin(child) >= left && getViewMax(child) <= right) {
2184                            child.addFocusables(views, direction, focusableMode);
2185                        }
2186                    }
2187                }
2188                // if we cannot find any, then just add all children.
2189                if (views.size() == focusableCount) {
2190                    for (int i = 0, count = getChildCount(); i < count; i++) {
2191                        View child = getChildAt(i);
2192                        if (child.getVisibility() == View.VISIBLE) {
2193                            child.addFocusables(views, direction, focusableMode);
2194                        }
2195                    }
2196                    if (views.size() != focusableCount) {
2197                        return true;
2198                    }
2199                } else {
2200                    return true;
2201                }
2202                // if still cannot find any, fall through and add itself
2203            }
2204            if (recyclerView.isFocusable()) {
2205                views.add(recyclerView);
2206            }
2207        }
2208        return true;
2209    }
2210
2211    @Override
2212    public View onFocusSearchFailed(View focused, int direction, Adapter adapter,
2213            Recycler recycler) {
2214        if (DEBUG) Log.v(getTag(), "onFocusSearchFailed direction " + direction);
2215
2216        View view = null;
2217        int movement = getMovement(direction);
2218        final FocusFinder ff = FocusFinder.getInstance();
2219        if (movement == NEXT_ITEM) {
2220            while (view == null && !appendOneVisibleItem()) {
2221                view = ff.findNextFocus(mBaseGridView, focused, direction);
2222            }
2223        } else if (movement == PREV_ITEM){
2224            while (view == null && !prependOneVisibleItem()) {
2225                view = ff.findNextFocus(mBaseGridView, focused, direction);
2226            }
2227        }
2228        if (view == null) {
2229            // returning the same view to prevent focus lost when scrolling past the end of the list
2230            if (movement == PREV_ITEM) {
2231                view = mFocusOutFront ? null : focused;
2232            } else if (movement == NEXT_ITEM){
2233                view = mFocusOutEnd ? null : focused;
2234            }
2235        }
2236        if (DEBUG) Log.v(getTag(), "returning view " + view);
2237        return view;
2238    }
2239
2240    boolean gridOnRequestFocusInDescendants(RecyclerView recyclerView, int direction,
2241            Rect previouslyFocusedRect) {
2242        switch (mFocusScrollStrategy) {
2243        case BaseGridView.FOCUS_SCROLL_ALIGNED:
2244        default:
2245            return gridOnRequestFocusInDescendantsAligned(recyclerView,
2246                    direction, previouslyFocusedRect);
2247        case BaseGridView.FOCUS_SCROLL_PAGE:
2248        case BaseGridView.FOCUS_SCROLL_ITEM:
2249            return gridOnRequestFocusInDescendantsUnaligned(recyclerView,
2250                    direction, previouslyFocusedRect);
2251        }
2252    }
2253
2254    private boolean gridOnRequestFocusInDescendantsAligned(RecyclerView recyclerView,
2255            int direction, Rect previouslyFocusedRect) {
2256        View view = getViewByPosition(mFocusPosition);
2257        if (view != null) {
2258            boolean result = view.requestFocus(direction, previouslyFocusedRect);
2259            if (!result && DEBUG) {
2260                Log.w(getTag(), "failed to request focus on " + view);
2261            }
2262            return result;
2263        }
2264        return false;
2265    }
2266
2267    private boolean gridOnRequestFocusInDescendantsUnaligned(RecyclerView recyclerView,
2268            int direction, Rect previouslyFocusedRect) {
2269        // focus to view not overlapping padding area to avoid scrolling in gaining focus
2270        int index;
2271        int increment;
2272        int end;
2273        int count = getChildCount();
2274        if ((direction & View.FOCUS_FORWARD) != 0) {
2275            index = 0;
2276            increment = 1;
2277            end = count;
2278        } else {
2279            index = count - 1;
2280            increment = -1;
2281            end = -1;
2282        }
2283        int left = mWindowAlignment.mainAxis().getPaddingLow();
2284        int right = mWindowAlignment.mainAxis().getClientSize() + left;
2285        for (int i = index; i != end; i += increment) {
2286            View child = getChildAt(i);
2287            if (child.getVisibility() == View.VISIBLE) {
2288                if (getViewMin(child) >= left && getViewMax(child) <= right) {
2289                    if (child.requestFocus(direction, previouslyFocusedRect)) {
2290                        return true;
2291                    }
2292                }
2293            }
2294        }
2295        return false;
2296    }
2297
2298    private final static int PREV_ITEM = 0;
2299    private final static int NEXT_ITEM = 1;
2300    private final static int PREV_ROW = 2;
2301    private final static int NEXT_ROW = 3;
2302
2303    private int getMovement(int direction) {
2304        int movement = View.FOCUS_LEFT;
2305
2306        if (mOrientation == HORIZONTAL) {
2307            switch(direction) {
2308                case View.FOCUS_LEFT:
2309                    movement = PREV_ITEM;
2310                    break;
2311                case View.FOCUS_RIGHT:
2312                    movement = NEXT_ITEM;
2313                    break;
2314                case View.FOCUS_UP:
2315                    movement = PREV_ROW;
2316                    break;
2317                case View.FOCUS_DOWN:
2318                    movement = NEXT_ROW;
2319                    break;
2320            }
2321         } else if (mOrientation == VERTICAL) {
2322             switch(direction) {
2323                 case View.FOCUS_LEFT:
2324                     movement = PREV_ROW;
2325                     break;
2326                 case View.FOCUS_RIGHT:
2327                     movement = NEXT_ROW;
2328                     break;
2329                 case View.FOCUS_UP:
2330                     movement = PREV_ITEM;
2331                     break;
2332                 case View.FOCUS_DOWN:
2333                     movement = NEXT_ITEM;
2334                     break;
2335             }
2336         }
2337
2338        return movement;
2339    }
2340
2341    int getChildDrawingOrder(RecyclerView recyclerView, int childCount, int i) {
2342        int focusIndex = getIndexByPosition(mFocusPosition);
2343        if (focusIndex == NO_POSITION) {
2344            return i;
2345        }
2346        // supposely 0 1 2 3 4 5 6 7 8 9, 4 is the center item
2347        // drawing order is 0 1 2 3 9 8 7 6 5 4
2348        if (i < focusIndex) {
2349            return i;
2350        } else if (i < childCount - 1) {
2351            return focusIndex + childCount - 1 - i;
2352        } else {
2353            return focusIndex;
2354        }
2355    }
2356
2357    @Override
2358    public void onAdapterChanged(RecyclerView.Adapter oldAdapter,
2359            RecyclerView.Adapter newAdapter) {
2360        discardLayoutInfo();
2361        super.onAdapterChanged(oldAdapter, newAdapter);
2362    }
2363
2364    private void discardLayoutInfo() {
2365        mGrid = null;
2366        mRows = null;
2367        mRowSizeSecondary = null;
2368        mFirstVisiblePos = -1;
2369        mLastVisiblePos = -1;
2370        mRowSecondarySizeRefresh = false;
2371    }
2372
2373    public void setLayoutEnabled(boolean layoutEnabled) {
2374        if (mLayoutEnabled != layoutEnabled) {
2375            mLayoutEnabled = layoutEnabled;
2376            requestLayout();
2377        }
2378    }
2379}
2380