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