GridLayoutManager.java revision 5a2782ae17df5331a594fe03d5d89251a8b9f6d4
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;
22
23import static android.support.v7.widget.RecyclerView.NO_ID;
24import static android.support.v7.widget.RecyclerView.NO_POSITION;
25import static android.support.v7.widget.RecyclerView.HORIZONTAL;
26import static android.support.v7.widget.RecyclerView.VERTICAL;
27
28import android.util.AttributeSet;
29import android.util.Log;
30import android.view.FocusFinder;
31import android.view.Gravity;
32import android.view.View;
33import android.view.ViewParent;
34import android.view.View.MeasureSpec;
35import android.view.ViewGroup.MarginLayoutParams;
36import android.view.ViewGroup;
37import android.view.animation.DecelerateInterpolator;
38import android.view.animation.Interpolator;
39
40import java.io.PrintWriter;
41import java.io.StringWriter;
42import java.util.ArrayList;
43
44final class GridLayoutManager extends RecyclerView.LayoutManager {
45
46     /*
47      * LayoutParams for {@link HorizontalGridView} and {@link VerticalGridView}.
48      * The class currently does three internal jobs:
49      * - Saves optical bounds insets.
50      * - Caches focus align view center.
51      * - Manages child view layout animation.
52      */
53    static class LayoutParams extends RecyclerView.LayoutParams {
54
55        // The view is saved only during animation.
56        private View mView;
57
58        // For placement
59        private int mLeftInset;
60        private int mTopInset;
61        private int mRighInset;
62        private int mBottomInset;
63
64        // For alignment
65        private int mAlignX;
66        private int mAlignY;
67
68        // For animations
69        private TimeAnimator mAnimator;
70        private long mDuration;
71        private boolean mFirstAttached;
72        // current virtual view position (scrollOffset + left/top) in the GridLayoutManager
73        private int mViewX, mViewY;
74        // animation start value of translation x and y
75        private float mAnimationStartTranslationX, mAnimationStartTranslationY;
76
77        public LayoutParams(Context c, AttributeSet attrs) {
78            super(c, attrs);
79        }
80
81        public LayoutParams(int width, int height) {
82            super(width, height);
83        }
84
85        public LayoutParams(MarginLayoutParams source) {
86            super(source);
87        }
88
89        public LayoutParams(ViewGroup.LayoutParams source) {
90            super(source);
91        }
92
93        public LayoutParams(RecyclerView.LayoutParams source) {
94            super(source);
95        }
96
97        public LayoutParams(LayoutParams source) {
98            super(source);
99        }
100
101        void onViewAttached() {
102            endAnimate();
103            mFirstAttached = true;
104        }
105
106        void onViewDetached() {
107            endAnimate();
108        }
109
110        int getAlignX() {
111            return mAlignX;
112        }
113
114        int getAlignY() {
115            return mAlignY;
116        }
117
118        int getOpticalLeft(View view) {
119            return view.getLeft() + mLeftInset;
120        }
121
122        int getOpticalTop(View view) {
123            return view.getTop() + mTopInset;
124        }
125
126        int getOpticalRight(View view) {
127            return view.getRight() - mRighInset;
128        }
129
130        int getOpticalBottom(View view) {
131            return view.getBottom() - mBottomInset;
132        }
133
134        int getOpticalWidth(View view) {
135            return view.getWidth() - mLeftInset - mRighInset;
136        }
137
138        int getOpticalHeight(View view) {
139            return view.getHeight() - mTopInset - mBottomInset;
140        }
141
142        void setAlignX(int alignX) {
143            mAlignX = alignX;
144        }
145
146        void setAlignY(int alignY) {
147            mAlignY = alignY;
148        }
149
150        void setOpticalInsets(int leftInset, int topInset, int rightInset, int bottomInset) {
151            mLeftInset = leftInset;
152            mTopInset = topInset;
153            mRighInset = rightInset;
154            mBottomInset = bottomInset;
155        }
156
157        private TimeAnimator.TimeListener mTimeListener = new TimeAnimator.TimeListener() {
158            @Override
159            public void onTimeUpdate(TimeAnimator animation, long totalTime, long deltaTime) {
160                if (mView == null) {
161                    return;
162                }
163                if (totalTime >= mDuration) {
164                    endAnimate();
165                } else {
166                    float fraction = (float) (totalTime / (double)mDuration);
167                    float fractionToEnd = 1 - mAnimator
168                        .getInterpolator().getInterpolation(fraction);
169                    mView.setTranslationX(fractionToEnd * mAnimationStartTranslationX);
170                    mView.setTranslationY(fractionToEnd * mAnimationStartTranslationY);
171                    invalidateItemDecoration();
172                }
173            }
174        };
175
176        void startAnimate(GridLayoutManager layout, View view, long startDelay) {
177            if (mAnimator == null) {
178                mAnimator = new TimeAnimator();
179                mAnimator.setTimeListener(mTimeListener);
180            }
181            if (mFirstAttached) {
182                // first time record the initial location and return without animation
183                // TODO do we need initial animation?
184                mViewX = layout.getScrollOffsetX() + getOpticalLeft(view);
185                mViewY = layout.getScrollOffsetY() + getOpticalTop(view);
186                mFirstAttached = false;
187                return;
188            }
189            if (!layout.isChildLayoutAnimated()) {
190                return;
191            }
192            mView = view;
193            int newViewX = layout.getScrollOffsetX() + getOpticalLeft(mView);
194            int newViewY = layout.getScrollOffsetY() + getOpticalTop(mView);
195            if (newViewX != mViewX || newViewY != mViewY) {
196                mAnimator.cancel();
197                mAnimationStartTranslationX = mView.getTranslationX();
198                mAnimationStartTranslationY = mView.getTranslationY();
199                mAnimationStartTranslationX += mViewX - newViewX;
200                mAnimationStartTranslationY += mViewY - newViewY;
201                mDuration = layout.getChildLayoutAnimationDuration();
202                mAnimator.setDuration(mDuration);
203                mAnimator.setInterpolator(layout.getChildLayoutAnimationInterpolator());
204                mAnimator.setStartDelay(startDelay);
205                mAnimator.start();
206                mViewX = newViewX;
207                mViewY = newViewY;
208            }
209        }
210
211        void endAnimate() {
212            if (mAnimator != null) {
213                mAnimator.end();
214            }
215            if (mView != null) {
216                mView.setTranslationX(0);
217                mView.setTranslationY(0);
218                mView = null;
219            }
220        }
221
222        private void invalidateItemDecoration() {
223            ViewParent parent = mView.getParent();
224            if (parent instanceof RecyclerView) {
225                // TODO: we only need invalidate parent if it has ItemDecoration
226                ((RecyclerView) parent).invalidate();
227            }
228        }
229    }
230
231    private static final String TAG = "GridLayoutManager";
232    private static final boolean DEBUG = false;
233
234    private static final Interpolator sDefaultAnimationChildLayoutInterpolator
235            = new DecelerateInterpolator();
236
237    private static final long DEFAULT_CHILD_ANIMATION_DURATION_MS = 250;
238
239    private String getTag() {
240        return TAG + ":" + mBaseGridView.getId();
241    }
242
243    private final BaseGridView mBaseGridView;
244
245    /**
246     * The orientation of a "row".
247     */
248    private int mOrientation = HORIZONTAL;
249
250    private RecyclerView.Adapter mAdapter;
251    private RecyclerView.Recycler mRecycler;
252
253    private boolean mInLayout = false;
254
255    private OnChildSelectedListener mChildSelectedListener = null;
256
257    /**
258     * The focused position, it's not the currently visually aligned position
259     * but it is the final position that we intend to focus on. If there are
260     * multiple setSelection() called, mFocusPosition saves last value.
261     */
262    private int mFocusPosition = NO_POSITION;
263
264    /**
265     * Force a full layout under certain situations.
266     */
267    private boolean mForceFullLayout;
268
269    /**
270     * The scroll offsets of the viewport relative to the entire view.
271     */
272    private int mScrollOffsetPrimary;
273    private int mScrollOffsetSecondary;
274
275    /**
276     * User-specified fixed size of each grid item in the secondary direction, can be
277     * 0 to be determined by parent size and number of rows.
278     */
279    private int mItemLengthSecondaryRequested;
280    /**
281     * The fixed size of each grid item in the secondary direction. This corresponds to
282     * the row height, equal for all rows. Grid items may have variable length
283     * in the primary direction.
284     *
285     */
286    private int mItemLengthSecondary;
287
288    /**
289     * Margin between items.
290     */
291    private int mHorizontalMargin;
292    /**
293     * Margin between items vertically.
294     */
295    private int mVerticalMargin;
296    /**
297     * Margin in main direction.
298     */
299    private int mMarginPrimary;
300    /**
301     * Margin in second direction.
302     */
303    private int mMarginSecondary;
304    /**
305     * How to position child in secondary direction.
306     */
307    private int mGravity = Gravity.LEFT | Gravity.TOP;
308    /**
309     * The number of rows in the grid.
310     */
311    private int mNumRows;
312    /**
313     * Number of rows requested, can be 0 to be determined by parent size and
314     * rowHeight.
315     */
316    private int mNumRowsRequested = 1;
317
318    /**
319     * Tracking start/end position of each row for visible items.
320     */
321    private StaggeredGrid.Row[] mRows;
322
323    /**
324     * Saves grid information of each view.
325     */
326    private StaggeredGrid mGrid;
327    /**
328     * Position of first item (included) that has attached views.
329     */
330    private int mFirstVisiblePos;
331    /**
332     * Position of last item (included) that has attached views.
333     */
334    private int mLastVisiblePos;
335
336    /**
337     * Defines how item view is aligned in the window.
338     */
339    private final WindowAlignment mWindowAlignment = new WindowAlignment();
340
341    /**
342     * Defines how item view is aligned.
343     */
344    private final ItemAlignment mItemAlignment = new ItemAlignment();
345
346    /**
347     * Dimensions of the view, width or height depending on orientation.
348     */
349    private int mSizePrimary;
350
351    /**
352     *  Allow DPAD key to navigate out at the front of the View (where position = 0),
353     *  default is false.
354     */
355    private boolean mFocusOutFront;
356
357    /**
358     * Allow DPAD key to navigate out at the end of the view, default is false.
359     */
360    private boolean mFocusOutEnd;
361
362    /**
363     * Animate layout changes from a child resizing or adding/removing a child.
364     */
365    private boolean mAnimateChildLayout = true;
366
367    /**
368     * Interpolator used to animate layout of children.
369     */
370    private Interpolator mAnimateLayoutChildInterpolator = sDefaultAnimationChildLayoutInterpolator;
371
372    /**
373     * Duration used to animate layout of children.
374     */
375    private long mAnimateLayoutChildDuration = DEFAULT_CHILD_ANIMATION_DURATION_MS;
376
377    public GridLayoutManager(BaseGridView baseGridView) {
378        mBaseGridView = baseGridView;
379    }
380
381    public void setOrientation(int orientation) {
382        if (orientation != HORIZONTAL && orientation != VERTICAL) {
383            if (DEBUG) Log.v(getTag(), "invalid orientation: " + orientation);
384            return;
385        }
386
387        mOrientation = orientation;
388        mWindowAlignment.setOrientation(orientation);
389        mItemAlignment.setOrientation(orientation);
390        mForceFullLayout = true;
391    }
392
393    public void setWindowAlignment(int windowAlignment) {
394        mWindowAlignment.mainAxis().setWindowAlignment(windowAlignment);
395    }
396
397    public int getWindowAlignment() {
398        return mWindowAlignment.mainAxis().getWindowAlignment();
399    }
400
401    public void setWindowAlignmentOffset(int alignmentOffset) {
402        mWindowAlignment.mainAxis().setWindowAlignmentOffset(alignmentOffset);
403    }
404
405    public int getWindowAlignmentOffset() {
406        return mWindowAlignment.mainAxis().getWindowAlignmentOffset();
407    }
408
409    public void setWindowAlignmentOffsetPercent(float offsetPercent) {
410        mWindowAlignment.mainAxis().setWindowAlignmentOffsetPercent(offsetPercent);
411    }
412
413    public float getWindowAlignmentOffsetPercent() {
414        return mWindowAlignment.mainAxis().getWindowAlignmentOffsetPercent();
415    }
416
417    public void setItemAlignmentOffset(int alignmentOffset) {
418        mItemAlignment.mainAxis().setItemAlignmentOffset(alignmentOffset);
419        updateChildAlignments();
420    }
421
422    public int getItemAlignmentOffset() {
423        return mItemAlignment.mainAxis().getItemAlignmentOffset();
424    }
425
426    public void setItemAlignmentOffsetPercent(float offsetPercent) {
427        mItemAlignment.mainAxis().setItemAlignmentOffsetPercent(offsetPercent);
428        updateChildAlignments();
429    }
430
431    public float getItemAlignmentOffsetPercent() {
432        return mItemAlignment.mainAxis().getItemAlignmentOffsetPercent();
433    }
434
435    public void setItemAlignmentViewId(int viewId) {
436        mItemAlignment.mainAxis().setItemAlignmentViewId(viewId);
437        updateChildAlignments();
438    }
439
440    public int getItemAlignmentViewId() {
441        return mItemAlignment.mainAxis().getItemAlignmentViewId();
442    }
443
444    public void setFocusOutAllowed(boolean throughFront, boolean throughEnd) {
445        mFocusOutFront = throughFront;
446        mFocusOutEnd = throughEnd;
447    }
448
449    public void setNumRows(int numRows) {
450        if (numRows < 0) throw new IllegalArgumentException();
451        mNumRowsRequested = numRows;
452        mForceFullLayout = true;
453    }
454
455    public void setRowHeight(int height) {
456        if (height < 0) throw new IllegalArgumentException();
457        mItemLengthSecondaryRequested = height;
458    }
459
460    public void setItemMargin(int margin) {
461        mVerticalMargin = mHorizontalMargin = margin;
462        mMarginPrimary = mMarginSecondary = margin;
463    }
464
465    public void setVerticalMargin(int margin) {
466        if (mOrientation == HORIZONTAL) {
467            mMarginSecondary = mVerticalMargin = margin;
468        } else {
469            mMarginPrimary = mVerticalMargin = margin;
470        }
471    }
472
473    public void setHorizontalMargin(int margin) {
474        if (mOrientation == HORIZONTAL) {
475            mMarginPrimary = mHorizontalMargin = margin;
476        } else {
477            mMarginSecondary = mHorizontalMargin = margin;
478        }
479    }
480
481    public int getVerticalMargin() {
482        return mVerticalMargin;
483    }
484
485    public int getHorizontalMargin() {
486        return mHorizontalMargin;
487    }
488
489    public void setGravity(int gravity) {
490        mGravity = gravity;
491    }
492
493    protected boolean hasDoneFirstLayout() {
494        return mGrid != null;
495    }
496
497    public void setOnChildSelectedListener(OnChildSelectedListener listener) {
498        mChildSelectedListener = listener;
499    }
500
501    private int getPositionByView(View view) {
502        return getPositionByIndex(mBaseGridView.indexOfChild(view));
503    }
504
505    private int getPositionByIndex(int index) {
506        if (index < 0) {
507            return NO_POSITION;
508        }
509        return mFirstVisiblePos + index;
510    }
511
512    private View getViewByPosition(int position) {
513        int index = getIndexByPosition(position);
514        if (index < 0) {
515            return null;
516        }
517        return getChildAt(index);
518    }
519
520    private int getIndexByPosition(int position) {
521        if (mFirstVisiblePos < 0 ||
522                position < mFirstVisiblePos || position > mLastVisiblePos) {
523            return NO_POSITION;
524        }
525        return position - mFirstVisiblePos;
526    }
527
528    private void dispatchChildSelected() {
529        if (mChildSelectedListener == null) {
530            return;
531        }
532
533        View view = getViewByPosition(mFocusPosition);
534
535        if (mFocusPosition != NO_POSITION) {
536            mChildSelectedListener.onChildSelected(mBaseGridView, view, mFocusPosition,
537                    mAdapter.getItemId(mFocusPosition));
538        } else {
539            mChildSelectedListener.onChildSelected(mBaseGridView, null, NO_POSITION, NO_ID);
540        }
541    }
542
543    @Override
544    public boolean canScrollHorizontally() {
545        // We can scroll horizontally if we have horizontal orientation, or if
546        // we are vertical and have more than one column.
547        return mOrientation == HORIZONTAL || mNumRows > 1;
548    }
549
550    @Override
551    public boolean canScrollVertically() {
552        // We can scroll vertically if we have vertical orientation, or if we
553        // are horizontal and have more than one row.
554        return mOrientation == VERTICAL || mNumRows > 1;
555    }
556
557    @Override
558    public RecyclerView.LayoutParams generateDefaultLayoutParams() {
559        return new LayoutParams(ViewGroup.LayoutParams.WRAP_CONTENT,
560                ViewGroup.LayoutParams.WRAP_CONTENT);
561    }
562
563    @Override
564    public RecyclerView.LayoutParams generateLayoutParams(Context context, AttributeSet attrs) {
565        return new LayoutParams(context, attrs);
566    }
567
568    @Override
569    public RecyclerView.LayoutParams generateLayoutParams(ViewGroup.LayoutParams lp) {
570        if (lp instanceof LayoutParams) {
571            return new LayoutParams((LayoutParams) lp);
572        } else if (lp instanceof RecyclerView.LayoutParams) {
573            return new LayoutParams((RecyclerView.LayoutParams) lp);
574        } else if (lp instanceof MarginLayoutParams) {
575            return new LayoutParams((MarginLayoutParams) lp);
576        } else {
577            return new LayoutParams(lp);
578        }
579    }
580
581    protected View getViewForPosition(int position) {
582        View v = mRecycler.getViewForPosition(mAdapter, position);
583        if (v != null) {
584            ((LayoutParams) v.getLayoutParams()).onViewAttached();
585        }
586        return v;
587    }
588
589    private int getViewMin(View v) {
590        LayoutParams p = (LayoutParams) v.getLayoutParams();
591        return (mOrientation == HORIZONTAL) ? p.getOpticalLeft(v) : p.getOpticalTop(v);
592    }
593
594    private int getViewMax(View v) {
595        LayoutParams p = (LayoutParams) v.getLayoutParams();
596        return (mOrientation == HORIZONTAL) ? p.getOpticalRight(v) : p.getOpticalBottom(v);
597    }
598
599    private int getViewCenter(View view) {
600        return (mOrientation == HORIZONTAL) ? getViewCenterX(view) : getViewCenterY(view);
601    }
602
603    private int getViewCenterSecondary(View view) {
604        return (mOrientation == HORIZONTAL) ? getViewCenterY(view) : getViewCenterX(view);
605    }
606
607    private int getViewCenterX(View v) {
608        LayoutParams p = (LayoutParams) v.getLayoutParams();
609        return p.getOpticalLeft(v) + p.getAlignX();
610    }
611
612    private int getViewCenterY(View v) {
613        LayoutParams p = (LayoutParams) v.getLayoutParams();
614        return p.getOpticalTop(v) + p.getAlignY();
615    }
616
617    /**
618     * Re-initialize data structures for a data change or handling invisible
619     * selection. The method tries its best to preserve position information so
620     * that staggered grid looks same before and after re-initialize.
621     * @param focusPosition The initial focusPosition that we would like to
622     *        focus on.
623     * @return Actual position that can be focused on.
624     */
625    private int init(RecyclerView.Adapter adapter, RecyclerView.Recycler recycler,
626            int focusPosition) {
627
628        final int newItemCount = adapter.getItemCount();
629
630        if (focusPosition == NO_POSITION && newItemCount > 0) {
631            // if focus position is never set before,  initialize it to 0
632            focusPosition = 0;
633        }
634        // If adapter has changed then caches are invalid; otherwise,
635        // we try to maintain each row's position if number of rows keeps the same
636        // and existing mGrid contains the focusPosition.
637        if (mRows != null && mNumRows == mRows.length &&
638                mGrid != null && mGrid.getSize() > 0 && focusPosition >= 0 &&
639                focusPosition >= mGrid.getFirstIndex() &&
640                focusPosition <= mGrid.getLastIndex()) {
641            // strip mGrid to a subset (like a column) that contains focusPosition
642            mGrid.stripDownTo(focusPosition);
643            // make sure that remaining items do not exceed new adapter size
644            int firstIndex = mGrid.getFirstIndex();
645            int lastIndex = mGrid.getLastIndex();
646            if (DEBUG) {
647                Log .v(getTag(), "mGrid firstIndex " + firstIndex + " lastIndex " + lastIndex);
648            }
649            for (int i = lastIndex; i >=firstIndex; i--) {
650                if (i >= newItemCount) {
651                    mGrid.removeLast();
652                }
653            }
654            if (mGrid.getSize() == 0) {
655                focusPosition = newItemCount - 1;
656                // initialize row start locations
657                for (int i = 0; i < mNumRows; i++) {
658                    mRows[i].low = 0;
659                    mRows[i].high = 0;
660                }
661                if (DEBUG) Log.v(getTag(), "mGrid zero size");
662            } else {
663                // initialize row start locations
664                for (int i = 0; i < mNumRows; i++) {
665                    mRows[i].low = Integer.MAX_VALUE;
666                    mRows[i].high = Integer.MIN_VALUE;
667                }
668                firstIndex = mGrid.getFirstIndex();
669                lastIndex = mGrid.getLastIndex();
670                if (focusPosition > lastIndex) {
671                    focusPosition = mGrid.getLastIndex();
672                }
673                if (DEBUG) {
674                    Log.v(getTag(), "mGrid firstIndex " + firstIndex + " lastIndex "
675                        + lastIndex + " focusPosition " + focusPosition);
676                }
677                // fill rows with minimal view positions of the subset
678                for (int i = firstIndex; i <= lastIndex; i++) {
679                    View v = getViewByPosition(i);
680                    if (v == null) {
681                        continue;
682                    }
683                    int row = mGrid.getLocation(i).row;
684                    int low = getViewMin(v) + mScrollOffsetPrimary;
685                    if (low < mRows[row].low) {
686                        mRows[row].low = mRows[row].high = low;
687                    }
688                }
689                // fill other rows that does not include the subset using first item
690                int firstItemRowPosition = mRows[mGrid.getLocation(firstIndex).row].low;
691                if (firstItemRowPosition == Integer.MAX_VALUE) {
692                    firstItemRowPosition = 0;
693                }
694                for (int i = 0; i < mNumRows; i++) {
695                    if (mRows[i].low == Integer.MAX_VALUE) {
696                        mRows[i].low = mRows[i].high = firstItemRowPosition;
697                    }
698                }
699            }
700
701            // Same adapter, we can reuse any attached views
702            detachAndScrapAttachedViews(recycler);
703
704        } else {
705            // otherwise recreate data structure
706            mRows = new StaggeredGrid.Row[mNumRows];
707            for (int i = 0; i < mNumRows; i++) {
708                mRows[i] = new StaggeredGrid.Row();
709            }
710            mGrid = new StaggeredGridDefault();
711            if (newItemCount == 0) {
712                focusPosition = NO_POSITION;
713            } else if (focusPosition >= newItemCount) {
714                focusPosition = newItemCount - 1;
715            }
716
717            // Adapter may have changed so remove all attached views permanently
718            removeAllViews();
719
720            mScrollOffsetPrimary = 0;
721            mScrollOffsetSecondary = 0;
722            mWindowAlignment.reset();
723        }
724
725        mAdapter = adapter;
726        mRecycler = recycler;
727        mGrid.setProvider(mGridProvider);
728        // mGrid share the same Row array information
729        mGrid.setRows(mRows);
730        mFirstVisiblePos = mLastVisiblePos = NO_POSITION;
731
732        initScrollController();
733
734        return focusPosition;
735    }
736
737    // TODO: use recyclerview support for measuring the whole container, once
738    // it's available.
739    void onMeasure(int widthSpec, int heightSpec, int[] result) {
740        int sizePrimary, sizeSecondary, modeSecondary, paddingSecondary;
741        int measuredSizeSecondary;
742        if (mOrientation == HORIZONTAL) {
743            sizePrimary = MeasureSpec.getSize(widthSpec);
744            sizeSecondary = MeasureSpec.getSize(heightSpec);
745            modeSecondary = MeasureSpec.getMode(heightSpec);
746            paddingSecondary = getPaddingTop() + getPaddingBottom();
747        } else {
748            sizeSecondary = MeasureSpec.getSize(widthSpec);
749            sizePrimary = MeasureSpec.getSize(heightSpec);
750            modeSecondary = MeasureSpec.getMode(widthSpec);
751            paddingSecondary = getPaddingLeft() + getPaddingRight();
752        }
753        switch (modeSecondary) {
754        case MeasureSpec.UNSPECIFIED:
755            if (mItemLengthSecondaryRequested == 0) {
756                if (mOrientation == HORIZONTAL) {
757                    throw new IllegalStateException("Must specify rowHeight or view height");
758                } else {
759                    throw new IllegalStateException("Must specify columnWidth or view width");
760                }
761            }
762            mItemLengthSecondary = mItemLengthSecondaryRequested;
763            if (mNumRowsRequested == 0) {
764                mNumRows = 1;
765            } else {
766                mNumRows = mNumRowsRequested;
767            }
768            measuredSizeSecondary = mItemLengthSecondary * mNumRows + mMarginSecondary
769                * (mNumRows - 1) + paddingSecondary;
770            break;
771        case MeasureSpec.AT_MOST:
772        case MeasureSpec.EXACTLY:
773            if (mNumRowsRequested == 0 && mItemLengthSecondaryRequested == 0) {
774                mNumRows = 1;
775                mItemLengthSecondary = sizeSecondary - paddingSecondary;
776            } else if (mNumRowsRequested == 0) {
777                mItemLengthSecondary = mItemLengthSecondaryRequested;
778                mNumRows = (sizeSecondary + mMarginSecondary)
779                    / (mItemLengthSecondaryRequested + mMarginSecondary);
780            } else if (mItemLengthSecondaryRequested == 0) {
781                mNumRows = mNumRowsRequested;
782                mItemLengthSecondary = (sizeSecondary - paddingSecondary - mMarginSecondary
783                        * (mNumRows - 1)) / mNumRows;
784            } else {
785                mNumRows = mNumRowsRequested;
786                mItemLengthSecondary = mItemLengthSecondaryRequested;
787            }
788            measuredSizeSecondary = sizeSecondary;
789            if (modeSecondary == MeasureSpec.AT_MOST) {
790                int childrenSize = mItemLengthSecondary * mNumRows + mMarginSecondary
791                    * (mNumRows - 1) + paddingSecondary;
792                if (childrenSize < measuredSizeSecondary) {
793                    measuredSizeSecondary = childrenSize;
794                }
795            }
796            break;
797        default:
798            throw new IllegalStateException("wrong spec");
799        }
800        if (mOrientation == HORIZONTAL) {
801            result[0] = sizePrimary;
802            result[1] = measuredSizeSecondary;
803        } else {
804            result[0] = measuredSizeSecondary;
805            result[1] = sizePrimary;
806        }
807        if (DEBUG) {
808            Log.v(getTag(), "onMeasure result " + result[0] + ", " + result[1]
809                    + " mItemLengthSecondary " + mItemLengthSecondary + " mNumRows " + mNumRows);
810        }
811    }
812
813    private void measureChild(View child) {
814        final ViewGroup.LayoutParams lp = child.getLayoutParams();
815
816        int widthSpec, heightSpec;
817        if (mOrientation == HORIZONTAL) {
818            widthSpec = ViewGroup.getChildMeasureSpec(
819                    MeasureSpec.makeMeasureSpec(0, MeasureSpec.UNSPECIFIED), 0, lp.width);
820            heightSpec = ViewGroup.getChildMeasureSpec(MeasureSpec.makeMeasureSpec(
821                    mItemLengthSecondary, MeasureSpec.EXACTLY), 0, lp.height);
822        } else {
823            heightSpec = ViewGroup.getChildMeasureSpec(
824                    MeasureSpec.makeMeasureSpec(0, MeasureSpec.UNSPECIFIED), 0, lp.height);
825            widthSpec = ViewGroup.getChildMeasureSpec(MeasureSpec.makeMeasureSpec(
826                    mItemLengthSecondary, MeasureSpec.EXACTLY), 0, lp.width);
827        }
828
829        child.measure(widthSpec, heightSpec);
830    }
831
832    private StaggeredGrid.Provider mGridProvider = new StaggeredGrid.Provider() {
833
834        @Override
835        public int getCount() {
836            return mAdapter.getItemCount();
837        }
838
839        @Override
840        public void createItem(int index, int rowIndex, boolean append) {
841            View v = getViewForPosition(index);
842            if (mFirstVisiblePos >= 0) {
843                // when StaggeredGrid append or prepend item, we must guarantee
844                // that sibling item has created views already.
845                if (append && index != mLastVisiblePos + 1) {
846                    throw new RuntimeException();
847                } else if (!append && index != mFirstVisiblePos - 1) {
848                    throw new RuntimeException();
849                }
850            }
851
852            if (append) {
853                addView(v);
854            } else {
855                addView(v, 0);
856            }
857
858            measureChild(v);
859
860            int length = mOrientation == HORIZONTAL ? v.getMeasuredWidth() : v.getMeasuredHeight();
861            int start, end;
862            if (append) {
863                start = mRows[rowIndex].high;
864                if (start != mRows[rowIndex].low) {
865                    // if there are existing item in the row,  add margin between
866                    start += mMarginPrimary;
867                } else {
868                    final int lastRow = mRows.length - 1;
869                    if (lastRow != rowIndex && mRows[lastRow].high != mRows[lastRow].low) {
870                        // if there are existing item in the last row, insert
871                        // the new item after the last item of last row.
872                        start = mRows[lastRow].high + mMarginPrimary;
873                    }
874                }
875                end = start + length;
876                mRows[rowIndex].high = end;
877            } else {
878                end = mRows[rowIndex].low;
879                if (end != mRows[rowIndex].high) {
880                    end -= mMarginPrimary;
881                } else if (0 != rowIndex && mRows[0].high != mRows[0].low) {
882                    // if there are existing item in the first row, insert
883                    // the new item before the first item of first row.
884                    end = mRows[0].low - mMarginPrimary;
885                }
886                start = end - length;
887                mRows[rowIndex].low = start;
888            }
889            if (mFirstVisiblePos < 0) {
890                mFirstVisiblePos = mLastVisiblePos = index;
891            } else {
892                if (append) {
893                    mLastVisiblePos++;
894                } else {
895                    mFirstVisiblePos--;
896                }
897            }
898            int startSecondary = rowIndex * (mItemLengthSecondary + mMarginSecondary);
899            layoutChild(v, start - mScrollOffsetPrimary, end - mScrollOffsetPrimary,
900                    startSecondary - mScrollOffsetSecondary);
901            if (DEBUG) {
902                Log.d(getTag(), "addView " + index + " " + v);
903            }
904            updateScrollMin();
905            updateScrollMax();
906        }
907    };
908
909    private void layoutChild(View v, int start, int end, int startSecondary) {
910         int measuredSecondary = mOrientation == HORIZONTAL ? v.getMeasuredHeight()
911                 : v.getMeasuredWidth();
912        if (measuredSecondary > mItemLengthSecondary) {
913           measuredSecondary = mItemLengthSecondary;
914        }
915        final int verticalGravity = mGravity & Gravity.VERTICAL_GRAVITY_MASK;
916        final int horizontalGravity = mGravity & Gravity.HORIZONTAL_GRAVITY_MASK;
917        if (mOrientation == HORIZONTAL && verticalGravity == Gravity.TOP
918                || mOrientation == VERTICAL && horizontalGravity == Gravity.LEFT) {
919            // do nothing
920        } else if (mOrientation == HORIZONTAL && verticalGravity == Gravity.BOTTOM
921                || mOrientation == VERTICAL && horizontalGravity == Gravity.RIGHT) {
922            startSecondary += mItemLengthSecondary - measuredSecondary;
923        } else if (mOrientation == HORIZONTAL && verticalGravity == Gravity.CENTER_VERTICAL
924                || mOrientation == VERTICAL && horizontalGravity == Gravity.CENTER_HORIZONTAL) {
925            startSecondary += (mItemLengthSecondary - measuredSecondary) / 2;
926        }
927        if (mOrientation == HORIZONTAL) {
928            v.layout(start, startSecondary, end, startSecondary + measuredSecondary);
929        } else {
930            v.layout(startSecondary, start, startSecondary + measuredSecondary, end);
931        }
932        updateChildAlignments(v);
933    }
934
935    private void updateChildAlignments(View v) {
936        LayoutParams p = (LayoutParams) v.getLayoutParams();
937        p.setAlignX(mItemAlignment.horizontal.getAlignmentPosition(v));
938        p.setAlignY(mItemAlignment.vertical.getAlignmentPosition(v));
939    }
940
941    private void updateChildAlignments() {
942        for (int i = 0, c = getChildCount(); i < c; i++) {
943            updateChildAlignments(getChildAt(i));
944        }
945    }
946
947    /**
948     * append invisible view of saved location
949     */
950    private void appendViewWithSavedLocation() {
951        int index = mLastVisiblePos + 1;
952        mGridProvider.createItem(index, mGrid.getLocation(index).row, true);
953    }
954
955    /**
956     * prepend invisible view of saved location
957     */
958    private void prependViewWithSavedLocation() {
959        int index = mFirstVisiblePos - 1;
960        mGridProvider.createItem(index, mGrid.getLocation(index).row, false);
961    }
962
963    private boolean needsAppendVisibleItem() {
964        if (mLastVisiblePos < mFocusPosition) {
965            return true;
966        }
967        int right = mScrollOffsetPrimary + mSizePrimary;
968        for (int i = 0; i < mNumRows; i++) {
969            if (mRows[i].low == mRows[i].high) {
970                if (mRows[i].high < right) {
971                    return true;
972                }
973            } else if (mRows[i].high < right - mMarginPrimary) {
974                return true;
975            }
976        }
977        return false;
978    }
979
980    private boolean needsPrependVisibleItem() {
981        if (mFirstVisiblePos > mFocusPosition) {
982            return true;
983        }
984        for (int i = 0; i < mNumRows; i++) {
985            if (mRows[i].low == mRows[i].high) {
986                if (mRows[i].low > mScrollOffsetPrimary) {
987                    return true;
988                }
989            } else if (mRows[i].low - mMarginPrimary > mScrollOffsetPrimary) {
990                return true;
991            }
992        }
993        return false;
994    }
995
996    // Append one column if possible and return true if reach end.
997    private boolean appendOneVisibleItem() {
998        if (mLastVisiblePos >= 0 && mLastVisiblePos < mGrid.getLastIndex()) {
999            appendViewWithSavedLocation();
1000        } else if (mLastVisiblePos < mAdapter.getItemCount() - 1) {
1001            mGrid.appendItems(mScrollOffsetPrimary + mSizePrimary);
1002        } else {
1003            return true;
1004        }
1005        return false;
1006    }
1007
1008    private void appendVisibleItems() {
1009        while (needsAppendVisibleItem()) {
1010            if (appendOneVisibleItem()) {
1011                break;
1012            }
1013        }
1014    }
1015
1016    // Prepend one column if possible and return true if reach end.
1017    private boolean prependOneVisibleItem() {
1018        if (mFirstVisiblePos > 0) {
1019            if (mFirstVisiblePos > mGrid.getFirstIndex()) {
1020                prependViewWithSavedLocation();
1021            } else {
1022                mGrid.prependItems(mScrollOffsetPrimary);
1023            }
1024        } else {
1025            return true;
1026        }
1027        return false;
1028    }
1029
1030    private void prependVisibleItems() {
1031        while (needsPrependVisibleItem()) {
1032            if (prependOneVisibleItem()) {
1033                break;
1034            }
1035        }
1036    }
1037
1038    private void removeChildAt(int position) {
1039        View v = getViewByPosition(position);
1040        if (v != null) {
1041            if (DEBUG) {
1042                Log.d(getTag(), "removeAndRecycleViewAt " + position);
1043            }
1044            ((LayoutParams) v.getLayoutParams()).onViewDetached();
1045            removeAndRecycleViewAt(getIndexByPosition(position), mRecycler);
1046        }
1047    }
1048
1049    private void removeInvisibleViewsAtEnd() {
1050        boolean update = false;
1051        while(mLastVisiblePos > mFirstVisiblePos && mLastVisiblePos > mFocusPosition) {
1052            View view = getViewByPosition(mLastVisiblePos);
1053            if (getViewMin(view) > mSizePrimary) {
1054                removeChildAt(mLastVisiblePos);
1055                mLastVisiblePos--;
1056                update = true;
1057            } else {
1058                break;
1059            }
1060        }
1061        if (update) {
1062            updateRowsMinMax();
1063        }
1064    }
1065
1066    private void removeInvisibleViewsAtFront() {
1067        boolean update = false;
1068        while(mLastVisiblePos > mFirstVisiblePos && mFirstVisiblePos < mFocusPosition) {
1069            View view = getViewByPosition(mFirstVisiblePos);
1070            if (getViewMax(view) < 0) {
1071                removeChildAt(mFirstVisiblePos);
1072                mFirstVisiblePos++;
1073                update = true;
1074            } else {
1075                break;
1076            }
1077        }
1078        if (update) {
1079            updateRowsMinMax();
1080        }
1081    }
1082
1083    private void updateRowsMinMax() {
1084        if (mFirstVisiblePos < 0) {
1085            return;
1086        }
1087        for (int i = 0; i < mNumRows; i++) {
1088            mRows[i].low = Integer.MAX_VALUE;
1089            mRows[i].high = Integer.MIN_VALUE;
1090        }
1091        for (int i = mFirstVisiblePos; i <= mLastVisiblePos; i++) {
1092            View view = getViewByPosition(i);
1093            int row = mGrid.getLocation(i).row;
1094            int low = getViewMin(view) + mScrollOffsetPrimary;
1095            if (low < mRows[row].low) {
1096                mRows[row].low = low;
1097            }
1098            int high = getViewMax(view) + mScrollOffsetPrimary;
1099            if (high > mRows[row].high) {
1100                mRows[row].high = high;
1101            }
1102        }
1103    }
1104
1105    /**
1106     * Relayout and re-positioning child for a possible new size and/or a new
1107     * start.
1108     *
1109     * @param view View to measure and layout.
1110     * @param start New start of the view or Integer.MIN_VALUE for not change.
1111     * @return New start of next view.
1112     */
1113    private int updateChildView(View view, int start, int startSecondary) {
1114        if (start == Integer.MIN_VALUE) {
1115            start = getViewMin(view);
1116        }
1117        int end;
1118        if (mOrientation == HORIZONTAL) {
1119            if (view.isLayoutRequested() || view.getMeasuredHeight() != mItemLengthSecondary) {
1120                measureChild(view);
1121            }
1122            end = start + view.getMeasuredWidth();
1123        } else {
1124            if (view.isLayoutRequested() || view.getMeasuredWidth() != mItemLengthSecondary) {
1125                measureChild(view);
1126            }
1127            end = start + view.getMeasuredHeight();
1128        }
1129
1130        layoutChild(view, start, end, startSecondary);
1131        return end + mMarginPrimary;
1132    }
1133
1134    // create a temporary structure that remembers visible items from left to
1135    // right on each row
1136    private ArrayList<Integer>[] buildRows() {
1137        ArrayList<Integer>[] rows = new ArrayList[mNumRows];
1138        for (int i = 0; i < mNumRows; i++) {
1139            rows[i] = new ArrayList();
1140        }
1141        if (mFirstVisiblePos >= 0) {
1142            for (int i = mFirstVisiblePos; i <= mLastVisiblePos; i++) {
1143                rows[mGrid.getLocation(i).row].add(i);
1144            }
1145        }
1146        return rows;
1147    }
1148
1149    // Fast layout when there is no structure change, adapter change, etc.
1150    protected void fastRelayout() {
1151        initScrollController();
1152
1153        ArrayList<Integer>[] rows = buildRows();
1154
1155        // relayout and repositioning views on each row
1156        for (int i = 0; i < mNumRows; i++) {
1157            ArrayList<Integer> row = rows[i];
1158            int start = Integer.MIN_VALUE;
1159            int startSecondary =
1160                i * (mItemLengthSecondary + mMarginSecondary) - mScrollOffsetSecondary;
1161            for (int j = 0, size = row.size(); j < size; j++) {
1162                int position = row.get(j);
1163                start = updateChildView(getViewByPosition(position), start, startSecondary);
1164            }
1165        }
1166
1167        appendVisibleItems();
1168        prependVisibleItems();
1169
1170        updateRowsMinMax();
1171        updateScrollMin();
1172        updateScrollMax();
1173
1174        View focusView = getViewByPosition(mFocusPosition == NO_POSITION ? 0 : mFocusPosition);
1175        if (focusView != null) {
1176            scrollToView(focusView, false);
1177        }
1178    }
1179
1180    // Lays out items based on the current scroll position
1181    @Override
1182    public void layoutChildren(RecyclerView.Adapter adapter, RecyclerView.Recycler recycler,
1183            boolean structureChanged) {
1184        if (DEBUG) {
1185            Log.v(getTag(), "layoutChildren start numRows " + mNumRows + " mScrollOffsetSecondary "
1186                    + mScrollOffsetSecondary + " mScrollOffsetPrimary " + mScrollOffsetPrimary
1187                    + " structureChanged " + structureChanged
1188                    + " mForceFullLayout " + mForceFullLayout);
1189            Log.v(getTag(), "width " + getWidth() + " height " + getHeight());
1190        }
1191
1192        if (mNumRows == 0) {
1193            // haven't done measure yet
1194            return;
1195        }
1196        final int itemCount = adapter.getItemCount();
1197        if (itemCount < 0) {
1198            return;
1199        }
1200
1201        mInLayout = true;
1202
1203        // Track the old focus view so we can adjust our system scroll position
1204        // so that any scroll animations happening now will remain valid.
1205        int delta = 0, deltaSecondary = 0;
1206        if (mFocusPosition != NO_POSITION) {
1207            View focusView = getViewByPosition(mFocusPosition);
1208            if (focusView != null) {
1209                delta = mWindowAlignment.mainAxis().getSystemScrollPos(
1210                        getViewCenter(focusView) + mScrollOffsetPrimary) - mScrollOffsetPrimary;
1211                deltaSecondary =
1212                    mWindowAlignment.secondAxis().getSystemScrollPos(
1213                            getViewCenterSecondary(focusView) + mScrollOffsetSecondary)
1214                    - mScrollOffsetSecondary;
1215            }
1216        }
1217
1218        boolean hasDoneFirstLayout = hasDoneFirstLayout();
1219        if (!structureChanged && !mForceFullLayout && hasDoneFirstLayout) {
1220            fastRelayout();
1221        } else {
1222            boolean hadFocus = mBaseGridView.hasFocus();
1223
1224            int newFocusPosition = init(adapter, recycler, mFocusPosition);
1225            if (DEBUG) {
1226                Log.v(getTag(), "mFocusPosition " + mFocusPosition + " newFocusPosition "
1227                    + newFocusPosition);
1228            }
1229
1230            // depending on result of init(), either recreating everything
1231            // or try to reuse the row start positions near mFocusPosition
1232            if (mGrid.getSize() == 0) {
1233                // this is a fresh creating all items, starting from
1234                // mFocusPosition with a estimated row index.
1235                mGrid.setStart(newFocusPosition, StaggeredGrid.START_DEFAULT);
1236
1237                // Can't track the old focus view
1238                delta = deltaSecondary = 0;
1239
1240            } else {
1241                // mGrid remembers Locations for the column that
1242                // contains mFocusePosition and also mRows remembers start
1243                // positions of each row.
1244                // Manually re-create child views for that column
1245                int firstIndex = mGrid.getFirstIndex();
1246                int lastIndex = mGrid.getLastIndex();
1247                for (int i = firstIndex; i <= lastIndex; i++) {
1248                    mGridProvider.createItem(i, mGrid.getLocation(i).row, true);
1249                }
1250            }
1251            // add visible views at end until reach the end of window
1252            appendVisibleItems();
1253            // add visible views at front until reach the start of window
1254            prependVisibleItems();
1255            // multiple rounds: scrollToView of first round may drag first/last child into
1256            // "visible window" and we update scrollMin/scrollMax then run second scrollToView
1257            int oldFirstVisible;
1258            int oldLastVisible;
1259            do {
1260                oldFirstVisible = mFirstVisiblePos;
1261                oldLastVisible = mLastVisiblePos;
1262                View focusView = getViewByPosition(newFocusPosition);
1263                // we need force to initialize the child view's position
1264                scrollToView(focusView, false);
1265                if (focusView != null && hadFocus) {
1266                    focusView.requestFocus();
1267                }
1268                appendVisibleItems();
1269                prependVisibleItems();
1270                removeInvisibleViewsAtFront();
1271                removeInvisibleViewsAtEnd();
1272            } while (mFirstVisiblePos != oldFirstVisible || mLastVisiblePos != oldLastVisible);
1273        }
1274        mForceFullLayout = false;
1275
1276        scrollDirectionPrimary(-delta);
1277        scrollDirectionSecondary(-deltaSecondary);
1278        appendVisibleItems();
1279        prependVisibleItems();
1280        removeInvisibleViewsAtFront();
1281        removeInvisibleViewsAtEnd();
1282
1283        if (DEBUG) {
1284            StringWriter sw = new StringWriter();
1285            PrintWriter pw = new PrintWriter(sw);
1286            mGrid.debugPrint(pw);
1287            Log.d(getTag(), sw.toString());
1288        }
1289
1290        removeAndRecycleScrap(recycler);
1291        attemptAnimateLayoutChild();
1292
1293        if (!hasDoneFirstLayout) {
1294            dispatchChildSelected();
1295        }
1296        mInLayout = false;
1297        if (DEBUG) Log.v(getTag(), "layoutChildren end");
1298    }
1299
1300    private void offsetChildrenSecondary(int increment) {
1301        final int childCount = getChildCount();
1302        if (mOrientation == HORIZONTAL) {
1303            for (int i = 0; i < childCount; i++) {
1304                getChildAt(i).offsetTopAndBottom(increment);
1305            }
1306        } else {
1307            for (int i = 0; i < childCount; i++) {
1308                getChildAt(i).offsetLeftAndRight(increment);
1309            }
1310        }
1311        mScrollOffsetSecondary -= increment;
1312    }
1313
1314    private void offsetChildrenPrimary(int increment) {
1315        final int childCount = getChildCount();
1316        if (mOrientation == VERTICAL) {
1317            for (int i = 0; i < childCount; i++) {
1318                getChildAt(i).offsetTopAndBottom(increment);
1319            }
1320        } else {
1321            for (int i = 0; i < childCount; i++) {
1322                getChildAt(i).offsetLeftAndRight(increment);
1323            }
1324        }
1325        mScrollOffsetPrimary -= increment;
1326    }
1327
1328    @Override
1329    public int scrollHorizontallyBy(int dx, Adapter adapter, Recycler recycler) {
1330        if (DEBUG) Log.v(TAG, "scrollHorizontallyBy " + dx);
1331
1332        if (mOrientation == HORIZONTAL) {
1333            return scrollDirectionPrimary(dx);
1334        } else {
1335            return scrollDirectionSecondary(dx);
1336        }
1337    }
1338
1339    @Override
1340    public int scrollVerticallyBy(int dy, Adapter adapter, Recycler recycler) {
1341        if (DEBUG) Log.v(TAG, "scrollVerticallyBy " + dy);
1342        if (mOrientation == VERTICAL) {
1343            return scrollDirectionPrimary(dy);
1344        } else {
1345            return scrollDirectionSecondary(dy);
1346        }
1347    }
1348
1349    // scroll in main direction may add/prune views
1350    private int scrollDirectionPrimary(int da) {
1351        offsetChildrenPrimary(-da);
1352        if (mInLayout) {
1353            return da;
1354        }
1355        if (da > 0) {
1356            appendVisibleItems();
1357            removeInvisibleViewsAtFront();
1358        } else if (da < 0) {
1359            prependVisibleItems();
1360            removeInvisibleViewsAtEnd();
1361        }
1362        attemptAnimateLayoutChild();
1363        mBaseGridView.invalidate();
1364        return da;
1365    }
1366
1367    // scroll in second direction will not add/prune views
1368    private int scrollDirectionSecondary(int dy) {
1369        offsetChildrenSecondary(-dy);
1370        mBaseGridView.invalidate();
1371        return dy;
1372    }
1373
1374    private void updateScrollMax() {
1375        if (mLastVisiblePos >= 0 && mLastVisiblePos == mAdapter.getItemCount() - 1) {
1376            int maxEdge = Integer.MIN_VALUE;
1377            for (int i = 0; i < mRows.length; i++) {
1378                if (mRows[i].high > maxEdge) {
1379                    maxEdge = mRows[i].high;
1380                }
1381            }
1382            mWindowAlignment.mainAxis().setMaxEdge(maxEdge);
1383            if (DEBUG) Log.v(getTag(), "updating scroll maxEdge to " + maxEdge);
1384        }
1385    }
1386
1387    private void updateScrollMin() {
1388        if (mFirstVisiblePos == 0) {
1389            int minEdge = Integer.MAX_VALUE;
1390            for (int i = 0; i < mRows.length; i++) {
1391                if (mRows[i].low < minEdge) {
1392                    minEdge = mRows[i].low;
1393                }
1394            }
1395            mWindowAlignment.mainAxis().setMinEdge(minEdge);
1396            if (DEBUG) Log.v(getTag(), "updating scroll minEdge to " + minEdge);
1397        }
1398    }
1399
1400    private void initScrollController() {
1401        mWindowAlignment.horizontal.setSize(getWidth());
1402        mWindowAlignment.horizontal.setPadding(getPaddingLeft(), getPaddingRight());
1403        mWindowAlignment.vertical.setSize(getHeight());
1404        mWindowAlignment.vertical.setPadding(getPaddingTop(), getPaddingBottom());
1405        mSizePrimary = mWindowAlignment.mainAxis().getSize();
1406        mWindowAlignment.mainAxis().invalidateScrollMin();
1407        mWindowAlignment.mainAxis().invalidateScrollMax();
1408
1409        // second axis min/max is determined at initialization, the mainAxis
1410        // min/max is determined later when we scroll to first or last item
1411        mWindowAlignment.secondAxis().setMinEdge(0);
1412        mWindowAlignment.secondAxis().setMaxEdge(mItemLengthSecondary * mNumRows + mMarginSecondary
1413                * (mNumRows - 1));
1414
1415        if (DEBUG) {
1416            Log.v(getTag(), "initScrollController mSizePrimary " + mSizePrimary + " "
1417                + " mItemLengthSecondary " + mItemLengthSecondary + " " + mWindowAlignment);
1418        }
1419    }
1420
1421    public void setSelection(RecyclerView parent, int position) {
1422        setSelection(parent, position, false);
1423    }
1424
1425    public void setSelectionSmooth(RecyclerView parent, int position) {
1426        setSelection(parent, position, true);
1427    }
1428
1429    public int getSelection() {
1430        return mFocusPosition;
1431    }
1432
1433    public void setSelection(RecyclerView parent, int position, boolean smooth) {
1434        if (mFocusPosition == position) {
1435            return;
1436        }
1437        View view = getViewByPosition(position);
1438        if (view != null) {
1439            scrollToView(view, smooth);
1440        } else {
1441            boolean right = position > mFocusPosition;
1442            mFocusPosition = position;
1443            if (smooth) {
1444                if (!hasDoneFirstLayout()) {
1445                    Log.w(getTag(), "setSelectionSmooth should " +
1446                            "not be called before first layout pass");
1447                    return;
1448                }
1449                if (right) {
1450                    appendVisibleItems();
1451                } else {
1452                    prependVisibleItems();
1453                }
1454                view = getViewByPosition(position);
1455                if (view != null) {
1456                    scrollToView(view, smooth);
1457                }
1458            } else {
1459                mForceFullLayout = true;
1460                parent.requestLayout();
1461            }
1462        }
1463    }
1464
1465    @Override
1466    public void onItemsAdded(RecyclerView recyclerView, int positionStart, int itemCount) {
1467        boolean needsLayout = false;
1468        if (itemCount != 0) {
1469            if (mFirstVisiblePos < 0) {
1470                needsLayout = true;
1471            } else if (!(positionStart > mLastVisiblePos + 1 ||
1472                    positionStart + itemCount < mFirstVisiblePos - 1)) {
1473                needsLayout = true;
1474            }
1475        }
1476        if (needsLayout) {
1477            recyclerView.requestLayout();
1478        }
1479    }
1480
1481    @Override
1482    public boolean onRequestChildFocus(RecyclerView parent, View child, View focused) {
1483        if (!mInLayout) {
1484            scrollToView(child, true);
1485        }
1486        return true;
1487    }
1488
1489    @Override
1490    public boolean requestChildRectangleOnScreen(RecyclerView parent, View view, Rect rect,
1491            boolean immediate) {
1492        if (DEBUG) Log.v(getTag(), "requestChildRectangleOnScreen " + view + " " + rect);
1493        return false;
1494    }
1495
1496    int getScrollOffsetX() {
1497        return mOrientation == HORIZONTAL ? mScrollOffsetPrimary : mScrollOffsetSecondary;
1498    }
1499
1500    int getScrollOffsetY() {
1501        return mOrientation == HORIZONTAL ? mScrollOffsetSecondary : mScrollOffsetPrimary;
1502    }
1503
1504    public void getViewSelectedOffsets(View view, int[] offsets) {
1505        int scrollOffsetX = getScrollOffsetX();
1506        int scrollOffsetY = getScrollOffsetY();
1507        int viewCenterX = scrollOffsetX + getViewCenterX(view);
1508        int viewCenterY = scrollOffsetY + getViewCenterY(view);
1509        offsets[0] = mWindowAlignment.horizontal.getSystemScrollPos(viewCenterX) - scrollOffsetX;
1510        offsets[1] = mWindowAlignment.vertical.getSystemScrollPos(viewCenterY) - scrollOffsetY;
1511    }
1512
1513    /**
1514     * Scroll to a given child view and change mFocusPosition.
1515     */
1516    private void scrollToView(View view, boolean smooth) {
1517        int newFocusPosition = getPositionByView(view);
1518        if (mInLayout || newFocusPosition != mFocusPosition) {
1519            mFocusPosition = newFocusPosition;
1520            dispatchChildSelected();
1521        }
1522        if (view == null) {
1523            return;
1524        }
1525        if (!view.hasFocus() && mBaseGridView.hasFocus()) {
1526            // transfer focus to the child if it does not have focus yet (e.g. triggered
1527            // by setSelection())
1528            view.requestFocus();
1529        }
1530        int viewCenterY = getScrollOffsetY() + getViewCenterY(view);
1531        int viewCenterX = getScrollOffsetX() + getViewCenterX(view);
1532        if (DEBUG) {
1533            Log.v(getTag(), "scrollToView smooth=" + smooth + " pos=" + mFocusPosition + " "
1534                    + viewCenterX+","+viewCenterY + " " + mWindowAlignment);
1535        }
1536
1537        if (mInLayout || viewCenterX != mWindowAlignment.horizontal.getScrollCenter()
1538                || viewCenterY != mWindowAlignment.vertical.getScrollCenter()) {
1539            mWindowAlignment.horizontal.updateScrollCenter(viewCenterX);
1540            mWindowAlignment.vertical.updateScrollCenter(viewCenterY);
1541            int scrollX = mWindowAlignment.horizontal.getSystemScrollPos();
1542            int scrollY = mWindowAlignment.vertical.getSystemScrollPos();
1543            if (DEBUG) {
1544                Log.v(getTag(), "adjustSystemScrollPos " + scrollX + " " + scrollY + " "
1545                    + mWindowAlignment);
1546            }
1547
1548            scrollX -= getScrollOffsetX();
1549            scrollY -= getScrollOffsetY();
1550
1551            if (DEBUG) Log.v(getTag(), "scrollX " + scrollX + " scrollY " + scrollY);
1552
1553            if (mInLayout) {
1554                if (mOrientation == HORIZONTAL) {
1555                    scrollDirectionPrimary(scrollX);
1556                    scrollDirectionSecondary(scrollY);
1557                } else {
1558                    scrollDirectionPrimary(scrollY);
1559                    scrollDirectionSecondary(scrollX);
1560                }
1561            } else if (smooth) {
1562                mBaseGridView.smoothScrollBy(scrollX, scrollY);
1563            } else {
1564                mBaseGridView.scrollBy(scrollX, scrollY);
1565            }
1566        }
1567    }
1568
1569    public void setAnimateChildLayout(boolean animateChildLayout) {
1570        mAnimateChildLayout = animateChildLayout;
1571        if (!mAnimateChildLayout) {
1572            for (int i = 0, c = getChildCount(); i < c; i++) {
1573                ((LayoutParams) getChildAt(i).getLayoutParams()).endAnimate();
1574            }
1575        }
1576    }
1577
1578    private void attemptAnimateLayoutChild() {
1579        for (int i = 0, c = getChildCount(); i < c; i++) {
1580            // TODO: start delay can be staggered
1581            View v = getChildAt(i);
1582            ((LayoutParams) v.getLayoutParams()).startAnimate(this, v, 0);
1583        }
1584    }
1585
1586    public boolean isChildLayoutAnimated() {
1587        return mAnimateChildLayout;
1588    }
1589
1590    public void setChildLayoutAnimationInterpolator(Interpolator interpolator) {
1591        mAnimateLayoutChildInterpolator = interpolator;
1592    }
1593
1594    public Interpolator getChildLayoutAnimationInterpolator() {
1595        return mAnimateLayoutChildInterpolator;
1596    }
1597
1598    public void setChildLayoutAnimationDuration(long duration) {
1599        mAnimateLayoutChildDuration = duration;
1600    }
1601
1602    public long getChildLayoutAnimationDuration() {
1603        return mAnimateLayoutChildDuration;
1604    }
1605
1606    private int findImmediateChildIndex(View view) {
1607        while (view != null && view != mBaseGridView) {
1608            int index = mBaseGridView.indexOfChild(view);
1609            if (index >= 0) {
1610                return index;
1611            }
1612            view = (View) view.getParent();
1613        }
1614        return NO_POSITION;
1615    }
1616
1617    @Override
1618    public boolean onAddFocusables(RecyclerView recyclerView,
1619            ArrayList<View> views, int direction, int focusableMode) {
1620        // If this viewgroup or one of its children currently has focus then we
1621        // consider our children for focus searching.
1622        // Otherwise, we only want the system to ignore our children and pass
1623        // focus to the viewgroup, which will pass focus on to its children
1624        // appropriately.
1625        if (recyclerView.hasFocus()) {
1626            final int movement = getMovement(direction);
1627            if (movement != PREV_ITEM && movement != NEXT_ITEM) {
1628                // Move on secondary direction uses default addFocusables().
1629                return false;
1630            }
1631            // Get current focus row.
1632            final View focused = recyclerView.findFocus();
1633            final int focusedPos = getPositionByIndex(findImmediateChildIndex(focused));
1634            final int focusedRow = mGrid != null && focusedPos != NO_POSITION ?
1635                    mGrid.getLocation(focusedPos).row : NO_POSITION;
1636            // Add focusables within the same row.
1637            final int focusableCount = views.size();
1638            final int descendantFocusability = recyclerView.getDescendantFocusability();
1639            if (mGrid != null && descendantFocusability != ViewGroup.FOCUS_BLOCK_DESCENDANTS) {
1640                for (int i = 0, count = getChildCount(); i < count; i++) {
1641                    final View child = getChildAt(i);
1642                    if (child.getVisibility() != View.VISIBLE) {
1643                        continue;
1644                    }
1645                    StaggeredGrid.Location loc = mGrid.getLocation(getPositionByIndex(i));
1646                    if (focusedRow == NO_POSITION || (loc != null && loc.row == focusedRow)) {
1647                        child.addFocusables(views,  direction, focusableMode);
1648                    }
1649                }
1650            }
1651            // From ViewGroup.addFocusables():
1652            // we add ourselves (if focusable) in all cases except for when we are
1653            // FOCUS_AFTER_DESCENDANTS and there are some descendants focusable.  this is
1654            // to avoid the focus search finding layouts when a more precise search
1655            // among the focusable children would be more interesting.
1656            if (descendantFocusability != ViewGroup.FOCUS_AFTER_DESCENDANTS
1657                    // No focusable descendants
1658                    || (focusableCount == views.size())) {
1659                if (recyclerView.isFocusable()) {
1660                    views.add(recyclerView);
1661                }
1662            }
1663        } else {
1664            if (recyclerView.isFocusable()) {
1665                views.add(recyclerView);
1666            }
1667        }
1668        return true;
1669    }
1670
1671    @Override
1672    public View onFocusSearchFailed(View focused, int direction, Adapter adapter,
1673            Recycler recycler) {
1674        if (DEBUG) Log.v(getTag(), "onFocusSearchFailed direction " + direction);
1675
1676        View view = null;
1677        int movement = getMovement(direction);
1678        final FocusFinder ff = FocusFinder.getInstance();
1679        if (movement == NEXT_ITEM) {
1680            while (view == null && !appendOneVisibleItem()) {
1681                view = ff.findNextFocus(mBaseGridView, focused, direction);
1682            }
1683        } else if (movement == PREV_ITEM){
1684            while (view == null && !prependOneVisibleItem()) {
1685                view = ff.findNextFocus(mBaseGridView, focused, direction);
1686            }
1687        }
1688        if (view == null) {
1689            // returning the same view to prevent focus lost when scrolling past the end of the list
1690            if (movement == PREV_ITEM) {
1691                view = mFocusOutFront ? null : focused;
1692            } else if (movement == NEXT_ITEM){
1693                view = mFocusOutEnd ? null : focused;
1694            }
1695        }
1696        if (DEBUG) Log.v(getTag(), "returning view " + view);
1697        return view;
1698    }
1699
1700    private final static int PREV_ITEM = 0;
1701    private final static int NEXT_ITEM = 1;
1702    private final static int PREV_ROW = 2;
1703    private final static int NEXT_ROW = 3;
1704
1705    boolean focusSelectedChild(int direction, Rect previouslyFocusedRect) {
1706        View view = getViewByPosition(mFocusPosition);
1707        if (view != null) {
1708            if (!view.requestFocus(direction, previouslyFocusedRect)) {
1709                if (DEBUG) {
1710                    Log.w(getTag(), "failed to request focus on " + view);
1711                }
1712            } else {
1713                return true;
1714            }
1715        }
1716        return false;
1717    }
1718
1719    private int getMovement(int direction) {
1720        int movement = View.FOCUS_LEFT;
1721
1722        if (mOrientation == HORIZONTAL) {
1723            switch(direction) {
1724                case View.FOCUS_LEFT:
1725                    movement = PREV_ITEM;
1726                    break;
1727                case View.FOCUS_RIGHT:
1728                    movement = NEXT_ITEM;
1729                    break;
1730                case View.FOCUS_UP:
1731                    movement = PREV_ROW;
1732                    break;
1733                case View.FOCUS_DOWN:
1734                    movement = NEXT_ROW;
1735                    break;
1736            }
1737         } else if (mOrientation == VERTICAL) {
1738             switch(direction) {
1739                 case View.FOCUS_LEFT:
1740                     movement = PREV_ROW;
1741                     break;
1742                 case View.FOCUS_RIGHT:
1743                     movement = NEXT_ROW;
1744                     break;
1745                 case View.FOCUS_UP:
1746                     movement = PREV_ITEM;
1747                     break;
1748                 case View.FOCUS_DOWN:
1749                     movement = NEXT_ITEM;
1750                     break;
1751             }
1752         }
1753
1754        return movement;
1755    }
1756
1757    @Override
1758    public void onAdapterChanged(RecyclerView.Adapter oldAdapter,
1759            RecyclerView.Adapter newAdapter) {
1760        mGrid = null;
1761        mRows = null;
1762        super.onAdapterChanged(oldAdapter, newAdapter);
1763    }
1764
1765}
1766