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