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