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