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