GridLayoutManager.java revision 2d8ec4a44f2ffcfb83247c5f0e12e45aec7e4174
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 onLayoutChildren(RecyclerView.Adapter adapter, RecyclerView.Recycler recycler,
1209            boolean structureChanged, RecyclerView.State state) {
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            RecyclerView.State state) {
1360        if (DEBUG) Log.v(TAG, "scrollHorizontallyBy " + dx);
1361
1362        if (mOrientation == HORIZONTAL) {
1363            return scrollDirectionPrimary(dx);
1364        } else {
1365            return scrollDirectionSecondary(dx);
1366        }
1367    }
1368
1369    @Override
1370    public int scrollVerticallyBy(int dy, Adapter adapter, Recycler recycler,
1371            RecyclerView.State state) {
1372        if (DEBUG) Log.v(TAG, "scrollVerticallyBy " + dy);
1373        if (mOrientation == VERTICAL) {
1374            return scrollDirectionPrimary(dy);
1375        } else {
1376            return scrollDirectionSecondary(dy);
1377        }
1378    }
1379
1380    // scroll in main direction may add/prune views
1381    private int scrollDirectionPrimary(int da) {
1382        offsetChildrenPrimary(-da);
1383        if (mInLayout) {
1384            return da;
1385        }
1386        if (da > 0) {
1387            appendVisibleItems();
1388            removeInvisibleViewsAtFront();
1389        } else if (da < 0) {
1390            prependVisibleItems();
1391            removeInvisibleViewsAtEnd();
1392        }
1393        attemptAnimateLayoutChild();
1394        mBaseGridView.invalidate();
1395        return da;
1396    }
1397
1398    // scroll in second direction will not add/prune views
1399    private int scrollDirectionSecondary(int dy) {
1400        offsetChildrenSecondary(-dy);
1401        mBaseGridView.invalidate();
1402        return dy;
1403    }
1404
1405    private void updateScrollMax() {
1406        if (mLastVisiblePos >= 0 && mLastVisiblePos == mAdapter.getItemCount() - 1) {
1407            int maxEdge = Integer.MIN_VALUE;
1408            for (int i = 0; i < mRows.length; i++) {
1409                if (mRows[i].high > maxEdge) {
1410                    maxEdge = mRows[i].high;
1411                }
1412            }
1413            mWindowAlignment.mainAxis().setMaxEdge(maxEdge);
1414            if (DEBUG) Log.v(getTag(), "updating scroll maxEdge to " + maxEdge);
1415        }
1416    }
1417
1418    private void updateScrollMin() {
1419        if (mFirstVisiblePos == 0) {
1420            int minEdge = Integer.MAX_VALUE;
1421            for (int i = 0; i < mRows.length; i++) {
1422                if (mRows[i].low < minEdge) {
1423                    minEdge = mRows[i].low;
1424                }
1425            }
1426            mWindowAlignment.mainAxis().setMinEdge(minEdge);
1427            if (DEBUG) Log.v(getTag(), "updating scroll minEdge to " + minEdge);
1428        }
1429    }
1430
1431    private void initScrollController() {
1432        mWindowAlignment.horizontal.setSize(getWidth());
1433        mWindowAlignment.horizontal.setPadding(getPaddingLeft(), getPaddingRight());
1434        mWindowAlignment.vertical.setSize(getHeight());
1435        mWindowAlignment.vertical.setPadding(getPaddingTop(), getPaddingBottom());
1436        mSizePrimary = mWindowAlignment.mainAxis().getSize();
1437        mWindowAlignment.mainAxis().invalidateScrollMin();
1438        mWindowAlignment.mainAxis().invalidateScrollMax();
1439
1440        // second axis min/max is determined at initialization, the mainAxis
1441        // min/max is determined later when we scroll to first or last item
1442        mWindowAlignment.secondAxis().setMinEdge(0);
1443        mWindowAlignment.secondAxis().setMaxEdge(mItemLengthSecondary * mNumRows + mMarginSecondary
1444                * (mNumRows - 1));
1445
1446        if (DEBUG) {
1447            Log.v(getTag(), "initScrollController mSizePrimary " + mSizePrimary + " "
1448                + " mItemLengthSecondary " + mItemLengthSecondary + " " + mWindowAlignment);
1449        }
1450    }
1451
1452    public void setSelection(RecyclerView parent, int position) {
1453        setSelection(parent, position, false);
1454    }
1455
1456    public void setSelectionSmooth(RecyclerView parent, int position) {
1457        setSelection(parent, position, true);
1458    }
1459
1460    public int getSelection() {
1461        return mFocusPosition;
1462    }
1463
1464    public void setSelection(RecyclerView parent, int position, boolean smooth) {
1465        if (mFocusPosition == position) {
1466            return;
1467        }
1468        View view = getViewByPosition(position);
1469        if (view != null) {
1470            scrollToView(view, smooth);
1471        } else {
1472            boolean right = position > mFocusPosition;
1473            mFocusPosition = position;
1474            if (smooth) {
1475                if (!hasDoneFirstLayout()) {
1476                    Log.w(getTag(), "setSelectionSmooth should " +
1477                            "not be called before first layout pass");
1478                    return;
1479                }
1480                if (right) {
1481                    appendVisibleItems();
1482                } else {
1483                    prependVisibleItems();
1484                }
1485                scrollToView(getViewByPosition(position), smooth);
1486            } else {
1487                mForceFullLayout = true;
1488                parent.requestLayout();
1489            }
1490        }
1491    }
1492
1493    @Override
1494    public void onItemsAdded(RecyclerView recyclerView, int positionStart, int itemCount) {
1495        boolean needsLayout = false;
1496        if (itemCount != 0) {
1497            if (mFirstVisiblePos < 0) {
1498                needsLayout = true;
1499            } else if (!(positionStart > mLastVisiblePos + 1 ||
1500                    positionStart + itemCount < mFirstVisiblePos - 1)) {
1501                needsLayout = true;
1502            }
1503        }
1504        if (needsLayout) {
1505            recyclerView.requestLayout();
1506        }
1507    }
1508
1509    @Override
1510    public boolean onRequestChildFocus(RecyclerView parent, View child, View focused) {
1511        if (!mInLayout) {
1512            scrollToView(child, true);
1513        }
1514        return true;
1515    }
1516
1517    @Override
1518    public boolean requestChildRectangleOnScreen(RecyclerView parent, View view, Rect rect,
1519            boolean immediate) {
1520        if (DEBUG) Log.v(getTag(), "requestChildRectangleOnScreen " + view + " " + rect);
1521        return false;
1522    }
1523
1524    int getScrollOffsetX() {
1525        return mOrientation == HORIZONTAL ? mScrollOffsetPrimary : mScrollOffsetSecondary;
1526    }
1527
1528    int getScrollOffsetY() {
1529        return mOrientation == HORIZONTAL ? mScrollOffsetSecondary : mScrollOffsetPrimary;
1530    }
1531
1532    public void getViewSelectedOffsets(View view, int[] offsets) {
1533        int scrollOffsetX = getScrollOffsetX();
1534        int scrollOffsetY = getScrollOffsetY();
1535        int viewCenterX = scrollOffsetX + getViewCenterX(view);
1536        int viewCenterY = scrollOffsetY + getViewCenterY(view);
1537        offsets[0] = mWindowAlignment.horizontal.getSystemScrollPos(viewCenterX) - scrollOffsetX;
1538        offsets[1] = mWindowAlignment.vertical.getSystemScrollPos(viewCenterY) - scrollOffsetY;
1539    }
1540
1541    /**
1542     * Scroll to a given child view and change mFocusPosition.
1543     */
1544    private void scrollToView(View view, boolean smooth) {
1545        int newFocusPosition = getPositionByView(view);
1546        if (mInLayout || newFocusPosition != mFocusPosition) {
1547            mFocusPosition = newFocusPosition;
1548            dispatchChildSelected();
1549        }
1550        if (mBaseGridView.isChildrenDrawingOrderEnabledInternal()) {
1551            mBaseGridView.invalidate();
1552        }
1553        if (view == null) {
1554            return;
1555        }
1556        if (!view.hasFocus() && mBaseGridView.hasFocus()) {
1557            // transfer focus to the child if it does not have focus yet (e.g. triggered
1558            // by setSelection())
1559            view.requestFocus();
1560        }
1561        switch (mFocusScrollStrategy) {
1562        case BaseGridView.FOCUS_SCROLL_ALIGNED:
1563        default:
1564            scrollToAlignedPosition(view, smooth);
1565            break;
1566        case BaseGridView.FOCUS_SCROLL_ITEM:
1567        case BaseGridView.FOCUS_SCROLL_PAGE:
1568            scrollItemOrPage(view, smooth);
1569            break;
1570        }
1571    }
1572
1573    private void scrollItemOrPage(View view, boolean smooth) {
1574        int pos = getPositionByView(view);
1575        int viewMin = getViewMin(view);
1576        int viewMax = getViewMax(view);
1577        // we either align "firstView" to left/top padding edge
1578        // or align "lastView" to right/bottom padding edge
1579        View firstView = null;
1580        View lastView = null;
1581        int paddingLow = mWindowAlignment.mainAxis().getPaddingLow();
1582        int clientSize = mWindowAlignment.mainAxis().getClientSize();
1583        final int row = mGrid.getLocation(pos).row;
1584        if (viewMin < paddingLow) {
1585            // view enters low padding area:
1586            firstView = view;
1587            if (mFocusScrollStrategy == BaseGridView.FOCUS_SCROLL_PAGE) {
1588                // scroll one "page" left/top,
1589                // align first visible item of the "page" at the low padding edge.
1590                while (!prependOneVisibleItem()) {
1591                    List<Integer> positions =
1592                            mGrid.getItemPositionsInRows(mFirstVisiblePos, pos)[row];
1593                    firstView = getViewByPosition(positions.get(0));
1594                    if (viewMax - getViewMin(firstView) > clientSize) {
1595                        if (positions.size() > 1) {
1596                            firstView = getViewByPosition(positions.get(1));
1597                        }
1598                        break;
1599                    }
1600                }
1601            }
1602        } else if (viewMax > clientSize + paddingLow) {
1603            // view enters high padding area:
1604            if (mFocusScrollStrategy == BaseGridView.FOCUS_SCROLL_PAGE) {
1605                // scroll whole one page right/bottom, align view at the low padding edge.
1606                firstView = view;
1607                do {
1608                    List<Integer> positions =
1609                            mGrid.getItemPositionsInRows(pos, mLastVisiblePos)[row];
1610                    lastView = getViewByPosition(positions.get(positions.size() - 1));
1611                    if (getViewMax(lastView) - viewMin > clientSize) {
1612                        lastView = null;
1613                        break;
1614                    }
1615                } while (!appendOneVisibleItem());
1616                if (lastView != null) {
1617                    // however if we reached end,  we should align last view.
1618                    firstView = null;
1619                }
1620            } else {
1621                lastView = view;
1622            }
1623        }
1624        int scrollPrimary = 0;
1625        int scrollSecondary = 0;
1626        if (firstView != null) {
1627            scrollPrimary = getViewMin(firstView) - paddingLow;
1628        } else if (lastView != null) {
1629            scrollPrimary = getViewMax(lastView) - (paddingLow + clientSize);
1630        }
1631        View secondaryAlignedView;
1632        if (firstView != null) {
1633            secondaryAlignedView = firstView;
1634        } else if (lastView != null) {
1635            secondaryAlignedView = lastView;
1636        } else {
1637            secondaryAlignedView = view;
1638        }
1639        int viewCenterSecondary = mScrollOffsetSecondary +
1640                getViewCenterSecondary(secondaryAlignedView);
1641        mWindowAlignment.secondAxis().updateScrollCenter(viewCenterSecondary);
1642        scrollSecondary = mWindowAlignment.secondAxis().getSystemScrollPos();
1643        scrollSecondary -= mScrollOffsetSecondary;
1644        scrollGrid(scrollPrimary, scrollSecondary, smooth);
1645    }
1646
1647    private void scrollToAlignedPosition(View view, boolean smooth) {
1648        int viewCenterPrimary = mScrollOffsetPrimary + getViewCenter(view);
1649        int viewCenterSecondary = mScrollOffsetSecondary + getViewCenterSecondary(view);
1650        if (DEBUG) {
1651            Log.v(getTag(), "scrollAligned smooth=" + smooth + " pos=" + mFocusPosition + " "
1652                    + viewCenterPrimary +","+viewCenterSecondary + " " + mWindowAlignment);
1653        }
1654
1655        if (mInLayout || viewCenterPrimary != mWindowAlignment.mainAxis().getScrollCenter()
1656                || viewCenterSecondary != mWindowAlignment.secondAxis().getScrollCenter()) {
1657            mWindowAlignment.mainAxis().updateScrollCenter(viewCenterPrimary);
1658            mWindowAlignment.secondAxis().updateScrollCenter(viewCenterSecondary);
1659            int scrollPrimary = mWindowAlignment.mainAxis().getSystemScrollPos();
1660            int scrollSecondary = mWindowAlignment.secondAxis().getSystemScrollPos();
1661            if (DEBUG) {
1662                Log.v(getTag(), "scrollAligned " + scrollPrimary + " " + scrollSecondary
1663                        +" " + mWindowAlignment);
1664            }
1665
1666            scrollPrimary -= mScrollOffsetPrimary;
1667            scrollSecondary -= mScrollOffsetSecondary;
1668
1669            scrollGrid(scrollPrimary, scrollSecondary, smooth);
1670        }
1671    }
1672
1673    private void scrollGrid(int scrollPrimary, int scrollSecondary, boolean smooth) {
1674        if (mInLayout) {
1675            scrollDirectionPrimary(scrollPrimary);
1676            scrollDirectionSecondary(scrollSecondary);
1677        } else {
1678            int scrollX;
1679            int scrollY;
1680            if (mOrientation == HORIZONTAL) {
1681                scrollX = scrollPrimary;
1682                scrollY = scrollSecondary;
1683            } else {
1684                scrollX = scrollSecondary;
1685                scrollY = scrollPrimary;
1686            }
1687            if (smooth) {
1688                mBaseGridView.smoothScrollBy(scrollX, scrollY);
1689            } else {
1690                mBaseGridView.scrollBy(scrollX, scrollY);
1691            }
1692        }
1693    }
1694
1695    public void setAnimateChildLayout(boolean animateChildLayout) {
1696        mAnimateChildLayout = animateChildLayout;
1697        for (int i = 0, c = getChildCount(); i < c; i++) {
1698            View v = getChildAt(i);
1699            LayoutParams p = (LayoutParams) v.getLayoutParams();
1700            if (!mAnimateChildLayout) {
1701                p.endAnimate();
1702            } else {
1703                // record initial location values
1704                p.mFirstAttached = true;
1705                p.startAnimate(this, v, 0);
1706            }
1707        }
1708    }
1709
1710    private void attemptAnimateLayoutChild() {
1711        if (!mAnimateChildLayout) {
1712            return;
1713        }
1714        for (int i = 0, c = getChildCount(); i < c; i++) {
1715            // TODO: start delay can be staggered
1716            View v = getChildAt(i);
1717            ((LayoutParams) v.getLayoutParams()).startAnimate(this, v, 0);
1718        }
1719    }
1720
1721    public boolean isChildLayoutAnimated() {
1722        return mAnimateChildLayout;
1723    }
1724
1725    public void setChildLayoutAnimationInterpolator(Interpolator interpolator) {
1726        mAnimateLayoutChildInterpolator = interpolator;
1727    }
1728
1729    public Interpolator getChildLayoutAnimationInterpolator() {
1730        return mAnimateLayoutChildInterpolator;
1731    }
1732
1733    public void setChildLayoutAnimationDuration(long duration) {
1734        mAnimateLayoutChildDuration = duration;
1735    }
1736
1737    public long getChildLayoutAnimationDuration() {
1738        return mAnimateLayoutChildDuration;
1739    }
1740
1741    private int findImmediateChildIndex(View view) {
1742        while (view != null && view != mBaseGridView) {
1743            int index = mBaseGridView.indexOfChild(view);
1744            if (index >= 0) {
1745                return index;
1746            }
1747            view = (View) view.getParent();
1748        }
1749        return NO_POSITION;
1750    }
1751
1752    @Override
1753    public boolean onAddFocusables(RecyclerView recyclerView,
1754            ArrayList<View> views, int direction, int focusableMode) {
1755        // If this viewgroup or one of its children currently has focus then we
1756        // consider our children for focus searching in main direction on the same row.
1757        // If this viewgroup has no focus and using focus align, we want the system
1758        // to ignore our children and pass focus to the viewgroup, which will pass
1759        // focus on to its children appropriately.
1760        // If this viewgroup has no focus and not using focus align, we want to
1761        // consider the child that does not overlap with padding area.
1762        if (recyclerView.hasFocus()) {
1763            final int movement = getMovement(direction);
1764            if (movement != PREV_ITEM && movement != NEXT_ITEM) {
1765                // Move on secondary direction uses default addFocusables().
1766                return false;
1767            }
1768            final View focused = recyclerView.findFocus();
1769            final int focusedPos = getPositionByIndex(findImmediateChildIndex(focused));
1770            // Add focusables of focused item.
1771            if (focusedPos != NO_POSITION) {
1772                getViewByPosition(focusedPos).addFocusables(views,  direction, focusableMode);
1773            }
1774            final int focusedRow = mGrid != null && focusedPos != NO_POSITION ?
1775                    mGrid.getLocation(focusedPos).row : NO_POSITION;
1776            // Add focusables of next neighbor of same row on the focus search direction.
1777            if (mGrid != null) {
1778                final int focusableCount = views.size();
1779                for (int i = 0, count = getChildCount(); i < count; i++) {
1780                    int index = movement == NEXT_ITEM ? i : count - 1 - i;
1781                    final View child = getChildAt(index);
1782                    if (child.getVisibility() != View.VISIBLE) {
1783                        continue;
1784                    }
1785                    int position = getPositionByIndex(index);
1786                    StaggeredGrid.Location loc = mGrid.getLocation(position);
1787                    if (focusedRow == NO_POSITION || (loc != null && loc.row == focusedRow)) {
1788                        if (focusedPos == NO_POSITION ||
1789                                (movement == NEXT_ITEM && position > focusedPos)
1790                                || (movement == PREV_ITEM && position < focusedPos)) {
1791                            child.addFocusables(views,  direction, focusableMode);
1792                            if (views.size() > focusableCount) {
1793                                break;
1794                            }
1795                        }
1796                    }
1797                }
1798            }
1799        } else {
1800            if (mFocusScrollStrategy != BaseGridView.FOCUS_SCROLL_ALIGNED) {
1801                // adding views not overlapping padding area to avoid scrolling in gaining focus
1802                int left = mWindowAlignment.mainAxis().getPaddingLow();
1803                int right = mWindowAlignment.mainAxis().getClientSize() + left;
1804                int focusableCount = views.size();
1805                for (int i = 0, count = getChildCount(); i < count; i++) {
1806                    View child = getChildAt(i);
1807                    if (child.getVisibility() == View.VISIBLE) {
1808                        if (getViewMin(child) >= left && getViewMax(child) <= right) {
1809                            child.addFocusables(views, direction, focusableMode);
1810                        }
1811                    }
1812                }
1813                // if we cannot find any, then just add all children.
1814                if (views.size() == focusableCount) {
1815                    for (int i = 0, count = getChildCount(); i < count; i++) {
1816                        View child = getChildAt(i);
1817                        if (child.getVisibility() == View.VISIBLE) {
1818                            child.addFocusables(views, direction, focusableMode);
1819                        }
1820                    }
1821                    if (views.size() != focusableCount) {
1822                        return true;
1823                    }
1824                } else {
1825                    return true;
1826                }
1827                // if still cannot find any, fall through and add itself
1828            }
1829            if (recyclerView.isFocusable()) {
1830                views.add(recyclerView);
1831            }
1832        }
1833        return true;
1834    }
1835
1836    @Override
1837    public View onFocusSearchFailed(View focused, int direction, Adapter adapter,
1838            Recycler recycler) {
1839        if (DEBUG) Log.v(getTag(), "onFocusSearchFailed direction " + direction);
1840
1841        View view = null;
1842        int movement = getMovement(direction);
1843        final FocusFinder ff = FocusFinder.getInstance();
1844        if (movement == NEXT_ITEM) {
1845            while (view == null && !appendOneVisibleItem()) {
1846                view = ff.findNextFocus(mBaseGridView, focused, direction);
1847            }
1848        } else if (movement == PREV_ITEM){
1849            while (view == null && !prependOneVisibleItem()) {
1850                view = ff.findNextFocus(mBaseGridView, focused, direction);
1851            }
1852        }
1853        if (view == null) {
1854            // returning the same view to prevent focus lost when scrolling past the end of the list
1855            if (movement == PREV_ITEM) {
1856                view = mFocusOutFront ? null : focused;
1857            } else if (movement == NEXT_ITEM){
1858                view = mFocusOutEnd ? null : focused;
1859            }
1860        }
1861        if (DEBUG) Log.v(getTag(), "returning view " + view);
1862        return view;
1863    }
1864
1865    boolean gridOnRequestFocusInDescendants(RecyclerView recyclerView, int direction,
1866            Rect previouslyFocusedRect) {
1867        switch (mFocusScrollStrategy) {
1868        case BaseGridView.FOCUS_SCROLL_ALIGNED:
1869        default:
1870            return gridOnRequestFocusInDescendantsAligned(recyclerView,
1871                    direction, previouslyFocusedRect);
1872        case BaseGridView.FOCUS_SCROLL_PAGE:
1873        case BaseGridView.FOCUS_SCROLL_ITEM:
1874            return gridOnRequestFocusInDescendantsUnaligned(recyclerView,
1875                    direction, previouslyFocusedRect);
1876        }
1877    }
1878
1879    private boolean gridOnRequestFocusInDescendantsAligned(RecyclerView recyclerView,
1880            int direction, Rect previouslyFocusedRect) {
1881        View view = getViewByPosition(mFocusPosition);
1882        if (view != null) {
1883            boolean result = view.requestFocus(direction, previouslyFocusedRect);
1884            if (!result && DEBUG) {
1885                Log.w(getTag(), "failed to request focus on " + view);
1886            }
1887            return result;
1888        }
1889        return false;
1890    }
1891
1892    private boolean gridOnRequestFocusInDescendantsUnaligned(RecyclerView recyclerView,
1893            int direction, Rect previouslyFocusedRect) {
1894        // focus to view not overlapping padding area to avoid scrolling in gaining focus
1895        int index;
1896        int increment;
1897        int end;
1898        int count = getChildCount();
1899        if ((direction & View.FOCUS_FORWARD) != 0) {
1900            index = 0;
1901            increment = 1;
1902            end = count;
1903        } else {
1904            index = count - 1;
1905            increment = -1;
1906            end = -1;
1907        }
1908        int left = mWindowAlignment.mainAxis().getPaddingLow();
1909        int right = mWindowAlignment.mainAxis().getClientSize() + left;
1910        for (int i = index; i != end; i += increment) {
1911            View child = getChildAt(i);
1912            if (child.getVisibility() == View.VISIBLE) {
1913                if (getViewMin(child) >= left && getViewMax(child) <= right) {
1914                    if (child.requestFocus(direction, previouslyFocusedRect)) {
1915                        return true;
1916                    }
1917                }
1918            }
1919        }
1920        return false;
1921    }
1922
1923    private final static int PREV_ITEM = 0;
1924    private final static int NEXT_ITEM = 1;
1925    private final static int PREV_ROW = 2;
1926    private final static int NEXT_ROW = 3;
1927
1928    private int getMovement(int direction) {
1929        int movement = View.FOCUS_LEFT;
1930
1931        if (mOrientation == HORIZONTAL) {
1932            switch(direction) {
1933                case View.FOCUS_LEFT:
1934                    movement = PREV_ITEM;
1935                    break;
1936                case View.FOCUS_RIGHT:
1937                    movement = NEXT_ITEM;
1938                    break;
1939                case View.FOCUS_UP:
1940                    movement = PREV_ROW;
1941                    break;
1942                case View.FOCUS_DOWN:
1943                    movement = NEXT_ROW;
1944                    break;
1945            }
1946         } else if (mOrientation == VERTICAL) {
1947             switch(direction) {
1948                 case View.FOCUS_LEFT:
1949                     movement = PREV_ROW;
1950                     break;
1951                 case View.FOCUS_RIGHT:
1952                     movement = NEXT_ROW;
1953                     break;
1954                 case View.FOCUS_UP:
1955                     movement = PREV_ITEM;
1956                     break;
1957                 case View.FOCUS_DOWN:
1958                     movement = NEXT_ITEM;
1959                     break;
1960             }
1961         }
1962
1963        return movement;
1964    }
1965
1966    int getChildDrawingOrder(RecyclerView recyclerView, int childCount, int i) {
1967        int focusIndex = getIndexByPosition(mFocusPosition);
1968        if (focusIndex == NO_POSITION) {
1969            return i;
1970        }
1971        // supposely 0 1 2 3 4 5 6 7 8 9, 4 is the center item
1972        // drawing order is 0 1 2 3 9 8 7 6 5 4
1973        if (i < focusIndex) {
1974            return i;
1975        } else if (i < childCount - 1) {
1976            return focusIndex + childCount - 1 - i;
1977        } else {
1978            return focusIndex;
1979        }
1980    }
1981
1982    @Override
1983    public void onAdapterChanged(RecyclerView.Adapter oldAdapter,
1984            RecyclerView.Adapter newAdapter) {
1985        mGrid = null;
1986        mRows = null;
1987        super.onAdapterChanged(oldAdapter, newAdapter);
1988    }
1989}
1990