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