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