GridLayoutManager.java revision e560a9d4e4a73def79923a15ae22109746fa9cbd
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, top, right, bottom;
941        if (mOrientation == HORIZONTAL) {
942            left = start;
943            top = startSecondary;
944            right = end;
945            bottom = startSecondary + measuredSecondary;
946        } else {
947            top = start;
948            left = startSecondary;
949            bottom = end;
950            right = startSecondary + measuredSecondary;
951        }
952        v.layout(left, top, right, bottom);
953        updateChildOpticalInsets(v, left, top, right, bottom);
954        updateChildAlignments(v);
955    }
956
957    private void updateChildOpticalInsets(View v, int left, int top, int right, int bottom) {
958        LayoutParams p = (LayoutParams) v.getLayoutParams();
959        p.setOpticalInsets(left - v.getLeft(), top - v.getTop(),
960                v.getRight() - right, v.getBottom() - bottom);
961    }
962
963    private void updateChildAlignments(View v) {
964        LayoutParams p = (LayoutParams) v.getLayoutParams();
965        p.setAlignX(mItemAlignment.horizontal.getAlignmentPosition(v));
966        p.setAlignY(mItemAlignment.vertical.getAlignmentPosition(v));
967    }
968
969    private void updateChildAlignments() {
970        for (int i = 0, c = getChildCount(); i < c; i++) {
971            updateChildAlignments(getChildAt(i));
972        }
973    }
974
975    private boolean needsAppendVisibleItem() {
976        if (mLastVisiblePos < mFocusPosition) {
977            return true;
978        }
979        int right = mScrollOffsetPrimary + mSizePrimary;
980        for (int i = 0; i < mNumRows; i++) {
981            if (mRows[i].low == mRows[i].high) {
982                if (mRows[i].high < right) {
983                    return true;
984                }
985            } else if (mRows[i].high < right - mMarginPrimary) {
986                return true;
987            }
988        }
989        return false;
990    }
991
992    private boolean needsPrependVisibleItem() {
993        if (mFirstVisiblePos > mFocusPosition) {
994            return true;
995        }
996        for (int i = 0; i < mNumRows; i++) {
997            if (mRows[i].low == mRows[i].high) {
998                if (mRows[i].low > mScrollOffsetPrimary) {
999                    return true;
1000                }
1001            } else if (mRows[i].low - mMarginPrimary > mScrollOffsetPrimary) {
1002                return true;
1003            }
1004        }
1005        return false;
1006    }
1007
1008    // Append one column if possible and return true if reach end.
1009    private boolean appendOneVisibleItem() {
1010        while (true) {
1011            if (mLastVisiblePos >= 0 && mLastVisiblePos < mGrid.getLastIndex()) {
1012                // append invisible view of saved location till last row
1013                final int index = mLastVisiblePos + 1;
1014                final int row = mGrid.getLocation(index).row;
1015                mGridProvider.createItem(index, row, true);
1016                if (row == mNumRows - 1) {
1017                    return false;
1018                }
1019            } else if (mLastVisiblePos < mAdapter.getItemCount() - 1) {
1020                mGrid.appendItems(mScrollOffsetPrimary + mSizePrimary);
1021                return false;
1022            } else {
1023                return true;
1024            }
1025        }
1026    }
1027
1028    private void appendVisibleItems() {
1029        while (needsAppendVisibleItem()) {
1030            if (appendOneVisibleItem()) {
1031                break;
1032            }
1033        }
1034    }
1035
1036    // Prepend one column if possible and return true if reach end.
1037    private boolean prependOneVisibleItem() {
1038        while (true) {
1039            if (mFirstVisiblePos > 0) {
1040                if (mFirstVisiblePos > mGrid.getFirstIndex()) {
1041                    // prepend invisible view of saved location till first row
1042                    final int index = mFirstVisiblePos - 1;
1043                    final int row = mGrid.getLocation(index).row;
1044                    mGridProvider.createItem(index, row, false);
1045                    if (row == 0) {
1046                        return false;
1047                    }
1048                } else {
1049                    mGrid.prependItems(mScrollOffsetPrimary);
1050                    return false;
1051                }
1052            } else {
1053                return true;
1054            }
1055        }
1056    }
1057
1058    private void prependVisibleItems() {
1059        while (needsPrependVisibleItem()) {
1060            if (prependOneVisibleItem()) {
1061                break;
1062            }
1063        }
1064    }
1065
1066    private void removeChildAt(int position) {
1067        View v = getViewByPosition(position);
1068        if (v != null) {
1069            if (DEBUG) {
1070                Log.d(getTag(), "removeAndRecycleViewAt " + position);
1071            }
1072            ((LayoutParams) v.getLayoutParams()).onViewDetached();
1073            removeAndRecycleViewAt(getIndexByPosition(position), mRecycler);
1074        }
1075    }
1076
1077    private void removeInvisibleViewsAtEnd() {
1078        boolean update = false;
1079        while(mLastVisiblePos > mFirstVisiblePos && mLastVisiblePos > mFocusPosition) {
1080            View view = getViewByPosition(mLastVisiblePos);
1081            if (getViewMin(view) > mSizePrimary) {
1082                removeChildAt(mLastVisiblePos);
1083                mLastVisiblePos--;
1084                update = true;
1085            } else {
1086                break;
1087            }
1088        }
1089        if (update) {
1090            updateRowsMinMax();
1091        }
1092    }
1093
1094    private void removeInvisibleViewsAtFront() {
1095        boolean update = false;
1096        while(mLastVisiblePos > mFirstVisiblePos && mFirstVisiblePos < mFocusPosition) {
1097            View view = getViewByPosition(mFirstVisiblePos);
1098            if (getViewMax(view) < 0) {
1099                removeChildAt(mFirstVisiblePos);
1100                mFirstVisiblePos++;
1101                update = true;
1102            } else {
1103                break;
1104            }
1105        }
1106        if (update) {
1107            updateRowsMinMax();
1108        }
1109    }
1110
1111    private void updateRowsMinMax() {
1112        if (mFirstVisiblePos < 0) {
1113            return;
1114        }
1115        for (int i = 0; i < mNumRows; i++) {
1116            mRows[i].low = Integer.MAX_VALUE;
1117            mRows[i].high = Integer.MIN_VALUE;
1118        }
1119        for (int i = mFirstVisiblePos; i <= mLastVisiblePos; i++) {
1120            View view = getViewByPosition(i);
1121            int row = mGrid.getLocation(i).row;
1122            int low = getViewMin(view) + mScrollOffsetPrimary;
1123            if (low < mRows[row].low) {
1124                mRows[row].low = low;
1125            }
1126            int high = getViewMax(view) + mScrollOffsetPrimary;
1127            if (high > mRows[row].high) {
1128                mRows[row].high = high;
1129            }
1130        }
1131    }
1132
1133    /**
1134     * Relayout and re-positioning child for a possible new size and/or a new
1135     * start.
1136     *
1137     * @param view View to measure and layout.
1138     * @param start New start of the view or Integer.MIN_VALUE for not change.
1139     * @return New start of next view.
1140     */
1141    private int updateChildView(View view, int start, int startSecondary) {
1142        if (start == Integer.MIN_VALUE) {
1143            start = getViewMin(view);
1144        }
1145        int end;
1146        if (mOrientation == HORIZONTAL) {
1147            if (view.isLayoutRequested() || view.getMeasuredHeight() != mItemLengthSecondary) {
1148                measureChild(view);
1149            }
1150            end = start + view.getMeasuredWidth();
1151        } else {
1152            if (view.isLayoutRequested() || view.getMeasuredWidth() != mItemLengthSecondary) {
1153                measureChild(view);
1154            }
1155            end = start + view.getMeasuredHeight();
1156        }
1157
1158        layoutChild(view, start, end, startSecondary);
1159        return end + mMarginPrimary;
1160    }
1161
1162    // Fast layout when there is no structure change, adapter change, etc.
1163    protected void fastRelayout() {
1164        initScrollController();
1165
1166        List<Integer>[] rows = mGrid.getItemPositionsInRows(mFirstVisiblePos, mLastVisiblePos);
1167
1168        // relayout and repositioning views on each row
1169        for (int i = 0; i < mNumRows; i++) {
1170            List<Integer> row = rows[i];
1171            int start = Integer.MIN_VALUE;
1172            int startSecondary =
1173                i * (mItemLengthSecondary + mMarginSecondary) - mScrollOffsetSecondary;
1174            for (int j = 0, size = row.size(); j < size; j++) {
1175                int position = row.get(j);
1176                start = updateChildView(getViewByPosition(position), start, startSecondary);
1177            }
1178        }
1179
1180        appendVisibleItems();
1181        prependVisibleItems();
1182
1183        updateRowsMinMax();
1184        updateScrollMin();
1185        updateScrollMax();
1186
1187        if (mFocusScrollStrategy == BaseGridView.FOCUS_SCROLL_ALIGNED) {
1188            View focusView = getViewByPosition(mFocusPosition == NO_POSITION ? 0 : mFocusPosition);
1189            scrollToView(focusView, false);
1190        }
1191    }
1192
1193    // Lays out items based on the current scroll position
1194    @Override
1195    public void layoutChildren(RecyclerView.Adapter adapter, RecyclerView.Recycler recycler,
1196            boolean structureChanged) {
1197        if (DEBUG) {
1198            Log.v(getTag(), "layoutChildren start numRows " + mNumRows + " mScrollOffsetSecondary "
1199                    + mScrollOffsetSecondary + " mScrollOffsetPrimary " + mScrollOffsetPrimary
1200                    + " structureChanged " + structureChanged
1201                    + " mForceFullLayout " + mForceFullLayout);
1202            Log.v(getTag(), "width " + getWidth() + " height " + getHeight());
1203        }
1204
1205        if (mNumRows == 0) {
1206            // haven't done measure yet
1207            return;
1208        }
1209        final int itemCount = adapter.getItemCount();
1210        if (itemCount < 0) {
1211            return;
1212        }
1213
1214        mInLayout = true;
1215
1216        // Track the old focus view so we can adjust our system scroll position
1217        // so that any scroll animations happening now will remain valid.
1218        int delta = 0, deltaSecondary = 0;
1219        if (mFocusPosition != NO_POSITION
1220                && mFocusScrollStrategy == BaseGridView.FOCUS_SCROLL_ALIGNED) {
1221            View focusView = getViewByPosition(mFocusPosition);
1222            if (focusView != null) {
1223                delta = mWindowAlignment.mainAxis().getSystemScrollPos(
1224                        getViewCenter(focusView) + mScrollOffsetPrimary) - mScrollOffsetPrimary;
1225                deltaSecondary =
1226                    mWindowAlignment.secondAxis().getSystemScrollPos(
1227                            getViewCenterSecondary(focusView) + mScrollOffsetSecondary)
1228                    - mScrollOffsetSecondary;
1229            }
1230        }
1231
1232        boolean hasDoneFirstLayout = hasDoneFirstLayout();
1233        if (!structureChanged && !mForceFullLayout && hasDoneFirstLayout) {
1234            fastRelayout();
1235        } else {
1236            boolean hadFocus = mBaseGridView.hasFocus();
1237
1238            int newFocusPosition = init(adapter, recycler, mFocusPosition);
1239            if (DEBUG) {
1240                Log.v(getTag(), "mFocusPosition " + mFocusPosition + " newFocusPosition "
1241                    + newFocusPosition);
1242            }
1243
1244            // depending on result of init(), either recreating everything
1245            // or try to reuse the row start positions near mFocusPosition
1246            if (mGrid.getSize() == 0) {
1247                // this is a fresh creating all items, starting from
1248                // mFocusPosition with a estimated row index.
1249                mGrid.setStart(newFocusPosition, StaggeredGrid.START_DEFAULT);
1250
1251                // Can't track the old focus view
1252                delta = deltaSecondary = 0;
1253
1254            } else {
1255                // mGrid remembers Locations for the column that
1256                // contains mFocusePosition and also mRows remembers start
1257                // positions of each row.
1258                // Manually re-create child views for that column
1259                int firstIndex = mGrid.getFirstIndex();
1260                int lastIndex = mGrid.getLastIndex();
1261                for (int i = firstIndex; i <= lastIndex; i++) {
1262                    mGridProvider.createItem(i, mGrid.getLocation(i).row, true);
1263                }
1264            }
1265            // add visible views at end until reach the end of window
1266            appendVisibleItems();
1267            // add visible views at front until reach the start of window
1268            prependVisibleItems();
1269            // multiple rounds: scrollToView of first round may drag first/last child into
1270            // "visible window" and we update scrollMin/scrollMax then run second scrollToView
1271            int oldFirstVisible;
1272            int oldLastVisible;
1273            do {
1274                oldFirstVisible = mFirstVisiblePos;
1275                oldLastVisible = mLastVisiblePos;
1276                View focusView = getViewByPosition(newFocusPosition);
1277                // we need force to initialize the child view's position
1278                scrollToView(focusView, false);
1279                if (focusView != null && hadFocus) {
1280                    focusView.requestFocus();
1281                }
1282                appendVisibleItems();
1283                prependVisibleItems();
1284                removeInvisibleViewsAtFront();
1285                removeInvisibleViewsAtEnd();
1286            } while (mFirstVisiblePos != oldFirstVisible || mLastVisiblePos != oldLastVisible);
1287        }
1288        mForceFullLayout = false;
1289
1290        if (mFocusScrollStrategy == BaseGridView.FOCUS_SCROLL_ALIGNED) {
1291            scrollDirectionPrimary(-delta);
1292            scrollDirectionSecondary(-deltaSecondary);
1293        }
1294        appendVisibleItems();
1295        prependVisibleItems();
1296        removeInvisibleViewsAtFront();
1297        removeInvisibleViewsAtEnd();
1298
1299        if (DEBUG) {
1300            StringWriter sw = new StringWriter();
1301            PrintWriter pw = new PrintWriter(sw);
1302            mGrid.debugPrint(pw);
1303            Log.d(getTag(), sw.toString());
1304        }
1305
1306        removeAndRecycleScrap(recycler);
1307        attemptAnimateLayoutChild();
1308
1309        if (!hasDoneFirstLayout) {
1310            dispatchChildSelected();
1311        }
1312        mInLayout = false;
1313        if (DEBUG) Log.v(getTag(), "layoutChildren end");
1314    }
1315
1316    private void offsetChildrenSecondary(int increment) {
1317        final int childCount = getChildCount();
1318        if (mOrientation == HORIZONTAL) {
1319            for (int i = 0; i < childCount; i++) {
1320                getChildAt(i).offsetTopAndBottom(increment);
1321            }
1322        } else {
1323            for (int i = 0; i < childCount; i++) {
1324                getChildAt(i).offsetLeftAndRight(increment);
1325            }
1326        }
1327        mScrollOffsetSecondary -= increment;
1328    }
1329
1330    private void offsetChildrenPrimary(int increment) {
1331        final int childCount = getChildCount();
1332        if (mOrientation == VERTICAL) {
1333            for (int i = 0; i < childCount; i++) {
1334                getChildAt(i).offsetTopAndBottom(increment);
1335            }
1336        } else {
1337            for (int i = 0; i < childCount; i++) {
1338                getChildAt(i).offsetLeftAndRight(increment);
1339            }
1340        }
1341        mScrollOffsetPrimary -= increment;
1342    }
1343
1344    @Override
1345    public int scrollHorizontallyBy(int dx, Adapter adapter, Recycler recycler) {
1346        if (DEBUG) Log.v(TAG, "scrollHorizontallyBy " + dx);
1347
1348        if (mOrientation == HORIZONTAL) {
1349            return scrollDirectionPrimary(dx);
1350        } else {
1351            return scrollDirectionSecondary(dx);
1352        }
1353    }
1354
1355    @Override
1356    public int scrollVerticallyBy(int dy, Adapter adapter, Recycler recycler) {
1357        if (DEBUG) Log.v(TAG, "scrollVerticallyBy " + dy);
1358        if (mOrientation == VERTICAL) {
1359            return scrollDirectionPrimary(dy);
1360        } else {
1361            return scrollDirectionSecondary(dy);
1362        }
1363    }
1364
1365    // scroll in main direction may add/prune views
1366    private int scrollDirectionPrimary(int da) {
1367        offsetChildrenPrimary(-da);
1368        if (mInLayout) {
1369            return da;
1370        }
1371        if (da > 0) {
1372            appendVisibleItems();
1373            removeInvisibleViewsAtFront();
1374        } else if (da < 0) {
1375            prependVisibleItems();
1376            removeInvisibleViewsAtEnd();
1377        }
1378        attemptAnimateLayoutChild();
1379        mBaseGridView.invalidate();
1380        return da;
1381    }
1382
1383    // scroll in second direction will not add/prune views
1384    private int scrollDirectionSecondary(int dy) {
1385        offsetChildrenSecondary(-dy);
1386        mBaseGridView.invalidate();
1387        return dy;
1388    }
1389
1390    private void updateScrollMax() {
1391        if (mLastVisiblePos >= 0 && mLastVisiblePos == mAdapter.getItemCount() - 1) {
1392            int maxEdge = Integer.MIN_VALUE;
1393            for (int i = 0; i < mRows.length; i++) {
1394                if (mRows[i].high > maxEdge) {
1395                    maxEdge = mRows[i].high;
1396                }
1397            }
1398            mWindowAlignment.mainAxis().setMaxEdge(maxEdge);
1399            if (DEBUG) Log.v(getTag(), "updating scroll maxEdge to " + maxEdge);
1400        }
1401    }
1402
1403    private void updateScrollMin() {
1404        if (mFirstVisiblePos == 0) {
1405            int minEdge = Integer.MAX_VALUE;
1406            for (int i = 0; i < mRows.length; i++) {
1407                if (mRows[i].low < minEdge) {
1408                    minEdge = mRows[i].low;
1409                }
1410            }
1411            mWindowAlignment.mainAxis().setMinEdge(minEdge);
1412            if (DEBUG) Log.v(getTag(), "updating scroll minEdge to " + minEdge);
1413        }
1414    }
1415
1416    private void initScrollController() {
1417        mWindowAlignment.horizontal.setSize(getWidth());
1418        mWindowAlignment.horizontal.setPadding(getPaddingLeft(), getPaddingRight());
1419        mWindowAlignment.vertical.setSize(getHeight());
1420        mWindowAlignment.vertical.setPadding(getPaddingTop(), getPaddingBottom());
1421        mSizePrimary = mWindowAlignment.mainAxis().getSize();
1422        mWindowAlignment.mainAxis().invalidateScrollMin();
1423        mWindowAlignment.mainAxis().invalidateScrollMax();
1424
1425        // second axis min/max is determined at initialization, the mainAxis
1426        // min/max is determined later when we scroll to first or last item
1427        mWindowAlignment.secondAxis().setMinEdge(0);
1428        mWindowAlignment.secondAxis().setMaxEdge(mItemLengthSecondary * mNumRows + mMarginSecondary
1429                * (mNumRows - 1));
1430
1431        if (DEBUG) {
1432            Log.v(getTag(), "initScrollController mSizePrimary " + mSizePrimary + " "
1433                + " mItemLengthSecondary " + mItemLengthSecondary + " " + mWindowAlignment);
1434        }
1435    }
1436
1437    public void setSelection(RecyclerView parent, int position) {
1438        setSelection(parent, position, false);
1439    }
1440
1441    public void setSelectionSmooth(RecyclerView parent, int position) {
1442        setSelection(parent, position, true);
1443    }
1444
1445    public int getSelection() {
1446        return mFocusPosition;
1447    }
1448
1449    public void setSelection(RecyclerView parent, int position, boolean smooth) {
1450        if (mFocusPosition == position) {
1451            return;
1452        }
1453        View view = getViewByPosition(position);
1454        if (view != null) {
1455            scrollToView(view, smooth);
1456        } else {
1457            boolean right = position > mFocusPosition;
1458            mFocusPosition = position;
1459            if (smooth) {
1460                if (!hasDoneFirstLayout()) {
1461                    Log.w(getTag(), "setSelectionSmooth should " +
1462                            "not be called before first layout pass");
1463                    return;
1464                }
1465                if (right) {
1466                    appendVisibleItems();
1467                } else {
1468                    prependVisibleItems();
1469                }
1470                scrollToView(getViewByPosition(position), smooth);
1471            } else {
1472                mForceFullLayout = true;
1473                parent.requestLayout();
1474            }
1475        }
1476    }
1477
1478    @Override
1479    public void onItemsAdded(RecyclerView recyclerView, int positionStart, int itemCount) {
1480        boolean needsLayout = false;
1481        if (itemCount != 0) {
1482            if (mFirstVisiblePos < 0) {
1483                needsLayout = true;
1484            } else if (!(positionStart > mLastVisiblePos + 1 ||
1485                    positionStart + itemCount < mFirstVisiblePos - 1)) {
1486                needsLayout = true;
1487            }
1488        }
1489        if (needsLayout) {
1490            recyclerView.requestLayout();
1491        }
1492    }
1493
1494    @Override
1495    public boolean onRequestChildFocus(RecyclerView parent, View child, View focused) {
1496        if (!mInLayout) {
1497            scrollToView(child, true);
1498        }
1499        return true;
1500    }
1501
1502    @Override
1503    public boolean requestChildRectangleOnScreen(RecyclerView parent, View view, Rect rect,
1504            boolean immediate) {
1505        if (DEBUG) Log.v(getTag(), "requestChildRectangleOnScreen " + view + " " + rect);
1506        return false;
1507    }
1508
1509    int getScrollOffsetX() {
1510        return mOrientation == HORIZONTAL ? mScrollOffsetPrimary : mScrollOffsetSecondary;
1511    }
1512
1513    int getScrollOffsetY() {
1514        return mOrientation == HORIZONTAL ? mScrollOffsetSecondary : mScrollOffsetPrimary;
1515    }
1516
1517    public void getViewSelectedOffsets(View view, int[] offsets) {
1518        int scrollOffsetX = getScrollOffsetX();
1519        int scrollOffsetY = getScrollOffsetY();
1520        int viewCenterX = scrollOffsetX + getViewCenterX(view);
1521        int viewCenterY = scrollOffsetY + getViewCenterY(view);
1522        offsets[0] = mWindowAlignment.horizontal.getSystemScrollPos(viewCenterX) - scrollOffsetX;
1523        offsets[1] = mWindowAlignment.vertical.getSystemScrollPos(viewCenterY) - scrollOffsetY;
1524    }
1525
1526    /**
1527     * Scroll to a given child view and change mFocusPosition.
1528     */
1529    private void scrollToView(View view, boolean smooth) {
1530        int newFocusPosition = getPositionByView(view);
1531        if (mInLayout || newFocusPosition != mFocusPosition) {
1532            mFocusPosition = newFocusPosition;
1533            dispatchChildSelected();
1534        }
1535        if (mBaseGridView.isChildrenDrawingOrderEnabledInternal()) {
1536            mBaseGridView.invalidate();
1537        }
1538        if (view == null) {
1539            return;
1540        }
1541        if (!view.hasFocus() && mBaseGridView.hasFocus()) {
1542            // transfer focus to the child if it does not have focus yet (e.g. triggered
1543            // by setSelection())
1544            view.requestFocus();
1545        }
1546        switch (mFocusScrollStrategy) {
1547        case BaseGridView.FOCUS_SCROLL_ALIGNED:
1548        default:
1549            scrollToAlignedPosition(view, smooth);
1550            break;
1551        case BaseGridView.FOCUS_SCROLL_ITEM:
1552        case BaseGridView.FOCUS_SCROLL_PAGE:
1553            scrollItemOrPage(view, smooth);
1554            break;
1555        }
1556    }
1557
1558    private void scrollItemOrPage(View view, boolean smooth) {
1559        int pos = getPositionByView(view);
1560        int viewMin = getViewMin(view);
1561        int viewMax = getViewMax(view);
1562        // we either align "firstView" to left/top padding edge
1563        // or align "lastView" to right/bottom padding edge
1564        View firstView = null;
1565        View lastView = null;
1566        int paddingLow = mWindowAlignment.mainAxis().getPaddingLow();
1567        int clientSize = mWindowAlignment.mainAxis().getClientSize();
1568        final int row = mGrid.getLocation(pos).row;
1569        if (viewMin < paddingLow) {
1570            // view enters low padding area:
1571            firstView = view;
1572            if (mFocusScrollStrategy == BaseGridView.FOCUS_SCROLL_PAGE) {
1573                // scroll one "page" left/top,
1574                // align first visible item of the "page" at the low padding edge.
1575                while (!prependOneVisibleItem()) {
1576                    List<Integer> positions =
1577                            mGrid.getItemPositionsInRows(mFirstVisiblePos, pos)[row];
1578                    firstView = getViewByPosition(positions.get(0));
1579                    if (viewMax - getViewMin(firstView) > clientSize) {
1580                        if (positions.size() > 1) {
1581                            firstView = getViewByPosition(positions.get(1));
1582                        }
1583                        break;
1584                    }
1585                }
1586            }
1587        } else if (viewMax > clientSize + paddingLow) {
1588            // view enters high padding area:
1589            if (mFocusScrollStrategy == BaseGridView.FOCUS_SCROLL_PAGE) {
1590                // scroll whole one page right/bottom, align view at the low padding edge.
1591                firstView = view;
1592                do {
1593                    List<Integer> positions =
1594                            mGrid.getItemPositionsInRows(pos, mLastVisiblePos)[row];
1595                    lastView = getViewByPosition(positions.get(positions.size() - 1));
1596                    if (getViewMax(lastView) - viewMin > clientSize) {
1597                        lastView = null;
1598                        break;
1599                    }
1600                } while (!appendOneVisibleItem());
1601                if (lastView != null) {
1602                    // however if we reached end,  we should align last view.
1603                    firstView = null;
1604                }
1605            } else {
1606                lastView = view;
1607            }
1608        }
1609        int scrollPrimary = 0;
1610        int scrollSecondary = 0;
1611        if (firstView != null) {
1612            scrollPrimary = getViewMin(firstView) - paddingLow;
1613        } else if (lastView != null) {
1614            scrollPrimary = getViewMax(lastView) - (paddingLow + clientSize);
1615        }
1616        View secondaryAlignedView;
1617        if (firstView != null) {
1618            secondaryAlignedView = firstView;
1619        } else if (lastView != null) {
1620            secondaryAlignedView = lastView;
1621        } else {
1622            secondaryAlignedView = view;
1623        }
1624        int viewCenterSecondary = mScrollOffsetSecondary +
1625                getViewCenterSecondary(secondaryAlignedView);
1626        mWindowAlignment.secondAxis().updateScrollCenter(viewCenterSecondary);
1627        scrollSecondary = mWindowAlignment.secondAxis().getSystemScrollPos();
1628        scrollSecondary -= mScrollOffsetSecondary;
1629        scrollGrid(scrollPrimary, scrollSecondary, smooth);
1630    }
1631
1632    private void scrollToAlignedPosition(View view, boolean smooth) {
1633        int viewCenterPrimary = mScrollOffsetPrimary + getViewCenter(view);
1634        int viewCenterSecondary = mScrollOffsetSecondary + getViewCenterSecondary(view);
1635        if (DEBUG) {
1636            Log.v(getTag(), "scrollAligned smooth=" + smooth + " pos=" + mFocusPosition + " "
1637                    + viewCenterPrimary +","+viewCenterSecondary + " " + mWindowAlignment);
1638        }
1639
1640        if (mInLayout || viewCenterPrimary != mWindowAlignment.mainAxis().getScrollCenter()
1641                || viewCenterSecondary != mWindowAlignment.secondAxis().getScrollCenter()) {
1642            mWindowAlignment.mainAxis().updateScrollCenter(viewCenterPrimary);
1643            mWindowAlignment.secondAxis().updateScrollCenter(viewCenterSecondary);
1644            int scrollPrimary = mWindowAlignment.mainAxis().getSystemScrollPos();
1645            int scrollSecondary = mWindowAlignment.secondAxis().getSystemScrollPos();
1646            if (DEBUG) {
1647                Log.v(getTag(), "scrollAligned " + scrollPrimary + " " + scrollSecondary
1648                        +" " + mWindowAlignment);
1649            }
1650
1651            scrollPrimary -= mScrollOffsetPrimary;
1652            scrollSecondary -= mScrollOffsetSecondary;
1653
1654            scrollGrid(scrollPrimary, scrollSecondary, smooth);
1655        }
1656    }
1657
1658    private void scrollGrid(int scrollPrimary, int scrollSecondary, boolean smooth) {
1659        if (mInLayout) {
1660            scrollDirectionPrimary(scrollPrimary);
1661            scrollDirectionSecondary(scrollSecondary);
1662        } else {
1663            int scrollX;
1664            int scrollY;
1665            if (mOrientation == HORIZONTAL) {
1666                scrollX = scrollPrimary;
1667                scrollY = scrollSecondary;
1668            } else {
1669                scrollX = scrollSecondary;
1670                scrollY = scrollPrimary;
1671            }
1672            if (smooth) {
1673                mBaseGridView.smoothScrollBy(scrollX, scrollY);
1674            } else {
1675                mBaseGridView.scrollBy(scrollX, scrollY);
1676            }
1677        }
1678    }
1679
1680    public void setAnimateChildLayout(boolean animateChildLayout) {
1681        mAnimateChildLayout = animateChildLayout;
1682        if (!mAnimateChildLayout) {
1683            for (int i = 0, c = getChildCount(); i < c; i++) {
1684                ((LayoutParams) getChildAt(i).getLayoutParams()).endAnimate();
1685            }
1686        }
1687    }
1688
1689    private void attemptAnimateLayoutChild() {
1690        for (int i = 0, c = getChildCount(); i < c; i++) {
1691            // TODO: start delay can be staggered
1692            View v = getChildAt(i);
1693            ((LayoutParams) v.getLayoutParams()).startAnimate(this, v, 0);
1694        }
1695    }
1696
1697    public boolean isChildLayoutAnimated() {
1698        return mAnimateChildLayout;
1699    }
1700
1701    public void setChildLayoutAnimationInterpolator(Interpolator interpolator) {
1702        mAnimateLayoutChildInterpolator = interpolator;
1703    }
1704
1705    public Interpolator getChildLayoutAnimationInterpolator() {
1706        return mAnimateLayoutChildInterpolator;
1707    }
1708
1709    public void setChildLayoutAnimationDuration(long duration) {
1710        mAnimateLayoutChildDuration = duration;
1711    }
1712
1713    public long getChildLayoutAnimationDuration() {
1714        return mAnimateLayoutChildDuration;
1715    }
1716
1717    private int findImmediateChildIndex(View view) {
1718        while (view != null && view != mBaseGridView) {
1719            int index = mBaseGridView.indexOfChild(view);
1720            if (index >= 0) {
1721                return index;
1722            }
1723            view = (View) view.getParent();
1724        }
1725        return NO_POSITION;
1726    }
1727
1728    @Override
1729    public boolean onAddFocusables(RecyclerView recyclerView,
1730            ArrayList<View> views, int direction, int focusableMode) {
1731        // If this viewgroup or one of its children currently has focus then we
1732        // consider our children for focus searching.
1733        // Otherwise, we only want the system to ignore our children and pass
1734        // focus to the viewgroup, which will pass focus on to its children
1735        // appropriately.
1736        if (recyclerView.hasFocus()) {
1737            final int movement = getMovement(direction);
1738            if (movement != PREV_ITEM && movement != NEXT_ITEM) {
1739                // Move on secondary direction uses default addFocusables().
1740                return false;
1741            }
1742            // Get current focus row.
1743            final View focused = recyclerView.findFocus();
1744            final int focusedPos = getPositionByIndex(findImmediateChildIndex(focused));
1745            final int focusedRow = mGrid != null && focusedPos != NO_POSITION ?
1746                    mGrid.getLocation(focusedPos).row : NO_POSITION;
1747            // Add focusables within the same row.
1748            final int focusableCount = views.size();
1749            final int descendantFocusability = recyclerView.getDescendantFocusability();
1750            if (mGrid != null && descendantFocusability != ViewGroup.FOCUS_BLOCK_DESCENDANTS) {
1751                // focusables will be the current focused view and next neighbor view of same row
1752                // on the focus search direction.
1753                for (int i = 0, count = getChildCount(); i < count; i++) {
1754                    int index = movement == NEXT_ITEM ? i : count - 1 - i;
1755                    final View child = getChildAt(index);
1756                    if (child.getVisibility() != View.VISIBLE) {
1757                        continue;
1758                    }
1759                    int position = getPositionByIndex(index);
1760                    StaggeredGrid.Location loc = mGrid.getLocation(position);
1761                    if (focusedRow == NO_POSITION || (loc != null && loc.row == focusedRow)) {
1762                        if (focusedPos == NO_POSITION ||
1763                                (movement == NEXT_ITEM && position >= focusedPos)
1764                                || (movement == PREV_ITEM && position <= focusedPos)) {
1765                            child.addFocusables(views,  direction, focusableMode);
1766                            if (focusedPos != NO_POSITION && focusedPos != position) {
1767                                break;
1768                            }
1769                        }
1770                    }
1771                }
1772            }
1773            // From ViewGroup.addFocusables():
1774            // we add ourselves (if focusable) in all cases except for when we are
1775            // FOCUS_AFTER_DESCENDANTS and there are some descendants focusable.  this is
1776            // to avoid the focus search finding layouts when a more precise search
1777            // among the focusable children would be more interesting.
1778            if (descendantFocusability != ViewGroup.FOCUS_AFTER_DESCENDANTS
1779                    // No focusable descendants
1780                    || (focusableCount == views.size())) {
1781                if (recyclerView.isFocusable()) {
1782                    views.add(recyclerView);
1783                }
1784            }
1785        } else {
1786            if (mFocusScrollStrategy != BaseGridView.FOCUS_SCROLL_ALIGNED) {
1787                // adding views not overlapping padding area to avoid scrolling in gaining focus
1788                int left = mWindowAlignment.mainAxis().getPaddingLow();
1789                int right = mWindowAlignment.mainAxis().getClientSize() + left;
1790                int size = views.size();
1791                for (int i = 0, count = getChildCount(); i < count; i++) {
1792                    View child = getChildAt(i);
1793                    if (child.getVisibility() == View.VISIBLE) {
1794                        int viewMin = getViewMin(child);
1795                        int viewMax = getViewMax(child);
1796                        if (viewMin >= left && viewMax <= right) {
1797                            child.addFocusables(views, direction, focusableMode);
1798                        }
1799                    }
1800                }
1801                // if we cannot find any, then just add all children.
1802                if (views.size() == size) {
1803                    for (int i = 0, count = getChildCount(); i < count; i++) {
1804                        View child = getChildAt(i);
1805                        if (child.getVisibility() == View.VISIBLE) {
1806                            child.addFocusables(views, direction, focusableMode);
1807                        }
1808                    }
1809                }
1810                return true;
1811            }
1812            if (recyclerView.isFocusable()) {
1813                views.add(recyclerView);
1814            }
1815        }
1816        return true;
1817    }
1818
1819    @Override
1820    public View onFocusSearchFailed(View focused, int direction, Adapter adapter,
1821            Recycler recycler) {
1822        if (DEBUG) Log.v(getTag(), "onFocusSearchFailed direction " + direction);
1823
1824        View view = null;
1825        int movement = getMovement(direction);
1826        final FocusFinder ff = FocusFinder.getInstance();
1827        if (movement == NEXT_ITEM) {
1828            while (view == null && !appendOneVisibleItem()) {
1829                view = ff.findNextFocus(mBaseGridView, focused, direction);
1830            }
1831        } else if (movement == PREV_ITEM){
1832            while (view == null && !prependOneVisibleItem()) {
1833                view = ff.findNextFocus(mBaseGridView, focused, direction);
1834            }
1835        }
1836        if (view == null) {
1837            // returning the same view to prevent focus lost when scrolling past the end of the list
1838            if (movement == PREV_ITEM) {
1839                view = mFocusOutFront ? null : focused;
1840            } else if (movement == NEXT_ITEM){
1841                view = mFocusOutEnd ? null : focused;
1842            }
1843        }
1844        if (DEBUG) Log.v(getTag(), "returning view " + view);
1845        return view;
1846    }
1847
1848    boolean onRequestFocus(int direction, Rect previouslyFocusedRect) {
1849        if (mFocusScrollStrategy != BaseGridView.FOCUS_SCROLL_ALIGNED) {
1850            return false;
1851        }
1852        View view = getViewByPosition(mFocusPosition);
1853        if (view != null) {
1854            boolean result = view.requestFocus(direction, previouslyFocusedRect);
1855            if (!result && DEBUG) {
1856                Log.w(getTag(), "failed to request focus on " + view);
1857            }
1858            return result;
1859        }
1860        return false;
1861    }
1862
1863    private final static int PREV_ITEM = 0;
1864    private final static int NEXT_ITEM = 1;
1865    private final static int PREV_ROW = 2;
1866    private final static int NEXT_ROW = 3;
1867
1868    private int getMovement(int direction) {
1869        int movement = View.FOCUS_LEFT;
1870
1871        if (mOrientation == HORIZONTAL) {
1872            switch(direction) {
1873                case View.FOCUS_LEFT:
1874                    movement = PREV_ITEM;
1875                    break;
1876                case View.FOCUS_RIGHT:
1877                    movement = NEXT_ITEM;
1878                    break;
1879                case View.FOCUS_UP:
1880                    movement = PREV_ROW;
1881                    break;
1882                case View.FOCUS_DOWN:
1883                    movement = NEXT_ROW;
1884                    break;
1885            }
1886         } else if (mOrientation == VERTICAL) {
1887             switch(direction) {
1888                 case View.FOCUS_LEFT:
1889                     movement = PREV_ROW;
1890                     break;
1891                 case View.FOCUS_RIGHT:
1892                     movement = NEXT_ROW;
1893                     break;
1894                 case View.FOCUS_UP:
1895                     movement = PREV_ITEM;
1896                     break;
1897                 case View.FOCUS_DOWN:
1898                     movement = NEXT_ITEM;
1899                     break;
1900             }
1901         }
1902
1903        return movement;
1904    }
1905
1906    int getChildDrawingOrder(RecyclerView recyclerView, int childCount, int i) {
1907        int focusIndex = getIndexByPosition(mFocusPosition);
1908        if (focusIndex == NO_POSITION) {
1909            return i;
1910        }
1911        // supposely 0 1 2 3 4 5 6 7 8 9, 4 is the center item
1912        // drawing order is 0 1 2 3 9 8 7 6 5 4
1913        if (i < focusIndex) {
1914            return i;
1915        } else if (i < childCount - 1) {
1916            return focusIndex + childCount - 1 - i;
1917        } else {
1918            return focusIndex;
1919        }
1920    }
1921
1922    @Override
1923    public void onAdapterChanged(RecyclerView.Adapter oldAdapter,
1924            RecyclerView.Adapter newAdapter) {
1925        mGrid = null;
1926        mRows = null;
1927        super.onAdapterChanged(oldAdapter, newAdapter);
1928    }
1929
1930}
1931