GridLayoutManager.java revision c0b265dd2df42b399ba1f959b3dfd14dd14012b5
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.content.Context;
17import android.graphics.PointF;
18import android.graphics.Rect;
19import android.os.Bundle;
20import android.os.Parcel;
21import android.os.Parcelable;
22import android.support.v4.util.CircularIntArray;
23import android.support.v4.view.ViewCompat;
24import android.support.v7.widget.LinearSmoothScroller;
25import android.support.v7.widget.RecyclerView;
26import android.support.v7.widget.RecyclerView.Recycler;
27import android.support.v7.widget.RecyclerView.State;
28import android.support.v17.leanback.os.TraceHelper;
29
30import static android.support.v7.widget.RecyclerView.NO_ID;
31import static android.support.v7.widget.RecyclerView.NO_POSITION;
32import static android.support.v7.widget.RecyclerView.HORIZONTAL;
33import static android.support.v7.widget.RecyclerView.VERTICAL;
34
35import android.util.AttributeSet;
36import android.util.Log;
37import android.view.FocusFinder;
38import android.view.Gravity;
39import android.view.View;
40import android.view.ViewParent;
41import android.view.View.MeasureSpec;
42import android.view.ViewGroup.MarginLayoutParams;
43import android.view.ViewGroup;
44
45import java.io.PrintWriter;
46import java.io.StringWriter;
47import java.util.ArrayList;
48import java.util.List;
49
50final class GridLayoutManager extends RecyclerView.LayoutManager {
51
52     /*
53      * LayoutParams for {@link HorizontalGridView} and {@link VerticalGridView}.
54      * The class currently does two internal jobs:
55      * - Saves optical bounds insets.
56      * - Caches focus align view center.
57      */
58    static class LayoutParams extends RecyclerView.LayoutParams {
59
60        // The view is saved only during animation.
61        private View mView;
62
63        // For placement
64        private int mLeftInset;
65        private int mTopInset;
66        private int mRightInset;
67        private int mBottomInset;
68
69        // For alignment
70        private int mAlignX;
71        private int mAlignY;
72
73        public LayoutParams(Context c, AttributeSet attrs) {
74            super(c, attrs);
75        }
76
77        public LayoutParams(int width, int height) {
78            super(width, height);
79        }
80
81        public LayoutParams(MarginLayoutParams source) {
82            super(source);
83        }
84
85        public LayoutParams(ViewGroup.LayoutParams source) {
86            super(source);
87        }
88
89        public LayoutParams(RecyclerView.LayoutParams source) {
90            super(source);
91        }
92
93        public LayoutParams(LayoutParams source) {
94            super(source);
95        }
96
97        int getAlignX() {
98            return mAlignX;
99        }
100
101        int getAlignY() {
102            return mAlignY;
103        }
104
105        int getOpticalLeft(View view) {
106            return view.getLeft() + mLeftInset;
107        }
108
109        int getOpticalTop(View view) {
110            return view.getTop() + mTopInset;
111        }
112
113        int getOpticalRight(View view) {
114            return view.getRight() - mRightInset;
115        }
116
117        int getOpticalBottom(View view) {
118            return view.getBottom() - mBottomInset;
119        }
120
121        int getOpticalWidth(View view) {
122            return view.getWidth() - mLeftInset - mRightInset;
123        }
124
125        int getOpticalHeight(View view) {
126            return view.getHeight() - mTopInset - mBottomInset;
127        }
128
129        int getOpticalLeftInset() {
130            return mLeftInset;
131        }
132
133        int getOpticalRightInset() {
134            return mRightInset;
135        }
136
137        int getOpticalTopInset() {
138            return mTopInset;
139        }
140
141        int getOpticalBottomInset() {
142            return mBottomInset;
143        }
144
145        void setAlignX(int alignX) {
146            mAlignX = alignX;
147        }
148
149        void setAlignY(int alignY) {
150            mAlignY = alignY;
151        }
152
153        void setOpticalInsets(int leftInset, int topInset, int rightInset, int bottomInset) {
154            mLeftInset = leftInset;
155            mTopInset = topInset;
156            mRightInset = rightInset;
157            mBottomInset = bottomInset;
158        }
159
160        private void invalidateItemDecoration() {
161            ViewParent parent = mView.getParent();
162            if (parent instanceof RecyclerView) {
163                // TODO: we only need invalidate parent if it has ItemDecoration
164                ((RecyclerView) parent).invalidate();
165            }
166        }
167    }
168
169    /**
170     * Base class which scrolls to selected view in onStop().
171     */
172    abstract class GridLinearSmoothScroller extends LinearSmoothScroller {
173        GridLinearSmoothScroller() {
174            super(mBaseGridView.getContext());
175        }
176
177        @Override
178        protected void onStop() {
179            // onTargetFound() may not be called if we hit the "wall" first.
180            View targetView = findViewByPosition(getTargetPosition());
181            if (hasFocus() && targetView != null) {
182                mInSelection = true;
183                targetView.requestFocus();
184                mInSelection = false;
185            }
186            if (needsDispatchChildSelectedOnStop()) {
187                dispatchChildSelected();
188            }
189            super.onStop();
190        }
191
192        boolean needsDispatchChildSelectedOnStop() {
193            return true;
194        }
195
196        @Override
197        protected void onTargetFound(View targetView,
198                RecyclerView.State state, Action action) {
199            if (getScrollPosition(targetView, sTwoInts)) {
200                int dx, dy;
201                if (mOrientation == HORIZONTAL) {
202                    dx = sTwoInts[0];
203                    dy = sTwoInts[1];
204                } else {
205                    dx = sTwoInts[1];
206                    dy = sTwoInts[0];
207                }
208                final int distance = (int) Math.sqrt(dx * dx + dy * dy);
209                final int time = calculateTimeForDeceleration(distance);
210                action.update(dx, dy, time, mDecelerateInterpolator);
211            }
212        }
213    }
214
215    /**
216     * The SmoothScroller that remembers pending DPAD keys and consume pending keys
217     * during scroll.
218     */
219    final class PendingMoveSmoothScroller extends GridLinearSmoothScroller {
220        // -2 is a target position that LinearSmoothScroller can never find until
221        // consumePendingMovesXXX() sets real targetPosition.
222        final static int TARGET_UNDEFINED = -2;
223        // whether the grid is staggered.
224        private final boolean mStaggeredGrid;
225        // Number of pending movements on primary direction, negative if PREV_ITEM.
226        private int mPendingMoves;
227
228        PendingMoveSmoothScroller(int initialPendingMoves, boolean staggeredGrid) {
229            mPendingMoves = initialPendingMoves;
230            mStaggeredGrid = staggeredGrid;
231            setTargetPosition(TARGET_UNDEFINED);
232        }
233
234        void increasePendingMoves() {
235            if (mPendingMoves < MAX_PENDING_MOVES) {
236                mPendingMoves++;
237                if (mPendingMoves == 0) {
238                    dispatchChildSelected();
239                }
240            }
241        }
242
243        void decreasePendingMoves() {
244            if (mPendingMoves > -MAX_PENDING_MOVES) {
245                mPendingMoves--;
246                if (mPendingMoves == 0) {
247                    dispatchChildSelected();
248                }
249            }
250        }
251
252        /**
253         * Called before laid out an item when non-staggered grid can handle pending movements
254         * by skipping "mNumRows" per movement;  staggered grid will have to wait the item
255         * has been laid out in consumePendingMovesAfterLayout().
256         */
257        void consumePendingMovesBeforeLayout() {
258            if (mStaggeredGrid || mPendingMoves == 0) {
259                return;
260            }
261            View newSelected = null;
262            int startPos = mPendingMoves > 0 ? mFocusPosition + mNumRows :
263                    mFocusPosition - mNumRows;
264            for (int pos = startPos; mPendingMoves != 0;
265                    pos = mPendingMoves > 0 ? pos + mNumRows: pos - mNumRows) {
266                View v = findViewByPosition(pos);
267                if (v == null) {
268                    break;
269                }
270                if (!canScrollTo(v)) {
271                    continue;
272                }
273                newSelected = v;
274                mFocusPosition = pos;
275                if (mPendingMoves > 0) {
276                    mPendingMoves--;
277                } else {
278                    mPendingMoves++;
279                }
280            }
281            if (newSelected != null && hasFocus()) {
282                mInSelection = true;
283                newSelected.requestFocus();
284                mInSelection = false;
285            }
286        }
287
288        /**
289         * Called after laid out an item.  Staggered grid should find view on same
290         * Row and consume pending movements.
291         */
292        void consumePendingMovesAfterLayout() {
293            if (mStaggeredGrid && mPendingMoves != 0) {
294                // consume pending moves, focus to item on the same row.
295                final int focusedRow = mGrid != null && mFocusPosition != NO_POSITION ?
296                        mGrid.getLocation(mFocusPosition).row : NO_POSITION;
297                View newSelected = null;
298                for (int i = 0, count = getChildCount(); i < count && mPendingMoves != 0; i++) {
299                    int index = mPendingMoves > 0 ? i : count - 1 - i;
300                    final View child = getChildAt(index);
301                    if (!canScrollTo(child)) {
302                        continue;
303                    }
304                    int position = getPositionByIndex(index);
305                    Grid.Location loc = mGrid.getLocation(position);
306                    if (focusedRow == NO_POSITION || (loc != null && loc.row == focusedRow)) {
307                        if (mFocusPosition == NO_POSITION) {
308                            mFocusPosition = position;
309                            newSelected = child;
310                        } else if ((mPendingMoves > 0 && position > mFocusPosition)
311                                || (mPendingMoves < 0 && position < mFocusPosition)) {
312                            mFocusPosition = position;
313                            if (mPendingMoves > 0) {
314                                mPendingMoves--;
315                            } else {
316                                mPendingMoves++;
317                            }
318                            newSelected = child;
319                        }
320                    }
321                }
322                if (newSelected != null && hasFocus()) {
323                    mInSelection = true;
324                    newSelected.requestFocus();
325                    mInSelection = false;
326                }
327                if (mPendingMoves == 0) {
328                    dispatchChildSelected();
329                }
330            }
331            if (mPendingMoves == 0 || (mPendingMoves > 0 && hasCreatedLastItem())
332                    || (mPendingMoves < 0 && hasCreatedFirstItem())) {
333                setTargetPosition(mFocusPosition);
334                stop();
335            }
336        }
337
338        @Override
339        protected void updateActionForInterimTarget(Action action) {
340            if (mPendingMoves == 0) {
341                return;
342            }
343            super.updateActionForInterimTarget(action);
344        }
345
346        @Override
347        public PointF computeScrollVectorForPosition(int targetPosition) {
348            if (mPendingMoves == 0) {
349                return null;
350            }
351            int direction = (mReverseFlowPrimary ? mPendingMoves > 0 : mPendingMoves < 0) ?
352                    -1 : 1;
353            if (mOrientation == HORIZONTAL) {
354                return new PointF(direction, 0);
355            } else {
356                return new PointF(0, direction);
357            }
358        }
359
360        @Override
361        boolean needsDispatchChildSelectedOnStop() {
362            return mPendingMoves != 0;
363        }
364
365        @Override
366        protected void onStop() {
367            super.onStop();
368            // if we hit wall,  need clear the remaining pending moves.
369            mPendingMoves = 0;
370            mPendingMoveSmoothScroller = null;
371            View v = findViewByPosition(getTargetPosition());
372            if (v != null) scrollToView(v, true);
373        }
374    };
375
376    private static final String TAG = "GridLayoutManager";
377    private static final boolean DEBUG = false;
378    private static final boolean TRACE = false;
379
380    // maximum pending movement in one direction.
381    private final static int MAX_PENDING_MOVES = 10;
382
383    private String getTag() {
384        return TAG + ":" + mBaseGridView.getId();
385    }
386
387    private final BaseGridView mBaseGridView;
388
389    /**
390     * Note on conventions in the presence of RTL layout directions:
391     * Many properties and method names reference entities related to the
392     * beginnings and ends of things.  In the presence of RTL flows,
393     * it may not be clear whether this is intended to reference a
394     * quantity that changes direction in RTL cases, or a quantity that
395     * does not.  Here are the conventions in use:
396     *
397     * start/end: coordinate quantities - do reverse
398     * (optical) left/right: coordinate quantities - do not reverse
399     * low/high: coordinate quantities - do not reverse
400     * min/max: coordinate quantities - do not reverse
401     * scroll offset - coordinate quantities - do not reverse
402     * first/last: positional indices - do not reverse
403     * front/end: positional indices - do not reverse
404     * prepend/append: related to positional indices - do not reverse
405     *
406     * Note that although quantities do not reverse in RTL flows, their
407     * relationship does.  In LTR flows, the first positional index is
408     * leftmost; in RTL flows, it is rightmost.  Thus, anywhere that
409     * positional quantities are mapped onto coordinate quantities,
410     * the flow must be checked and the logic reversed.
411     */
412
413    /**
414     * The orientation of a "row".
415     */
416    private int mOrientation = HORIZONTAL;
417
418    private RecyclerView.State mState;
419    private RecyclerView.Recycler mRecycler;
420
421    private boolean mInLayout;
422    private boolean mInScroll;
423    private boolean mInFastRelayout;
424    /**
425     * During full layout pass, when GridView had focus: onLayoutChildren will
426     * skip non-focusable child and adjust mFocusPosition.
427     */
428    private boolean mInLayoutSearchFocus;
429    private boolean mInSelection = false;
430
431    private OnChildSelectedListener mChildSelectedListener = null;
432
433    private OnChildLaidOutListener mChildLaidOutListener = null;
434
435    /**
436     * The focused position, it's not the currently visually aligned position
437     * but it is the final position that we intend to focus on. If there are
438     * multiple setSelection() called, mFocusPosition saves last value.
439     */
440    private int mFocusPosition = NO_POSITION;
441
442    /**
443     * LinearSmoothScroller that consume pending DPAD movements.
444     */
445    private PendingMoveSmoothScroller mPendingMoveSmoothScroller;
446
447    /**
448     * The offset to be applied to mFocusPosition, due to adapter change, on the next
449     * layout.  Set to Integer.MIN_VALUE means item was removed.
450     * TODO:  This is somewhat duplication of RecyclerView getOldPosition() which is
451     * unfortunately cleared after prelayout.
452     */
453    private int mFocusPositionOffset = 0;
454
455    /**
456     * Extra pixels applied on primary direction.
457     */
458    private int mPrimaryScrollExtra;
459
460    /**
461     * Force a full layout under certain situations.  E.g. Rows change, jump to invisible child.
462     */
463    private boolean mForceFullLayout;
464
465    /**
466     * True if layout is enabled.
467     */
468    private boolean mLayoutEnabled = true;
469
470    /**
471     * override child visibility
472     */
473    private int mChildVisibility = -1;
474
475    /**
476     * The scroll offsets of the viewport relative to the entire view.
477     */
478    private int mScrollOffsetPrimary;
479    private int mScrollOffsetSecondary;
480
481    /**
482     * User-specified row height/column width.  Can be WRAP_CONTENT.
483     */
484    private int mRowSizeSecondaryRequested;
485
486    /**
487     * The fixed size of each grid item in the secondary direction. This corresponds to
488     * the row height, equal for all rows. Grid items may have variable length
489     * in the primary direction.
490     */
491    private int mFixedRowSizeSecondary;
492
493    /**
494     * Tracks the secondary size of each row.
495     */
496    private int[] mRowSizeSecondary;
497
498    /**
499     * Flag controlling whether the current/next layout should
500     * be updating the secondary size of rows.
501     */
502    private boolean mRowSecondarySizeRefresh;
503
504    /**
505     * The maximum measured size of the view.
506     */
507    private int mMaxSizeSecondary;
508
509    /**
510     * Margin between items.
511     */
512    private int mHorizontalMargin;
513    /**
514     * Margin between items vertically.
515     */
516    private int mVerticalMargin;
517    /**
518     * Margin in main direction.
519     */
520    private int mMarginPrimary;
521    /**
522     * Margin in second direction.
523     */
524    private int mMarginSecondary;
525    /**
526     * How to position child in secondary direction.
527     */
528    private int mGravity = Gravity.START | Gravity.TOP;
529    /**
530     * The number of rows in the grid.
531     */
532    private int mNumRows;
533    /**
534     * Number of rows requested, can be 0 to be determined by parent size and
535     * rowHeight.
536     */
537    private int mNumRowsRequested = 1;
538
539    /**
540     * Saves grid information of each view.
541     */
542    Grid mGrid;
543
544    /**
545     * Focus Scroll strategy.
546     */
547    private int mFocusScrollStrategy = BaseGridView.FOCUS_SCROLL_ALIGNED;
548    /**
549     * Defines how item view is aligned in the window.
550     */
551    private final WindowAlignment mWindowAlignment = new WindowAlignment();
552
553    /**
554     * Defines how item view is aligned.
555     */
556    private final ItemAlignment mItemAlignment = new ItemAlignment();
557
558    /**
559     * Dimensions of the view, width or height depending on orientation.
560     */
561    private int mSizePrimary;
562
563    /**
564     *  Allow DPAD key to navigate out at the front of the View (where position = 0),
565     *  default is false.
566     */
567    private boolean mFocusOutFront;
568
569    /**
570     * Allow DPAD key to navigate out at the end of the view, default is false.
571     */
572    private boolean mFocusOutEnd;
573
574    /**
575     * True if focus search is disabled.
576     */
577    private boolean mFocusSearchDisabled;
578
579    /**
580     * True if prune child,  might be disabled during transition.
581     */
582    private boolean mPruneChild = true;
583
584    /**
585     * True if scroll content,  might be disabled during transition.
586     */
587    private boolean mScrollEnabled = true;
588
589    /**
590     * Temporary variable: an int array of length=2.
591     */
592    private static int[] sTwoInts = new int[2];
593
594    /**
595     * Set to true for RTL layout in horizontal orientation
596     */
597    private boolean mReverseFlowPrimary = false;
598
599    /**
600     * Set to true for RTL layout in vertical orientation
601     */
602    private boolean mReverseFlowSecondary = false;
603
604    /**
605     * Temporaries used for measuring.
606     */
607    private int[] mMeasuredDimension = new int[2];
608
609    final ViewsStateBundle mChildrenStates = new ViewsStateBundle();
610
611    public GridLayoutManager(BaseGridView baseGridView) {
612        mBaseGridView = baseGridView;
613    }
614
615    public void setOrientation(int orientation) {
616        if (orientation != HORIZONTAL && orientation != VERTICAL) {
617            if (DEBUG) Log.v(getTag(), "invalid orientation: " + orientation);
618            return;
619        }
620
621        mOrientation = orientation;
622        mWindowAlignment.setOrientation(orientation);
623        mItemAlignment.setOrientation(orientation);
624        mForceFullLayout = true;
625    }
626
627    public void onRtlPropertiesChanged(int layoutDirection) {
628        if (mOrientation == HORIZONTAL) {
629            mReverseFlowPrimary = layoutDirection == View.LAYOUT_DIRECTION_RTL;
630            mReverseFlowSecondary = false;
631        } else {
632            mReverseFlowSecondary = layoutDirection == View.LAYOUT_DIRECTION_RTL;
633            mReverseFlowPrimary = false;
634        }
635        mWindowAlignment.horizontal.setReversedFlow(layoutDirection == View.LAYOUT_DIRECTION_RTL);
636    }
637
638    public int getFocusScrollStrategy() {
639        return mFocusScrollStrategy;
640    }
641
642    public void setFocusScrollStrategy(int focusScrollStrategy) {
643        mFocusScrollStrategy = focusScrollStrategy;
644    }
645
646    public void setWindowAlignment(int windowAlignment) {
647        mWindowAlignment.mainAxis().setWindowAlignment(windowAlignment);
648    }
649
650    public int getWindowAlignment() {
651        return mWindowAlignment.mainAxis().getWindowAlignment();
652    }
653
654    public void setWindowAlignmentOffset(int alignmentOffset) {
655        mWindowAlignment.mainAxis().setWindowAlignmentOffset(alignmentOffset);
656    }
657
658    public int getWindowAlignmentOffset() {
659        return mWindowAlignment.mainAxis().getWindowAlignmentOffset();
660    }
661
662    public void setWindowAlignmentOffsetPercent(float offsetPercent) {
663        mWindowAlignment.mainAxis().setWindowAlignmentOffsetPercent(offsetPercent);
664    }
665
666    public float getWindowAlignmentOffsetPercent() {
667        return mWindowAlignment.mainAxis().getWindowAlignmentOffsetPercent();
668    }
669
670    public void setItemAlignmentOffset(int alignmentOffset) {
671        mItemAlignment.mainAxis().setItemAlignmentOffset(alignmentOffset);
672        updateChildAlignments();
673    }
674
675    public int getItemAlignmentOffset() {
676        return mItemAlignment.mainAxis().getItemAlignmentOffset();
677    }
678
679    public void setItemAlignmentOffsetWithPadding(boolean withPadding) {
680        mItemAlignment.mainAxis().setItemAlignmentOffsetWithPadding(withPadding);
681        updateChildAlignments();
682    }
683
684    public boolean isItemAlignmentOffsetWithPadding() {
685        return mItemAlignment.mainAxis().isItemAlignmentOffsetWithPadding();
686    }
687
688    public void setItemAlignmentOffsetPercent(float offsetPercent) {
689        mItemAlignment.mainAxis().setItemAlignmentOffsetPercent(offsetPercent);
690        updateChildAlignments();
691    }
692
693    public float getItemAlignmentOffsetPercent() {
694        return mItemAlignment.mainAxis().getItemAlignmentOffsetPercent();
695    }
696
697    public void setItemAlignmentViewId(int viewId) {
698        mItemAlignment.mainAxis().setItemAlignmentViewId(viewId);
699        updateChildAlignments();
700    }
701
702    public int getItemAlignmentViewId() {
703        return mItemAlignment.mainAxis().getItemAlignmentViewId();
704    }
705
706    public void setFocusOutAllowed(boolean throughFront, boolean throughEnd) {
707        mFocusOutFront = throughFront;
708        mFocusOutEnd = throughEnd;
709    }
710
711    public void setNumRows(int numRows) {
712        if (numRows < 0) throw new IllegalArgumentException();
713        mNumRowsRequested = numRows;
714    }
715
716    /**
717     * Set the row height. May be WRAP_CONTENT, or a size in pixels.
718     */
719    public void setRowHeight(int height) {
720        if (height >= 0 || height == ViewGroup.LayoutParams.WRAP_CONTENT) {
721            mRowSizeSecondaryRequested = height;
722        } else {
723            throw new IllegalArgumentException("Invalid row height: " + height);
724        }
725    }
726
727    public void setItemMargin(int margin) {
728        mVerticalMargin = mHorizontalMargin = margin;
729        mMarginPrimary = mMarginSecondary = margin;
730    }
731
732    public void setVerticalMargin(int margin) {
733        if (mOrientation == HORIZONTAL) {
734            mMarginSecondary = mVerticalMargin = margin;
735        } else {
736            mMarginPrimary = mVerticalMargin = margin;
737        }
738    }
739
740    public void setHorizontalMargin(int margin) {
741        if (mOrientation == HORIZONTAL) {
742            mMarginPrimary = mHorizontalMargin = margin;
743        } else {
744            mMarginSecondary = mHorizontalMargin = margin;
745        }
746    }
747
748    public int getVerticalMargin() {
749        return mVerticalMargin;
750    }
751
752    public int getHorizontalMargin() {
753        return mHorizontalMargin;
754    }
755
756    public void setGravity(int gravity) {
757        mGravity = gravity;
758    }
759
760    protected boolean hasDoneFirstLayout() {
761        return mGrid != null;
762    }
763
764    public void setOnChildSelectedListener(OnChildSelectedListener listener) {
765        mChildSelectedListener = listener;
766    }
767
768    void setOnChildLaidOutListener(OnChildLaidOutListener listener) {
769        mChildLaidOutListener = listener;
770    }
771
772    private int getPositionByView(View view) {
773        if (view == null) {
774            return NO_POSITION;
775        }
776        LayoutParams params = (LayoutParams) view.getLayoutParams();
777        if (params == null || params.isItemRemoved()) {
778            // when item is removed, the position value can be any value.
779            return NO_POSITION;
780        }
781        return params.getViewPosition();
782    }
783
784    private int getPositionByIndex(int index) {
785        return getPositionByView(getChildAt(index));
786    }
787
788    private void dispatchChildSelected() {
789        if (mChildSelectedListener == null) {
790            return;
791        }
792
793        if (TRACE) TraceHelper.beginSection("onChildSelected");
794        View view = mFocusPosition == NO_POSITION ? null : findViewByPosition(mFocusPosition);
795        if (view != null) {
796            RecyclerView.ViewHolder vh = mBaseGridView.getChildViewHolder(view);
797            mChildSelectedListener.onChildSelected(mBaseGridView, view, mFocusPosition,
798                    vh == null? NO_ID: vh.getItemId());
799        } else {
800            mChildSelectedListener.onChildSelected(mBaseGridView, null, NO_POSITION, NO_ID);
801        }
802        if (TRACE) TraceHelper.endSection();
803
804        // Children may request layout when a child selection event occurs (such as a change of
805        // padding on the current and previously selected rows).
806        // If in layout, a child requesting layout may have been laid out before the selection
807        // callback.
808        // If it was not, the child will be laid out after the selection callback.
809        // If so, the layout request will be honoured though the view system will emit a double-
810        // layout warning.
811        // If not in layout, we may be scrolling in which case the child layout request will be
812        // eaten by recyclerview.  Post a requestLayout.
813        if (!mInLayout && !mBaseGridView.isLayoutRequested()) {
814            int childCount = getChildCount();
815            for (int i = 0; i < childCount; i++) {
816                if (getChildAt(i).isLayoutRequested()) {
817                    forceRequestLayout();
818                    break;
819                }
820            }
821        }
822    }
823
824    @Override
825    public boolean canScrollHorizontally() {
826        // We can scroll horizontally if we have horizontal orientation, or if
827        // we are vertical and have more than one column.
828        return mOrientation == HORIZONTAL || mNumRows > 1;
829    }
830
831    @Override
832    public boolean canScrollVertically() {
833        // We can scroll vertically if we have vertical orientation, or if we
834        // are horizontal and have more than one row.
835        return mOrientation == VERTICAL || mNumRows > 1;
836    }
837
838    @Override
839    public RecyclerView.LayoutParams generateDefaultLayoutParams() {
840        return new LayoutParams(ViewGroup.LayoutParams.WRAP_CONTENT,
841                ViewGroup.LayoutParams.WRAP_CONTENT);
842    }
843
844    @Override
845    public RecyclerView.LayoutParams generateLayoutParams(Context context, AttributeSet attrs) {
846        return new LayoutParams(context, attrs);
847    }
848
849    @Override
850    public RecyclerView.LayoutParams generateLayoutParams(ViewGroup.LayoutParams lp) {
851        if (lp instanceof LayoutParams) {
852            return new LayoutParams((LayoutParams) lp);
853        } else if (lp instanceof RecyclerView.LayoutParams) {
854            return new LayoutParams((RecyclerView.LayoutParams) lp);
855        } else if (lp instanceof MarginLayoutParams) {
856            return new LayoutParams((MarginLayoutParams) lp);
857        } else {
858            return new LayoutParams(lp);
859        }
860    }
861
862    protected View getViewForPosition(int position) {
863        return mRecycler.getViewForPosition(position);
864    }
865
866    final int getOpticalLeft(View v) {
867        return ((LayoutParams) v.getLayoutParams()).getOpticalLeft(v);
868    }
869
870    final int getOpticalRight(View v) {
871        return ((LayoutParams) v.getLayoutParams()).getOpticalRight(v);
872    }
873
874    final int getOpticalTop(View v) {
875        return ((LayoutParams) v.getLayoutParams()).getOpticalTop(v);
876    }
877
878    final int getOpticalBottom(View v) {
879        return ((LayoutParams) v.getLayoutParams()).getOpticalBottom(v);
880    }
881
882    private int getViewMin(View v) {
883        return (mOrientation == HORIZONTAL) ? getOpticalLeft(v) : getOpticalTop(v);
884    }
885
886    private int getViewMax(View v) {
887        return (mOrientation == HORIZONTAL) ? getOpticalRight(v) : getOpticalBottom(v);
888    }
889
890    private int getViewCenter(View view) {
891        return (mOrientation == HORIZONTAL) ? getViewCenterX(view) : getViewCenterY(view);
892    }
893
894    private int getViewCenterSecondary(View view) {
895        return (mOrientation == HORIZONTAL) ? getViewCenterY(view) : getViewCenterX(view);
896    }
897
898    private int getViewCenterX(View v) {
899        LayoutParams p = (LayoutParams) v.getLayoutParams();
900        return p.getOpticalLeft(v) + p.getAlignX();
901    }
902
903    private int getViewCenterY(View v) {
904        LayoutParams p = (LayoutParams) v.getLayoutParams();
905        return p.getOpticalTop(v) + p.getAlignY();
906    }
907
908    /**
909     * Save Recycler and State for convenience.  Must be paired with leaveContext().
910     */
911    private void saveContext(Recycler recycler, State state) {
912        if (mRecycler != null || mState != null) {
913            Log.e(TAG, "Recycler information was not released, bug!");
914        }
915        mRecycler = recycler;
916        mState = state;
917    }
918
919    /**
920     * Discard saved Recycler and State.
921     */
922    private void leaveContext() {
923        mRecycler = null;
924        mState = null;
925    }
926
927    /**
928     * Re-initialize data structures for a data change or handling invisible
929     * selection. The method tries its best to preserve position information so
930     * that staggered grid looks same before and after re-initialize.
931     * @return true if can fastRelayout()
932     */
933    private boolean layoutInit() {
934        boolean focusViewWasInTree = mGrid != null && mFocusPosition >= 0
935                && mFocusPosition >= mGrid.getFirstVisibleIndex()
936                && mFocusPosition <= mGrid.getLastVisibleIndex();
937        final int newItemCount = mState.getItemCount();
938        if (newItemCount == 0) {
939            mFocusPosition = NO_POSITION;
940        } else if (mFocusPosition >= newItemCount) {
941            mFocusPosition = newItemCount - 1;
942        } else if (mFocusPosition == NO_POSITION && newItemCount > 0) {
943            // if focus position is never set before,  initialize it to 0
944            mFocusPosition = 0;
945        }
946        if (!mState.didStructureChange() && mGrid.getFirstVisibleIndex() >= 0 &&
947                !mForceFullLayout && mGrid != null && mGrid.getNumRows() == mNumRows) {
948            updateScrollController();
949            updateScrollSecondAxis();
950            mGrid.setMargin(mMarginPrimary);
951            if (!focusViewWasInTree && mFocusPosition != NO_POSITION) {
952                mGrid.setStart(mFocusPosition);
953            }
954            return true;
955        } else {
956            mForceFullLayout = false;
957            int firstVisibleIndex = focusViewWasInTree ? mGrid.getFirstVisibleIndex() : 0;
958
959            if (mGrid == null || mNumRows != mGrid.getNumRows() ||
960                    mReverseFlowPrimary != mGrid.isReversedFlow()) {
961                mGrid = Grid.createStaggeredMultipleRows(mNumRows);
962                mGrid.setProvider(mGridProvider);
963                mGrid.setReversedFlow(mReverseFlowPrimary);
964            }
965            initScrollController();
966            updateScrollSecondAxis();
967            mGrid.setMargin(mMarginPrimary);
968            detachAndScrapAttachedViews(mRecycler);
969            mGrid.resetVisibleIndex();
970            if (mFocusPosition == NO_POSITION) {
971                mBaseGridView.clearFocus();
972            }
973            mWindowAlignment.mainAxis().invalidateScrollMin();
974            mWindowAlignment.mainAxis().invalidateScrollMax();
975            if (focusViewWasInTree && firstVisibleIndex <= mFocusPosition) {
976                // if focusView was in tree, we will add item from first visible item
977                mGrid.setStart(firstVisibleIndex);
978            } else {
979                // if focusView was not in tree, it's probably because focus position jumped
980                // far away from visible range,  so use mFocusPosition as start
981                mGrid.setStart(mFocusPosition);
982            }
983            return false;
984        }
985    }
986
987    private int getRowSizeSecondary(int rowIndex) {
988        if (mFixedRowSizeSecondary != 0) {
989            return mFixedRowSizeSecondary;
990        }
991        if (mRowSizeSecondary == null) {
992            return 0;
993        }
994        return mRowSizeSecondary[rowIndex];
995    }
996
997    private int getRowStartSecondary(int rowIndex) {
998        int start = 0;
999        // Iterate from left to right, which is a different index traversal
1000        // in RTL flow
1001        if (mReverseFlowSecondary) {
1002            for (int i = mNumRows-1; i > rowIndex; i--) {
1003                start += getRowSizeSecondary(i) + mMarginSecondary;
1004            }
1005        } else {
1006            for (int i = 0; i < rowIndex; i++) {
1007                start += getRowSizeSecondary(i) + mMarginSecondary;
1008            }
1009        }
1010        return start;
1011    }
1012
1013    private int getSizeSecondary() {
1014        int rightmostIndex = mReverseFlowSecondary ? 0 : mNumRows - 1;
1015        return getRowStartSecondary(rightmostIndex) + getRowSizeSecondary(rightmostIndex);
1016    }
1017
1018    private void measureScrapChild(int position, int widthSpec, int heightSpec,
1019            int[] measuredDimension) {
1020        View view = mRecycler.getViewForPosition(position);
1021        if (view != null) {
1022            LayoutParams p = (LayoutParams) view.getLayoutParams();
1023            int childWidthSpec = ViewGroup.getChildMeasureSpec(widthSpec,
1024                    getPaddingLeft() + getPaddingRight(), p.width);
1025            int childHeightSpec = ViewGroup.getChildMeasureSpec(heightSpec,
1026                    getPaddingTop() + getPaddingBottom(), p.height);
1027            view.measure(childWidthSpec, childHeightSpec);
1028            measuredDimension[0] = view.getMeasuredWidth();
1029            measuredDimension[1] = view.getMeasuredHeight();
1030            mRecycler.recycleView(view);
1031        }
1032    }
1033
1034    private boolean processRowSizeSecondary(boolean measure) {
1035        if (mFixedRowSizeSecondary != 0) {
1036            return false;
1037        }
1038
1039        if (TRACE) TraceHelper.beginSection("processRowSizeSecondary");
1040        CircularIntArray[] rows = mGrid == null ? null : mGrid.getItemPositionsInRows();
1041        boolean changed = false;
1042        int scrapChildWidth = -1;
1043        int scrapChildHeight = -1;
1044
1045        for (int rowIndex = 0; rowIndex < mNumRows; rowIndex++) {
1046            CircularIntArray row = rows == null ? null : rows[rowIndex];
1047            final int rowItemsPairCount = row == null ? 0 : row.size();
1048            int rowSize = -1;
1049            for (int rowItemPairIndex = 0; rowItemPairIndex < rowItemsPairCount;
1050                    rowItemPairIndex += 2) {
1051                final int rowIndexStart = row.get(rowItemPairIndex);
1052                final int rowIndexEnd = row.get(rowItemPairIndex + 1);
1053                for (int i = rowIndexStart; i <= rowIndexEnd; i++) {
1054                    final View view = findViewByPosition(i);
1055                    if (view == null) {
1056                        continue;
1057                    }
1058                    if (measure && view.isLayoutRequested()) {
1059                        measureChild(view);
1060                    }
1061                    final int secondarySize = mOrientation == HORIZONTAL ?
1062                            view.getMeasuredHeight() : view.getMeasuredWidth();
1063                    if (secondarySize > rowSize) {
1064                        rowSize = secondarySize;
1065                    }
1066                }
1067            }
1068
1069            final int itemCount = mState.getItemCount();
1070            if (measure && rowSize < 0 && itemCount > 0) {
1071                if (scrapChildWidth < 0 && scrapChildHeight < 0) {
1072                    int position;
1073                    if (mFocusPosition == NO_POSITION) {
1074                        position = 0;
1075                    } else if (mFocusPosition >= itemCount) {
1076                        position = itemCount - 1;
1077                    } else {
1078                        position = mFocusPosition;
1079                    }
1080                    measureScrapChild(position,
1081                            MeasureSpec.makeMeasureSpec(0, MeasureSpec.UNSPECIFIED),
1082                            MeasureSpec.makeMeasureSpec(0, MeasureSpec.UNSPECIFIED),
1083                            mMeasuredDimension);
1084                    scrapChildWidth = mMeasuredDimension[0];
1085                    scrapChildHeight = mMeasuredDimension[1];
1086                    if (DEBUG) Log.v(TAG, "measured scrap child: " + scrapChildWidth +
1087                            " " + scrapChildHeight);
1088                }
1089                rowSize = mOrientation == HORIZONTAL ? scrapChildHeight : scrapChildWidth;
1090            }
1091            if (rowSize < 0) {
1092                rowSize = 0;
1093            }
1094            if (mRowSizeSecondary[rowIndex] != rowSize) {
1095                if (DEBUG) Log.v(getTag(), "row size secondary changed: " + mRowSizeSecondary[rowIndex] +
1096                        ", " + rowSize);
1097                mRowSizeSecondary[rowIndex] = rowSize;
1098                changed = true;
1099            }
1100        }
1101
1102        if (TRACE) TraceHelper.endSection();
1103        return changed;
1104    }
1105
1106    /**
1107     * Checks if we need to update row secondary sizes.
1108     */
1109    private void updateRowSecondarySizeRefresh() {
1110        mRowSecondarySizeRefresh = processRowSizeSecondary(false);
1111        if (mRowSecondarySizeRefresh) {
1112            if (DEBUG) Log.v(getTag(), "mRowSecondarySizeRefresh now set");
1113            forceRequestLayout();
1114        }
1115    }
1116
1117    private void forceRequestLayout() {
1118        if (DEBUG) Log.v(getTag(), "forceRequestLayout");
1119        // RecyclerView prevents us from requesting layout in many cases
1120        // (during layout, during scroll, etc.)
1121        // For secondary row size wrap_content support we currently need a
1122        // second layout pass to update the measured size after having measured
1123        // and added child views in layoutChildren.
1124        // Force the second layout by posting a delayed runnable.
1125        // TODO: investigate allowing a second layout pass,
1126        // or move child add/measure logic to the measure phase.
1127        ViewCompat.postOnAnimation(mBaseGridView, mRequestLayoutRunnable);
1128    }
1129
1130    private final Runnable mRequestLayoutRunnable = new Runnable() {
1131        @Override
1132        public void run() {
1133            if (DEBUG) Log.v(getTag(), "request Layout from runnable");
1134            requestLayout();
1135        }
1136     };
1137
1138    @Override
1139    public void onMeasure(Recycler recycler, State state, int widthSpec, int heightSpec) {
1140        saveContext(recycler, state);
1141
1142        int sizePrimary, sizeSecondary, modeSecondary, paddingSecondary;
1143        int measuredSizeSecondary;
1144        if (mOrientation == HORIZONTAL) {
1145            sizePrimary = MeasureSpec.getSize(widthSpec);
1146            sizeSecondary = MeasureSpec.getSize(heightSpec);
1147            modeSecondary = MeasureSpec.getMode(heightSpec);
1148            paddingSecondary = getPaddingTop() + getPaddingBottom();
1149        } else {
1150            sizeSecondary = MeasureSpec.getSize(widthSpec);
1151            sizePrimary = MeasureSpec.getSize(heightSpec);
1152            modeSecondary = MeasureSpec.getMode(widthSpec);
1153            paddingSecondary = getPaddingLeft() + getPaddingRight();
1154        }
1155        if (DEBUG) Log.v(getTag(), "onMeasure widthSpec " + Integer.toHexString(widthSpec) +
1156                " heightSpec " + Integer.toHexString(heightSpec) +
1157                " modeSecondary " + Integer.toHexString(modeSecondary) +
1158                " sizeSecondary " + sizeSecondary + " " + this);
1159
1160        mMaxSizeSecondary = sizeSecondary;
1161
1162        if (mRowSizeSecondaryRequested == ViewGroup.LayoutParams.WRAP_CONTENT) {
1163            mNumRows = mNumRowsRequested == 0 ? 1 : mNumRowsRequested;
1164            mFixedRowSizeSecondary = 0;
1165
1166            if (mRowSizeSecondary == null || mRowSizeSecondary.length != mNumRows) {
1167                mRowSizeSecondary = new int[mNumRows];
1168            }
1169
1170            // Measure all current children and update cached row heights
1171            processRowSizeSecondary(true);
1172
1173            switch (modeSecondary) {
1174            case MeasureSpec.UNSPECIFIED:
1175                measuredSizeSecondary = getSizeSecondary() + paddingSecondary;
1176                break;
1177            case MeasureSpec.AT_MOST:
1178                measuredSizeSecondary = Math.min(getSizeSecondary() + paddingSecondary,
1179                        mMaxSizeSecondary);
1180                break;
1181            case MeasureSpec.EXACTLY:
1182                measuredSizeSecondary = mMaxSizeSecondary;
1183                break;
1184            default:
1185                throw new IllegalStateException("wrong spec");
1186            }
1187
1188        } else {
1189            switch (modeSecondary) {
1190            case MeasureSpec.UNSPECIFIED:
1191                if (mRowSizeSecondaryRequested == 0) {
1192                    if (mOrientation == HORIZONTAL) {
1193                        throw new IllegalStateException("Must specify rowHeight or view height");
1194                    } else {
1195                        throw new IllegalStateException("Must specify columnWidth or view width");
1196                    }
1197                }
1198                mFixedRowSizeSecondary = mRowSizeSecondaryRequested;
1199                mNumRows = mNumRowsRequested == 0 ? 1 : mNumRowsRequested;
1200                measuredSizeSecondary = mFixedRowSizeSecondary * mNumRows + mMarginSecondary
1201                    * (mNumRows - 1) + paddingSecondary;
1202                break;
1203            case MeasureSpec.AT_MOST:
1204            case MeasureSpec.EXACTLY:
1205                if (mNumRowsRequested == 0 && mRowSizeSecondaryRequested == 0) {
1206                    mNumRows = 1;
1207                    mFixedRowSizeSecondary = sizeSecondary - paddingSecondary;
1208                } else if (mNumRowsRequested == 0) {
1209                    mFixedRowSizeSecondary = mRowSizeSecondaryRequested;
1210                    mNumRows = (sizeSecondary + mMarginSecondary)
1211                        / (mRowSizeSecondaryRequested + mMarginSecondary);
1212                } else if (mRowSizeSecondaryRequested == 0) {
1213                    mNumRows = mNumRowsRequested;
1214                    mFixedRowSizeSecondary = (sizeSecondary - paddingSecondary - mMarginSecondary
1215                            * (mNumRows - 1)) / mNumRows;
1216                } else {
1217                    mNumRows = mNumRowsRequested;
1218                    mFixedRowSizeSecondary = mRowSizeSecondaryRequested;
1219                }
1220                measuredSizeSecondary = sizeSecondary;
1221                if (modeSecondary == MeasureSpec.AT_MOST) {
1222                    int childrenSize = mFixedRowSizeSecondary * mNumRows + mMarginSecondary
1223                        * (mNumRows - 1) + paddingSecondary;
1224                    if (childrenSize < measuredSizeSecondary) {
1225                        measuredSizeSecondary = childrenSize;
1226                    }
1227                }
1228                break;
1229            default:
1230                throw new IllegalStateException("wrong spec");
1231            }
1232        }
1233        if (mOrientation == HORIZONTAL) {
1234            setMeasuredDimension(sizePrimary, measuredSizeSecondary);
1235        } else {
1236            setMeasuredDimension(measuredSizeSecondary, sizePrimary);
1237        }
1238        if (DEBUG) {
1239            Log.v(getTag(), "onMeasure sizePrimary " + sizePrimary +
1240                    " measuredSizeSecondary " + measuredSizeSecondary +
1241                    " mFixedRowSizeSecondary " + mFixedRowSizeSecondary +
1242                    " mNumRows " + mNumRows);
1243        }
1244        leaveContext();
1245    }
1246
1247    private void measureChild(View child) {
1248        if (TRACE) TraceHelper.beginSection("measureChild");
1249        final ViewGroup.LayoutParams lp = child.getLayoutParams();
1250        final int secondarySpec = (mRowSizeSecondaryRequested == ViewGroup.LayoutParams.WRAP_CONTENT) ?
1251                MeasureSpec.makeMeasureSpec(0, MeasureSpec.UNSPECIFIED) :
1252                MeasureSpec.makeMeasureSpec(mFixedRowSizeSecondary, MeasureSpec.EXACTLY);
1253        int widthSpec, heightSpec;
1254
1255        if (mOrientation == HORIZONTAL) {
1256            widthSpec = ViewGroup.getChildMeasureSpec(
1257                    MeasureSpec.makeMeasureSpec(0, MeasureSpec.UNSPECIFIED),
1258                    0, lp.width);
1259            heightSpec = ViewGroup.getChildMeasureSpec(secondarySpec, 0, lp.height);
1260        } else {
1261            heightSpec = ViewGroup.getChildMeasureSpec(
1262                    MeasureSpec.makeMeasureSpec(0, MeasureSpec.UNSPECIFIED),
1263                    0, lp.height);
1264            widthSpec = ViewGroup.getChildMeasureSpec(secondarySpec, 0, lp.width);
1265        }
1266        child.measure(widthSpec, heightSpec);
1267        if (DEBUG) Log.v(getTag(), "measureChild secondarySpec " + Integer.toHexString(secondarySpec) +
1268                " widthSpec " + Integer.toHexString(widthSpec) +
1269                " heightSpec " + Integer.toHexString(heightSpec) +
1270                " measuredWidth " + child.getMeasuredWidth() +
1271                " measuredHeight " + child.getMeasuredHeight());
1272        if (DEBUG) Log.v(getTag(), "child lp width " + lp.width + " height " + lp.height);
1273        if (TRACE) TraceHelper.endSection();
1274    }
1275
1276    private Grid.Provider mGridProvider = new Grid.Provider() {
1277
1278        @Override
1279        public int getCount() {
1280            return mState.getItemCount();
1281        }
1282
1283        @Override
1284        public int createItem(int index, boolean append, Object[] item) {
1285            if (TRACE) TraceHelper.beginSection("createItem");
1286            if (TRACE) TraceHelper.beginSection("getview");
1287            View v = getViewForPosition(index);
1288            if (TRACE) TraceHelper.endSection();
1289            // See recyclerView docs:  we don't need re-add scraped view if it was removed.
1290            if (!((RecyclerView.LayoutParams) v.getLayoutParams()).isItemRemoved()) {
1291                if (TRACE) TraceHelper.beginSection("addView");
1292                if (append) {
1293                    addView(v);
1294                } else {
1295                    addView(v, 0);
1296                }
1297                if (TRACE) TraceHelper.endSection();
1298                if (mChildVisibility != -1) {
1299                    v.setVisibility(mChildVisibility);
1300                }
1301
1302                if (mPendingMoveSmoothScroller != null) {
1303                    mPendingMoveSmoothScroller.consumePendingMovesBeforeLayout();
1304                }
1305                if (!mInLayout) {
1306                    // when we are appending item during scroll pass and the item's position
1307                    // matches the mFocusPosition,  we should signal a childSelected event.
1308                    // However if we are still running PendingMoveSmoothScroller,  we defer and
1309                    // signal the event in PendingMoveSmoothScroller.onStop().  This can
1310                    // avoid lots of childSelected events during a long smooth scrolling and
1311                    // increase performance.
1312                    if (index == mFocusPosition && (mPendingMoveSmoothScroller == null
1313                            || mPendingMoveSmoothScroller.mPendingMoves == 0)) {
1314                        dispatchChildSelected();
1315                    }
1316                } else if (!mInFastRelayout) {
1317                    // fastRelayout will dispatch event at end of onLayoutChildren().
1318                    // For full layout, two situations here:
1319                    // 1. mInLayoutSearchFocus is false, dispatchChildSelected() at mFocusPosition.
1320                    // 2. mInLayoutSearchFocus is true:  dispatchChildSelected() on first child
1321                    //    equal to or after mFocusPosition that can take focus.
1322                    if (!mInLayoutSearchFocus && index == mFocusPosition) {
1323                        dispatchChildSelected();
1324                    } else if (mInLayoutSearchFocus && index >= mFocusPosition
1325                            && v.hasFocusable()) {
1326                        mFocusPosition = index;
1327                        mInLayoutSearchFocus = false;
1328                        dispatchChildSelected();
1329                    }
1330                }
1331                measureChild(v);
1332            }
1333            item[0] = v;
1334            return mOrientation == HORIZONTAL ? v.getMeasuredWidth() : v.getMeasuredHeight();
1335        }
1336
1337        @Override
1338        public void addItem(Object item, int index, int length, int rowIndex, int edge) {
1339            View v = (View) item;
1340            int start, end;
1341            if (edge == Integer.MIN_VALUE || edge == Integer.MAX_VALUE) {
1342                edge = !mGrid.isReversedFlow() ? mWindowAlignment.mainAxis().getPaddingLow()
1343                        : mWindowAlignment.mainAxis().getSize()
1344                                - mWindowAlignment.mainAxis().getPaddingHigh();
1345            }
1346            boolean edgeIsMin = !mGrid.isReversedFlow();
1347            if (edgeIsMin) {
1348                start = edge;
1349                end = edge + length;
1350            } else {
1351                start = edge - length;
1352                end = edge;
1353            }
1354            int startSecondary = getRowStartSecondary(rowIndex) - mScrollOffsetSecondary;
1355            mChildrenStates.loadView(v, index);
1356            layoutChild(rowIndex, v, start, end, startSecondary);
1357            if (DEBUG) {
1358                Log.d(getTag(), "addView " + index + " " + v);
1359            }
1360            if (TRACE) TraceHelper.endSection();
1361
1362            if (index == mGrid.getFirstVisibleIndex()) {
1363                if (!mGrid.isReversedFlow()) {
1364                    updateScrollMin();
1365                } else {
1366                    updateScrollMax();
1367                }
1368            }
1369            if (index == mGrid.getLastVisibleIndex()) {
1370                if (!mGrid.isReversedFlow()) {
1371                    updateScrollMax();
1372                } else {
1373                    updateScrollMin();
1374                }
1375            }
1376            if (!mInLayout && mPendingMoveSmoothScroller != null) {
1377                mPendingMoveSmoothScroller.consumePendingMovesAfterLayout();
1378            }
1379            if (mChildLaidOutListener != null) {
1380                RecyclerView.ViewHolder vh = mBaseGridView.getChildViewHolder(v);
1381                mChildLaidOutListener.onChildLaidOut(mBaseGridView, v, index,
1382                        vh == null ? NO_ID : vh.getItemId());
1383            }
1384        }
1385
1386        @Override
1387        public void removeItem(int index) {
1388            if (TRACE) TraceHelper.beginSection("removeItem");
1389            View v = findViewByPosition(index);
1390            if (mInLayout) {
1391                detachAndScrapView(v, mRecycler);
1392            } else {
1393                removeAndRecycleView(v, mRecycler);
1394            }
1395            if (TRACE) TraceHelper.endSection();
1396        }
1397
1398        @Override
1399        public int getEdge(int index) {
1400            if (mReverseFlowPrimary) {
1401                return getViewMax(findViewByPosition(index));
1402            } else {
1403                return getViewMin(findViewByPosition(index));
1404            }
1405        }
1406
1407        @Override
1408        public int getSize(int index) {
1409            final View v = findViewByPosition(index);
1410            return mOrientation == HORIZONTAL ? v.getMeasuredWidth() : v.getMeasuredHeight();
1411        }
1412    };
1413
1414    private void layoutChild(int rowIndex, View v, int start, int end, int startSecondary) {
1415        if (TRACE) TraceHelper.beginSection("layoutChild");
1416        int sizeSecondary = mOrientation == HORIZONTAL ? v.getMeasuredHeight()
1417                : v.getMeasuredWidth();
1418        if (mFixedRowSizeSecondary > 0) {
1419            sizeSecondary = Math.min(sizeSecondary, mFixedRowSizeSecondary);
1420        }
1421        final int verticalGravity = mGravity & Gravity.VERTICAL_GRAVITY_MASK;
1422        final int horizontalGravity = (mReverseFlowPrimary || mReverseFlowSecondary) ?
1423                Gravity.getAbsoluteGravity(mGravity & Gravity.RELATIVE_HORIZONTAL_GRAVITY_MASK, View.LAYOUT_DIRECTION_RTL) :
1424                mGravity & Gravity.HORIZONTAL_GRAVITY_MASK;
1425        if (mOrientation == HORIZONTAL && verticalGravity == Gravity.TOP
1426                || mOrientation == VERTICAL && horizontalGravity == Gravity.LEFT) {
1427            // do nothing
1428        } else if (mOrientation == HORIZONTAL && verticalGravity == Gravity.BOTTOM
1429                || mOrientation == VERTICAL && horizontalGravity == Gravity.RIGHT) {
1430            startSecondary += getRowSizeSecondary(rowIndex) - sizeSecondary;
1431        } else if (mOrientation == HORIZONTAL && verticalGravity == Gravity.CENTER_VERTICAL
1432                || mOrientation == VERTICAL && horizontalGravity == Gravity.CENTER_HORIZONTAL) {
1433            startSecondary += (getRowSizeSecondary(rowIndex) - sizeSecondary) / 2;
1434        }
1435        int left, top, right, bottom;
1436        if (mOrientation == HORIZONTAL) {
1437            left = start;
1438            top = startSecondary;
1439            right = end;
1440            bottom = startSecondary + sizeSecondary;
1441        } else {
1442            top = start;
1443            left = startSecondary;
1444            bottom = end;
1445            right = startSecondary + sizeSecondary;
1446        }
1447        v.layout(left, top, right, bottom);
1448        updateChildOpticalInsets(v, left, top, right, bottom);
1449        updateChildAlignments(v);
1450        if (TRACE) TraceHelper.endSection();
1451    }
1452
1453    private void updateChildOpticalInsets(View v, int left, int top, int right, int bottom) {
1454        LayoutParams p = (LayoutParams) v.getLayoutParams();
1455        p.setOpticalInsets(left - v.getLeft(), top - v.getTop(),
1456                v.getRight() - right, v.getBottom() - bottom);
1457    }
1458
1459    private void updateChildAlignments(View v) {
1460        LayoutParams p = (LayoutParams) v.getLayoutParams();
1461        p.setAlignX(mItemAlignment.horizontal.getAlignmentPosition(v));
1462        p.setAlignY(mItemAlignment.vertical.getAlignmentPosition(v));
1463    }
1464
1465    private void updateChildAlignments() {
1466        for (int i = 0, c = getChildCount(); i < c; i++) {
1467            updateChildAlignments(getChildAt(i));
1468        }
1469    }
1470
1471    private void removeInvisibleViewsAtEnd() {
1472        if (mPruneChild) {
1473            mGrid.removeInvisibleItemsAtEnd(mFocusPosition,
1474                    mReverseFlowPrimary ? 0 : mSizePrimary);
1475        }
1476    }
1477
1478    private void removeInvisibleViewsAtFront() {
1479        if (mPruneChild) {
1480            mGrid.removeInvisibleItemsAtFront(mFocusPosition,
1481                    mReverseFlowPrimary ? mSizePrimary : 0);
1482        }
1483    }
1484
1485    private boolean appendOneColumnVisibleItems() {
1486        return mGrid.appendOneColumnVisibleItems();
1487    }
1488
1489    private boolean prependOneColumnVisibleItems() {
1490        return mGrid.prependOneColumnVisibleItems();
1491    }
1492
1493    private void appendVisibleItems() {
1494        mGrid.appendVisibleItems(mReverseFlowPrimary ? 0 : mSizePrimary);
1495    }
1496
1497    private void prependVisibleItems() {
1498        mGrid.prependVisibleItems(mReverseFlowPrimary ? mSizePrimary : 0);
1499    }
1500
1501    /**
1502     * Fast layout when there is no structure change, adapter change, etc.
1503     * It will layout all views was layout requested or updated, until hit a view
1504     * with different size,  then it break and detachAndScrap all views after that.
1505     */
1506    private void fastRelayout() {
1507        boolean invalidateAfter = false;
1508        final int childCount = getChildCount();
1509        int position = -1;
1510        for (int index = 0; index < childCount; index++) {
1511            View view = getChildAt(index);
1512            position = getPositionByIndex(index);
1513            Grid.Location location = mGrid.getLocation(position);
1514            if (location == null) {
1515                if (DEBUG) Log.w(getTag(), "fastRelayout(): no Location at " + position);
1516                invalidateAfter = true;
1517                break;
1518            }
1519
1520            int startSecondary = getRowStartSecondary(location.row) - mScrollOffsetSecondary;
1521            int primarySize, end;
1522            int start = getViewMin(view);
1523            int oldPrimarySize = (mOrientation == HORIZONTAL) ?
1524                    view.getMeasuredWidth() :
1525                    view.getMeasuredHeight();
1526
1527            LayoutParams lp = (LayoutParams) view.getLayoutParams();
1528            if (lp.viewNeedsUpdate()) {
1529                int viewIndex = mBaseGridView.indexOfChild(view);
1530                detachAndScrapView(view, mRecycler);
1531                view = getViewForPosition(position);
1532                addView(view, viewIndex);
1533            }
1534
1535            if (view.isLayoutRequested()) {
1536                measureChild(view);
1537            }
1538            if (mOrientation == HORIZONTAL) {
1539                primarySize = view.getMeasuredWidth();
1540                end = start + primarySize;
1541            } else {
1542                primarySize = view.getMeasuredHeight();
1543                end = start + primarySize;
1544            }
1545            layoutChild(location.row, view, start, end, startSecondary);
1546            if (oldPrimarySize != primarySize) {
1547                // size changed invalidate remaining Locations
1548                if (DEBUG) Log.d(getTag(), "fastRelayout: view size changed at " + position);
1549                invalidateAfter = true;
1550                break;
1551            }
1552        }
1553        if (invalidateAfter) {
1554            final int savedLastPos = mGrid.getLastVisibleIndex();
1555            mGrid.invalidateItemsAfter(position);
1556            if (mPruneChild) {
1557                // in regular prune child mode, we just append items up to edge limit
1558                appendVisibleItems();
1559                if (mFocusPosition >= 0 && mFocusPosition <= savedLastPos) {
1560                    // make sure add focus view back:  the view might be outside edge limit
1561                    // when there is delta in onLayoutChildren().
1562                    while (mGrid.getLastVisibleIndex() < mFocusPosition) {
1563                        mGrid.appendOneColumnVisibleItems();
1564                    }
1565                }
1566            } else {
1567                // prune disabled(e.g. in RowsFragment transition): append all removed items
1568                while (mGrid.appendOneColumnVisibleItems()
1569                        && mGrid.getLastVisibleIndex() < savedLastPos);
1570            }
1571        }
1572        updateScrollMin();
1573        updateScrollMax();
1574        updateScrollSecondAxis();
1575    }
1576
1577    public void removeAndRecycleAllViews(RecyclerView.Recycler recycler) {
1578        if (TRACE) TraceHelper.beginSection("removeAndRecycleAllViews");
1579        if (DEBUG) Log.v(TAG, "removeAndRecycleAllViews " + getChildCount());
1580        for (int i = getChildCount() - 1; i >= 0; i--) {
1581            removeAndRecycleViewAt(i, recycler);
1582        }
1583        if (TRACE) TraceHelper.endSection();
1584    }
1585
1586    // Lays out items based on the current scroll position
1587    @Override
1588    public void onLayoutChildren(RecyclerView.Recycler recycler, RecyclerView.State state) {
1589        if (DEBUG) {
1590            Log.v(getTag(), "layoutChildren start numRows " + mNumRows + " mScrollOffsetSecondary "
1591                    + mScrollOffsetSecondary + " mScrollOffsetPrimary " + mScrollOffsetPrimary
1592                    + " inPreLayout " + state.isPreLayout()
1593                    + " didStructureChange " + state.didStructureChange()
1594                    + " mForceFullLayout " + mForceFullLayout);
1595            Log.v(getTag(), "width " + getWidth() + " height " + getHeight());
1596        }
1597
1598        if (mNumRows == 0) {
1599            // haven't done measure yet
1600            return;
1601        }
1602        final int itemCount = state.getItemCount();
1603        if (itemCount < 0) {
1604            return;
1605        }
1606
1607        if (!mLayoutEnabled) {
1608            discardLayoutInfo();
1609            removeAndRecycleAllViews(recycler);
1610            return;
1611        }
1612        mInLayout = true;
1613
1614        final boolean scrollToFocus = !isSmoothScrolling()
1615                && mFocusScrollStrategy == BaseGridView.FOCUS_SCROLL_ALIGNED;
1616        if (mFocusPosition != NO_POSITION && mFocusPositionOffset != Integer.MIN_VALUE) {
1617            mFocusPosition = mFocusPosition + mFocusPositionOffset;
1618            mFocusPositionOffset = 0;
1619        }
1620        saveContext(recycler, state);
1621
1622        // Track the old focus view so we can adjust our system scroll position
1623        // so that any scroll animations happening now will remain valid.
1624        // We must use same delta in Pre Layout (if prelayout exists) and second layout.
1625        // So we cache the deltas in PreLayout and use it in second layout.
1626        int delta = 0, deltaSecondary = 0;
1627        if (mFocusPosition != NO_POSITION && scrollToFocus
1628                && mBaseGridView.getScrollState() != RecyclerView.SCROLL_STATE_IDLE) {
1629            // FIXME: we should get the remaining scroll animation offset from RecyclerView
1630            View focusView = findViewByPosition(mFocusPosition);
1631            if (focusView != null) {
1632                delta = mWindowAlignment.mainAxis().getSystemScrollPos(mScrollOffsetPrimary
1633                        + getViewCenter(focusView), false, false) - mScrollOffsetPrimary;
1634                deltaSecondary = mWindowAlignment.secondAxis().getSystemScrollPos(
1635                        mScrollOffsetSecondary + getViewCenterSecondary(focusView),
1636                        false, false) - mScrollOffsetSecondary;
1637            }
1638        }
1639
1640        boolean hadFocus = mBaseGridView.hasFocus();
1641        int savedFocusPos = mFocusPosition;
1642        if (mInFastRelayout = layoutInit()) {
1643            fastRelayout();
1644            // appends items till focus position.
1645            if (mFocusPosition != NO_POSITION) {
1646                View focusView = findViewByPosition(mFocusPosition);
1647                if (focusView != null) {
1648                    if (scrollToFocus) {
1649                        scrollToView(focusView, false);
1650                    }
1651                    if (hadFocus && !focusView.hasFocus()) {
1652                        focusView.requestFocus();
1653                    }
1654                }
1655            }
1656        } else {
1657            mInLayoutSearchFocus = hadFocus;
1658            if (mFocusPosition != NO_POSITION) {
1659                // appends items till focus position.
1660                while (appendOneColumnVisibleItems()
1661                        && findViewByPosition(mFocusPosition) == null) ;
1662            }
1663            // multiple rounds: scrollToView of first round may drag first/last child into
1664            // "visible window" and we update scrollMin/scrollMax then run second scrollToView
1665            int oldFirstVisible;
1666            int oldLastVisible;
1667            do {
1668                updateScrollMin();
1669                updateScrollMax();
1670                oldFirstVisible = mGrid.getFirstVisibleIndex();
1671                oldLastVisible = mGrid.getLastVisibleIndex();
1672                View focusView = findViewByPosition(mFocusPosition);
1673                // we need force to initialize the child view's position
1674                scrollToView(focusView, false);
1675                if (focusView != null && hadFocus && !focusView.hasFocus()) {
1676                    focusView.requestFocus();
1677                }
1678                appendVisibleItems();
1679                prependVisibleItems();
1680                removeInvisibleViewsAtFront();
1681                removeInvisibleViewsAtEnd();
1682            } while (mGrid.getFirstVisibleIndex() != oldFirstVisible ||
1683                    mGrid.getLastVisibleIndex() != oldLastVisible);
1684        }
1685
1686        if (scrollToFocus) {
1687            scrollDirectionPrimary(-delta);
1688            scrollDirectionSecondary(-deltaSecondary);
1689        }
1690        appendVisibleItems();
1691        prependVisibleItems();
1692        removeInvisibleViewsAtFront();
1693        removeInvisibleViewsAtEnd();
1694
1695        if (DEBUG) {
1696            StringWriter sw = new StringWriter();
1697            PrintWriter pw = new PrintWriter(sw);
1698            mGrid.debugPrint(pw);
1699            Log.d(getTag(), sw.toString());
1700        }
1701
1702        if (mRowSecondarySizeRefresh) {
1703            mRowSecondarySizeRefresh = false;
1704        } else {
1705            updateRowSecondarySizeRefresh();
1706        }
1707
1708        // For fastRelayout, only dispatch event when focus position changes.
1709        if (mInFastRelayout && mFocusPosition != savedFocusPos) {
1710            dispatchChildSelected();
1711        } else if (!mInFastRelayout && mInLayoutSearchFocus) {
1712            // For full layout we dispatchChildSelected() in createItem() unless searched all
1713            // children and found none is focusable then dispatchChildSelected() here.
1714            dispatchChildSelected();
1715        }
1716
1717        mInLayout = false;
1718        leaveContext();
1719        if (DEBUG) Log.v(getTag(), "layoutChildren end");
1720    }
1721
1722    private void offsetChildrenSecondary(int increment) {
1723        final int childCount = getChildCount();
1724        if (mOrientation == HORIZONTAL) {
1725            for (int i = 0; i < childCount; i++) {
1726                getChildAt(i).offsetTopAndBottom(increment);
1727            }
1728        } else {
1729            for (int i = 0; i < childCount; i++) {
1730                getChildAt(i).offsetLeftAndRight(increment);
1731            }
1732        }
1733    }
1734
1735    private void offsetChildrenPrimary(int increment) {
1736        final int childCount = getChildCount();
1737        if (mOrientation == VERTICAL) {
1738            for (int i = 0; i < childCount; i++) {
1739                getChildAt(i).offsetTopAndBottom(increment);
1740            }
1741        } else {
1742            for (int i = 0; i < childCount; i++) {
1743                getChildAt(i).offsetLeftAndRight(increment);
1744            }
1745        }
1746    }
1747
1748    @Override
1749    public int scrollHorizontallyBy(int dx, Recycler recycler, RecyclerView.State state) {
1750        if (DEBUG) Log.v(getTag(), "scrollHorizontallyBy " + dx);
1751        if (!mLayoutEnabled || !hasDoneFirstLayout()) {
1752            return 0;
1753        }
1754        saveContext(recycler, state);
1755        mInScroll = true;
1756        int result;
1757        if (mOrientation == HORIZONTAL) {
1758            result = scrollDirectionPrimary(dx);
1759        } else {
1760            result = scrollDirectionSecondary(dx);
1761        }
1762        leaveContext();
1763        mInScroll = false;
1764        return result;
1765    }
1766
1767    @Override
1768    public int scrollVerticallyBy(int dy, Recycler recycler, RecyclerView.State state) {
1769        if (DEBUG) Log.v(getTag(), "scrollVerticallyBy " + dy);
1770        if (!mLayoutEnabled || !hasDoneFirstLayout()) {
1771            return 0;
1772        }
1773        mInScroll = true;
1774        saveContext(recycler, state);
1775        int result;
1776        if (mOrientation == VERTICAL) {
1777            result = scrollDirectionPrimary(dy);
1778        } else {
1779            result = scrollDirectionSecondary(dy);
1780        }
1781        leaveContext();
1782        mInScroll = false;
1783        return result;
1784    }
1785
1786    // scroll in main direction may add/prune views
1787    private int scrollDirectionPrimary(int da) {
1788        if (TRACE) TraceHelper.beginSection("scrollPrimary");
1789        boolean isMaxUnknown = false, isMinUnknown = false;
1790        int minScroll = 0, maxScroll = 0;
1791        if (da > 0) {
1792            isMaxUnknown = mWindowAlignment.mainAxis().isMaxUnknown();
1793            if (!isMaxUnknown) {
1794                maxScroll = mWindowAlignment.mainAxis().getMaxScroll();
1795                if (mScrollOffsetPrimary + da > maxScroll) {
1796                    da = maxScroll - mScrollOffsetPrimary;
1797                }
1798            }
1799        } else if (da < 0) {
1800            isMinUnknown = mWindowAlignment.mainAxis().isMinUnknown();
1801            if (!isMinUnknown) {
1802                minScroll = mWindowAlignment.mainAxis().getMinScroll();
1803                if (mScrollOffsetPrimary + da < minScroll) {
1804                    da = minScroll - mScrollOffsetPrimary;
1805                }
1806            }
1807        }
1808        if (da == 0) {
1809            if (TRACE) TraceHelper.endSection();
1810            return 0;
1811        }
1812        offsetChildrenPrimary(-da);
1813        mScrollOffsetPrimary += da;
1814        if (mInLayout) {
1815            if (TRACE) TraceHelper.endSection();
1816            return da;
1817        }
1818
1819        int childCount = getChildCount();
1820        boolean updated;
1821
1822        if (mReverseFlowPrimary ? da > 0 : da < 0) {
1823            prependVisibleItems();
1824        } else {
1825            appendVisibleItems();
1826        }
1827        updated = getChildCount() > childCount;
1828        childCount = getChildCount();
1829
1830        if (TRACE) TraceHelper.beginSection("remove");
1831        if (mReverseFlowPrimary ? da > 0 : da < 0) {
1832            removeInvisibleViewsAtEnd();
1833        } else {
1834            removeInvisibleViewsAtFront();
1835        }
1836        if (TRACE) TraceHelper.endSection();
1837        updated |= getChildCount() < childCount;
1838        if (updated) {
1839            updateRowSecondarySizeRefresh();
1840        }
1841
1842        mBaseGridView.invalidate();
1843        if (TRACE) TraceHelper.endSection();
1844        return da;
1845    }
1846
1847    // scroll in second direction will not add/prune views
1848    private int scrollDirectionSecondary(int dy) {
1849        if (dy == 0) {
1850            return 0;
1851        }
1852        offsetChildrenSecondary(-dy);
1853        mScrollOffsetSecondary += dy;
1854        mBaseGridView.invalidate();
1855        return dy;
1856    }
1857
1858    private void updateScrollMax() {
1859        int highVisiblePos = (!mReverseFlowPrimary) ? mGrid.getLastVisibleIndex()
1860                : mGrid.getFirstVisibleIndex();
1861        int highMaxPos = (!mReverseFlowPrimary) ? mState.getItemCount() - 1 : 0;
1862        if (highVisiblePos < 0) {
1863            return;
1864        }
1865        final boolean highAvailable = highVisiblePos == highMaxPos;
1866        final boolean maxUnknown = mWindowAlignment.mainAxis().isMaxUnknown();
1867        if (!highAvailable && maxUnknown) {
1868            return;
1869        }
1870        int maxEdge = mGrid.findRowMax(true, sTwoInts) + mScrollOffsetPrimary;
1871        int rowIndex = sTwoInts[0];
1872        int pos = sTwoInts[1];
1873        int savedMaxEdge = mWindowAlignment.mainAxis().getMaxEdge();
1874        mWindowAlignment.mainAxis().setMaxEdge(maxEdge);
1875        int maxScroll = getPrimarySystemScrollPosition(findViewByPosition(pos));
1876        mWindowAlignment.mainAxis().setMaxEdge(savedMaxEdge);
1877
1878        if (highAvailable) {
1879            mWindowAlignment.mainAxis().setMaxEdge(maxEdge);
1880            mWindowAlignment.mainAxis().setMaxScroll(maxScroll);
1881            if (DEBUG) Log.v(getTag(), "updating scroll maxEdge to " + maxEdge +
1882                    " scrollMax to " + maxScroll);
1883        } else {
1884            mWindowAlignment.mainAxis().invalidateScrollMax();
1885            if (DEBUG) Log.v(getTag(), "Invalidate scrollMax since it should be "
1886                    + "greater than " + maxScroll);
1887        }
1888    }
1889
1890    private void updateScrollMin() {
1891        int lowVisiblePos = (!mReverseFlowPrimary) ? mGrid.getFirstVisibleIndex()
1892                : mGrid.getLastVisibleIndex();
1893        int lowMinPos = (!mReverseFlowPrimary) ? 0 : mState.getItemCount() - 1;
1894        if (lowVisiblePos < 0) {
1895            return;
1896        }
1897        final boolean lowAvailable = lowVisiblePos == lowMinPos;
1898        final boolean minUnknown = mWindowAlignment.mainAxis().isMinUnknown();
1899        if (!lowAvailable && minUnknown) {
1900            return;
1901        }
1902        int minEdge = mGrid.findRowMin(false, sTwoInts) + mScrollOffsetPrimary;
1903        int rowIndex = sTwoInts[0];
1904        int pos = sTwoInts[1];
1905        int savedMinEdge = mWindowAlignment.mainAxis().getMinEdge();
1906        mWindowAlignment.mainAxis().setMinEdge(minEdge);
1907        int minScroll = getPrimarySystemScrollPosition(findViewByPosition(pos));
1908        mWindowAlignment.mainAxis().setMinEdge(savedMinEdge);
1909
1910        if (lowAvailable) {
1911            mWindowAlignment.mainAxis().setMinEdge(minEdge);
1912            mWindowAlignment.mainAxis().setMinScroll(minScroll);
1913            if (DEBUG) Log.v(getTag(), "updating scroll minEdge to " + minEdge +
1914                    " scrollMin to " + minScroll);
1915        } else {
1916            mWindowAlignment.mainAxis().invalidateScrollMin();
1917            if (DEBUG) Log.v(getTag(), "Invalidate scrollMin, since it should be "
1918                    + "less than " + minScroll);
1919        }
1920    }
1921
1922    private void updateScrollSecondAxis() {
1923        mWindowAlignment.secondAxis().setMinEdge(0);
1924        mWindowAlignment.secondAxis().setMaxEdge(getSizeSecondary());
1925    }
1926
1927    private void initScrollController() {
1928        mWindowAlignment.reset();
1929        mWindowAlignment.horizontal.setSize(getWidth());
1930        mWindowAlignment.vertical.setSize(getHeight());
1931        mWindowAlignment.horizontal.setPadding(getPaddingLeft(), getPaddingRight());
1932        mWindowAlignment.vertical.setPadding(getPaddingTop(), getPaddingBottom());
1933        mSizePrimary = mWindowAlignment.mainAxis().getSize();
1934        mScrollOffsetPrimary = -mWindowAlignment.mainAxis().getPaddingLow();
1935        mScrollOffsetSecondary = -mWindowAlignment.secondAxis().getPaddingLow();
1936
1937        if (DEBUG) {
1938            Log.v(getTag(), "initScrollController mSizePrimary " + mSizePrimary
1939                    + " mWindowAlignment " + mWindowAlignment
1940                    + " mScrollOffsetPrimary " + mScrollOffsetPrimary);
1941        }
1942    }
1943
1944    private void updateScrollController() {
1945        // mScrollOffsetPrimary and mScrollOffsetSecondary includes the padding.
1946        // e.g. when topPadding is 16 for horizontal grid view,  the initial
1947        // mScrollOffsetSecondary is -16.  fastRelayout() put views based on offsets(not padding),
1948        // when padding changes to 20,  we also need update mScrollOffsetSecondary to -20 before
1949        // fastRelayout() is performed
1950        int paddingPrimaryDiff, paddingSecondaryDiff;
1951        if (mOrientation == HORIZONTAL) {
1952            paddingPrimaryDiff = getPaddingLeft() - mWindowAlignment.horizontal.getPaddingLow();
1953            paddingSecondaryDiff = getPaddingTop() - mWindowAlignment.vertical.getPaddingLow();
1954        } else {
1955            paddingPrimaryDiff = getPaddingTop() - mWindowAlignment.vertical.getPaddingLow();
1956            paddingSecondaryDiff = getPaddingLeft() - mWindowAlignment.horizontal.getPaddingLow();
1957        }
1958        mScrollOffsetPrimary -= paddingPrimaryDiff;
1959        mScrollOffsetSecondary -= paddingSecondaryDiff;
1960
1961        mWindowAlignment.horizontal.setSize(getWidth());
1962        mWindowAlignment.vertical.setSize(getHeight());
1963        mWindowAlignment.horizontal.setPadding(getPaddingLeft(), getPaddingRight());
1964        mWindowAlignment.vertical.setPadding(getPaddingTop(), getPaddingBottom());
1965        mSizePrimary = mWindowAlignment.mainAxis().getSize();
1966
1967        if (DEBUG) {
1968            Log.v(getTag(), "updateScrollController mSizePrimary " + mSizePrimary
1969                    + " mWindowAlignment " + mWindowAlignment
1970                    + " mScrollOffsetPrimary " + mScrollOffsetPrimary);
1971        }
1972    }
1973
1974    public void setSelection(RecyclerView parent, int position,
1975            int primaryScrollExtra) {
1976        setSelection(parent, position, false, primaryScrollExtra);
1977    }
1978
1979    public void setSelectionSmooth(RecyclerView parent, int position) {
1980        setSelection(parent, position, true, 0);
1981    }
1982
1983    public int getSelection() {
1984        return mFocusPosition;
1985    }
1986
1987    public void setSelection(RecyclerView parent, int position, boolean smooth,
1988            int primaryScrollExtra) {
1989        if (mFocusPosition != position && position != NO_POSITION
1990                || primaryScrollExtra != mPrimaryScrollExtra) {
1991            scrollToSelection(parent, position, smooth, primaryScrollExtra);
1992        }
1993    }
1994
1995    private void scrollToSelection(RecyclerView parent, int position, boolean smooth,
1996            int primaryScrollExtra) {
1997        if (TRACE) TraceHelper.beginSection("scrollToSelection");
1998        mPrimaryScrollExtra = primaryScrollExtra;
1999        View view = findViewByPosition(position);
2000        if (view != null) {
2001            mInSelection = true;
2002            scrollToView(view, smooth);
2003            mInSelection = false;
2004        } else {
2005            mFocusPosition = position;
2006            mFocusPositionOffset = 0;
2007            if (!mLayoutEnabled) {
2008                return;
2009            }
2010            if (smooth) {
2011                if (!hasDoneFirstLayout()) {
2012                    Log.w(getTag(), "setSelectionSmooth should " +
2013                            "not be called before first layout pass");
2014                    return;
2015                }
2016                startPositionSmoothScroller(position);
2017            } else {
2018                mForceFullLayout = true;
2019                parent.requestLayout();
2020            }
2021        }
2022        if (TRACE) TraceHelper.endSection();
2023    }
2024
2025    void startPositionSmoothScroller(int position) {
2026        LinearSmoothScroller linearSmoothScroller = new GridLinearSmoothScroller() {
2027            @Override
2028            public PointF computeScrollVectorForPosition(int targetPosition) {
2029                if (getChildCount() == 0) {
2030                    return null;
2031                }
2032                final int firstChildPos = getPosition(getChildAt(0));
2033                // TODO We should be able to deduce direction from bounds of current and target
2034                // focus, rather than making assumptions about positions and directionality
2035                final boolean isStart = mReverseFlowPrimary ? targetPosition > firstChildPos
2036                        : targetPosition < firstChildPos;
2037                final int direction = isStart ? -1 : 1;
2038                if (mOrientation == HORIZONTAL) {
2039                    return new PointF(direction, 0);
2040                } else {
2041                    return new PointF(0, direction);
2042                }
2043            }
2044
2045        };
2046        linearSmoothScroller.setTargetPosition(position);
2047        startSmoothScroll(linearSmoothScroller);
2048    }
2049
2050    private void processPendingMovement(boolean forward) {
2051        if (forward ? hasCreatedLastItem() : hasCreatedFirstItem()) {
2052            return;
2053        }
2054        if (mPendingMoveSmoothScroller == null) {
2055            // Stop existing scroller and create a new PendingMoveSmoothScroller.
2056            mBaseGridView.stopScroll();
2057            PendingMoveSmoothScroller linearSmoothScroller = new PendingMoveSmoothScroller(
2058                    forward ? 1 : -1, mNumRows > 1);
2059            mFocusPositionOffset = 0;
2060            startSmoothScroll(linearSmoothScroller);
2061            if (linearSmoothScroller.isRunning()) {
2062                mPendingMoveSmoothScroller = linearSmoothScroller;
2063            }
2064        } else {
2065            if (forward) {
2066                mPendingMoveSmoothScroller.increasePendingMoves();
2067            } else {
2068                mPendingMoveSmoothScroller.decreasePendingMoves();
2069            }
2070        }
2071    }
2072
2073    @Override
2074    public void onItemsAdded(RecyclerView recyclerView, int positionStart, int itemCount) {
2075        if (DEBUG) Log.v(getTag(), "onItemsAdded positionStart "
2076                + positionStart + " itemCount " + itemCount);
2077        if (mFocusPosition != NO_POSITION && mFocusPositionOffset != Integer.MIN_VALUE) {
2078            int pos = mFocusPosition + mFocusPositionOffset;
2079            if (positionStart <= pos) {
2080                mFocusPositionOffset += itemCount;
2081            }
2082        }
2083        mChildrenStates.clear();
2084    }
2085
2086    @Override
2087    public void onItemsChanged(RecyclerView recyclerView) {
2088        if (DEBUG) Log.v(getTag(), "onItemsChanged");
2089        mFocusPositionOffset = 0;
2090        mChildrenStates.clear();
2091    }
2092
2093    @Override
2094    public void onItemsRemoved(RecyclerView recyclerView, int positionStart, int itemCount) {
2095        if (DEBUG) Log.v(getTag(), "onItemsRemoved positionStart "
2096                + positionStart + " itemCount " + itemCount);
2097        if (mFocusPosition != NO_POSITION && mFocusPositionOffset != Integer.MIN_VALUE) {
2098            int pos = mFocusPosition + mFocusPositionOffset;
2099            if (positionStart <= pos) {
2100                if (positionStart + itemCount > pos) {
2101                    // stop updating offset after the focus item was removed
2102                    mFocusPositionOffset = Integer.MIN_VALUE;
2103                } else {
2104                    mFocusPositionOffset -= itemCount;
2105                }
2106            }
2107        }
2108        mChildrenStates.clear();
2109    }
2110
2111    @Override
2112    public void onItemsMoved(RecyclerView recyclerView, int fromPosition, int toPosition,
2113            int itemCount) {
2114        if (DEBUG) Log.v(getTag(), "onItemsMoved fromPosition "
2115                + fromPosition + " toPosition " + toPosition);
2116        if (mFocusPosition != NO_POSITION && mFocusPositionOffset != Integer.MIN_VALUE) {
2117            int pos = mFocusPosition + mFocusPositionOffset;
2118            if (fromPosition <= pos && pos < fromPosition + itemCount) {
2119                // moved items include focused position
2120                mFocusPositionOffset += toPosition - fromPosition;
2121            } else if (fromPosition < pos && toPosition > pos - itemCount) {
2122                // move items before focus position to after focused position
2123                mFocusPositionOffset -= itemCount;
2124            } else if (fromPosition > pos && toPosition < pos) {
2125                // move items after focus position to before focused position
2126                mFocusPositionOffset += itemCount;
2127            }
2128        }
2129        mChildrenStates.clear();
2130    }
2131
2132    @Override
2133    public void onItemsUpdated(RecyclerView recyclerView, int positionStart, int itemCount) {
2134        if (DEBUG) Log.v(getTag(), "onItemsUpdated positionStart "
2135                + positionStart + " itemCount " + itemCount);
2136        for (int i = positionStart, end = positionStart + itemCount; i < end; i++) {
2137            mChildrenStates.remove(i);
2138        }
2139    }
2140
2141    @Override
2142    public boolean onRequestChildFocus(RecyclerView parent, View child, View focused) {
2143        if (mFocusSearchDisabled) {
2144            return true;
2145        }
2146        if (getPositionByView(child) == NO_POSITION) {
2147            // This shouldn't happen, but in case it does be sure not to attempt a
2148            // scroll to a view whose item has been removed.
2149            return true;
2150        }
2151        if (!mInLayout && !mInSelection && !mInScroll) {
2152            scrollToView(child, true);
2153        }
2154        return true;
2155    }
2156
2157    @Override
2158    public boolean requestChildRectangleOnScreen(RecyclerView parent, View view, Rect rect,
2159            boolean immediate) {
2160        if (DEBUG) Log.v(getTag(), "requestChildRectangleOnScreen " + view + " " + rect);
2161        return false;
2162    }
2163
2164    int getScrollOffsetX() {
2165        return mOrientation == HORIZONTAL ? mScrollOffsetPrimary : mScrollOffsetSecondary;
2166    }
2167
2168    int getScrollOffsetY() {
2169        return mOrientation == HORIZONTAL ? mScrollOffsetSecondary : mScrollOffsetPrimary;
2170    }
2171
2172    public void getViewSelectedOffsets(View view, int[] offsets) {
2173        if (mOrientation == HORIZONTAL) {
2174            offsets[0] = getPrimarySystemScrollPosition(view) - mScrollOffsetPrimary;
2175            offsets[1] = getSecondarySystemScrollPosition(view) - mScrollOffsetSecondary;
2176        } else {
2177            offsets[1] = getPrimarySystemScrollPosition(view) - mScrollOffsetPrimary;
2178            offsets[0] = getSecondarySystemScrollPosition(view) - mScrollOffsetSecondary;
2179        }
2180    }
2181
2182    private int getPrimarySystemScrollPosition(View view) {
2183        final int viewCenterPrimary = mScrollOffsetPrimary + getViewCenter(view);
2184        final int viewMin = getViewMin(view);
2185        final int viewMax = getViewMax(view);
2186        // TODO: change to use State object in onRequestChildFocus()
2187        boolean isMin, isMax;
2188        if (!mReverseFlowPrimary) {
2189            isMin = mGrid.getFirstVisibleIndex() == 0;
2190            isMax = mGrid.getLastVisibleIndex() == (mState == null ?
2191                    getItemCount() : mState.getItemCount()) - 1;
2192        } else {
2193            isMax = mGrid.getFirstVisibleIndex() == 0;
2194            isMin = mGrid.getLastVisibleIndex() == (mState == null ?
2195                    getItemCount() : mState.getItemCount()) - 1;
2196        }
2197        for (int i = getChildCount() - 1; (isMin || isMax) && i >= 0; i--) {
2198            View v = getChildAt(i);
2199            if (v == view || v == null) {
2200                continue;
2201            }
2202            if (isMin && getViewMin(v) < viewMin) {
2203                isMin = false;
2204            }
2205            if (isMax && getViewMax(v) > viewMax) {
2206                isMax = false;
2207            }
2208        }
2209        return mWindowAlignment.mainAxis().getSystemScrollPos(viewCenterPrimary, isMin, isMax);
2210    }
2211
2212    private int getSecondarySystemScrollPosition(View view) {
2213        int viewCenterSecondary = mScrollOffsetSecondary + getViewCenterSecondary(view);
2214        int pos = getPositionByView(view);
2215        Grid.Location location = mGrid.getLocation(pos);
2216        final int row = location.row;
2217        final boolean isMin, isMax;
2218        if (!mReverseFlowSecondary) {
2219            isMin = row == 0;
2220            isMax = row == mGrid.getNumRows() - 1;
2221        } else {
2222            isMax = row == 0;
2223            isMin = row == mGrid.getNumRows() - 1;
2224        }
2225        return mWindowAlignment.secondAxis().getSystemScrollPos(viewCenterSecondary, isMin, isMax);
2226    }
2227
2228    /**
2229     * Scroll to a given child view and change mFocusPosition.
2230     */
2231    private void scrollToView(View view, boolean smooth) {
2232        int newFocusPosition = getPositionByView(view);
2233        if (newFocusPosition != mFocusPosition) {
2234            mFocusPosition = newFocusPosition;
2235            mFocusPositionOffset = 0;
2236            if (!mInLayout) {
2237                dispatchChildSelected();
2238            }
2239            if (mBaseGridView.isChildrenDrawingOrderEnabledInternal()) {
2240                mBaseGridView.invalidate();
2241            }
2242        }
2243        if (view == null) {
2244            return;
2245        }
2246        if (!view.hasFocus() && mBaseGridView.hasFocus()) {
2247            // transfer focus to the child if it does not have focus yet (e.g. triggered
2248            // by setSelection())
2249            view.requestFocus();
2250        }
2251        if (!mScrollEnabled && smooth) {
2252            return;
2253        }
2254        if (getScrollPosition(view, sTwoInts)) {
2255            scrollGrid(sTwoInts[0], sTwoInts[1], smooth);
2256        }
2257    }
2258
2259    private boolean getScrollPosition(View view, int[] deltas) {
2260        switch (mFocusScrollStrategy) {
2261        case BaseGridView.FOCUS_SCROLL_ALIGNED:
2262        default:
2263            return getAlignedPosition(view, deltas);
2264        case BaseGridView.FOCUS_SCROLL_ITEM:
2265        case BaseGridView.FOCUS_SCROLL_PAGE:
2266            return getNoneAlignedPosition(view, deltas);
2267        }
2268    }
2269
2270    private boolean getNoneAlignedPosition(View view, int[] deltas) {
2271        int pos = getPositionByView(view);
2272        int viewMin = getViewMin(view);
2273        int viewMax = getViewMax(view);
2274        // we either align "firstView" to left/top padding edge
2275        // or align "lastView" to right/bottom padding edge
2276        View firstView = null;
2277        View lastView = null;
2278        int paddingLow = mWindowAlignment.mainAxis().getPaddingLow();
2279        int clientSize = mWindowAlignment.mainAxis().getClientSize();
2280        final int row = mGrid.getRowIndex(pos);
2281        if (viewMin < paddingLow) {
2282            // view enters low padding area:
2283            firstView = view;
2284            if (mFocusScrollStrategy == BaseGridView.FOCUS_SCROLL_PAGE) {
2285                // scroll one "page" left/top,
2286                // align first visible item of the "page" at the low padding edge.
2287                while (prependOneColumnVisibleItems()) {
2288                    CircularIntArray positions =
2289                            mGrid.getItemPositionsInRows(mGrid.getFirstVisibleIndex(), pos)[row];
2290                    firstView = findViewByPosition(positions.get(0));
2291                    if (viewMax - getViewMin(firstView) > clientSize) {
2292                        if (positions.size() > 2) {
2293                            firstView = findViewByPosition(positions.get(2));
2294                        }
2295                        break;
2296                    }
2297                }
2298            }
2299        } else if (viewMax > clientSize + paddingLow) {
2300            // view enters high padding area:
2301            if (mFocusScrollStrategy == BaseGridView.FOCUS_SCROLL_PAGE) {
2302                // scroll whole one page right/bottom, align view at the low padding edge.
2303                firstView = view;
2304                do {
2305                    CircularIntArray positions =
2306                            mGrid.getItemPositionsInRows(pos, mGrid.getLastVisibleIndex())[row];
2307                    lastView = findViewByPosition(positions.get(positions.size() - 1));
2308                    if (getViewMax(lastView) - viewMin > clientSize) {
2309                        lastView = null;
2310                        break;
2311                    }
2312                } while (appendOneColumnVisibleItems());
2313                if (lastView != null) {
2314                    // however if we reached end,  we should align last view.
2315                    firstView = null;
2316                }
2317            } else {
2318                lastView = view;
2319            }
2320        }
2321        int scrollPrimary = 0;
2322        int scrollSecondary = 0;
2323        if (firstView != null) {
2324            scrollPrimary = getViewMin(firstView) - paddingLow;
2325        } else if (lastView != null) {
2326            scrollPrimary = getViewMax(lastView) - (paddingLow + clientSize);
2327        }
2328        View secondaryAlignedView;
2329        if (firstView != null) {
2330            secondaryAlignedView = firstView;
2331        } else if (lastView != null) {
2332            secondaryAlignedView = lastView;
2333        } else {
2334            secondaryAlignedView = view;
2335        }
2336        scrollSecondary = getSecondarySystemScrollPosition(secondaryAlignedView);
2337        scrollSecondary -= mScrollOffsetSecondary;
2338        if (scrollPrimary != 0 || scrollSecondary != 0) {
2339            deltas[0] = scrollPrimary;
2340            deltas[1] = scrollSecondary;
2341            return true;
2342        }
2343        return false;
2344    }
2345
2346    private boolean getAlignedPosition(View view, int[] deltas) {
2347        int scrollPrimary = getPrimarySystemScrollPosition(view);
2348        int scrollSecondary = getSecondarySystemScrollPosition(view);
2349        if (DEBUG) {
2350            Log.v(getTag(), "getAlignedPosition " + scrollPrimary + " " + scrollSecondary
2351                    + " " + mPrimaryScrollExtra + " " + mWindowAlignment);
2352            Log.v(getTag(), "getAlignedPosition " + mScrollOffsetPrimary + " " + mScrollOffsetSecondary);
2353        }
2354        scrollPrimary -= mScrollOffsetPrimary;
2355        scrollSecondary -= mScrollOffsetSecondary;
2356        scrollPrimary += mPrimaryScrollExtra;
2357        if (scrollPrimary != 0 || scrollSecondary != 0) {
2358            deltas[0] = scrollPrimary;
2359            deltas[1] = scrollSecondary;
2360            return true;
2361        }
2362        return false;
2363    }
2364
2365    private void scrollGrid(int scrollPrimary, int scrollSecondary, boolean smooth) {
2366        if (mInLayout) {
2367            scrollDirectionPrimary(scrollPrimary);
2368            scrollDirectionSecondary(scrollSecondary);
2369        } else {
2370            int scrollX;
2371            int scrollY;
2372            if (mOrientation == HORIZONTAL) {
2373                scrollX = scrollPrimary;
2374                scrollY = scrollSecondary;
2375            } else {
2376                scrollX = scrollSecondary;
2377                scrollY = scrollPrimary;
2378            }
2379            if (smooth) {
2380                mBaseGridView.smoothScrollBy(scrollX, scrollY);
2381            } else {
2382                mBaseGridView.scrollBy(scrollX, scrollY);
2383            }
2384        }
2385    }
2386
2387    public void setPruneChild(boolean pruneChild) {
2388        if (mPruneChild != pruneChild) {
2389            mPruneChild = pruneChild;
2390            if (mPruneChild) {
2391                requestLayout();
2392            }
2393        }
2394    }
2395
2396    public boolean getPruneChild() {
2397        return mPruneChild;
2398    }
2399
2400    public void setScrollEnabled(boolean scrollEnabled) {
2401        if (mScrollEnabled != scrollEnabled) {
2402            mScrollEnabled = scrollEnabled;
2403            if (mScrollEnabled && mFocusScrollStrategy == BaseGridView.FOCUS_SCROLL_ALIGNED
2404                    && mFocusPosition != NO_POSITION) {
2405                scrollToSelection(mBaseGridView, mFocusPosition, true, mPrimaryScrollExtra);
2406            }
2407        }
2408    }
2409
2410    public boolean isScrollEnabled() {
2411        return mScrollEnabled;
2412    }
2413
2414    private int findImmediateChildIndex(View view) {
2415        while (view != null && view != mBaseGridView) {
2416            int index = mBaseGridView.indexOfChild(view);
2417            if (index >= 0) {
2418                return index;
2419            }
2420            view = (View) view.getParent();
2421        }
2422        return NO_POSITION;
2423    }
2424
2425    void setFocusSearchDisabled(boolean disabled) {
2426        mFocusSearchDisabled = disabled;
2427    }
2428
2429    boolean isFocusSearchDisabled() {
2430        return mFocusSearchDisabled;
2431    }
2432
2433    @Override
2434    public View onInterceptFocusSearch(View focused, int direction) {
2435        if (mFocusSearchDisabled) {
2436            return focused;
2437        }
2438        return null;
2439    }
2440
2441    boolean hasPreviousViewInSameRow(int pos) {
2442        if (mGrid == null || pos == NO_POSITION || mGrid.getFirstVisibleIndex() < 0) {
2443            return false;
2444        }
2445        if (mGrid.getFirstVisibleIndex() > 0) {
2446            return true;
2447        }
2448        final int focusedRow = mGrid.getLocation(pos).row;
2449        for (int i = getChildCount() - 1; i >= 0; i--) {
2450            int position = getPositionByIndex(i);
2451            Grid.Location loc = mGrid.getLocation(position);
2452            if (loc != null && loc.row == focusedRow) {
2453                if (position < pos) {
2454                    return true;
2455                }
2456            }
2457        }
2458        return false;
2459    }
2460
2461    @Override
2462    public boolean onAddFocusables(RecyclerView recyclerView,
2463            ArrayList<View> views, int direction, int focusableMode) {
2464        if (mFocusSearchDisabled) {
2465            return true;
2466        }
2467        // If this viewgroup or one of its children currently has focus then we
2468        // consider our children for focus searching in main direction on the same row.
2469        // If this viewgroup has no focus and using focus align, we want the system
2470        // to ignore our children and pass focus to the viewgroup, which will pass
2471        // focus on to its children appropriately.
2472        // If this viewgroup has no focus and not using focus align, we want to
2473        // consider the child that does not overlap with padding area.
2474        if (recyclerView.hasFocus()) {
2475            final int movement = getMovement(direction);
2476            if (movement != PREV_ITEM && movement != NEXT_ITEM) {
2477                // Move on secondary direction uses default addFocusables().
2478                return false;
2479            }
2480            if (mPendingMoveSmoothScroller != null) {
2481                // don't find next focusable if has pending movement.
2482                return true;
2483            }
2484            final View focused = recyclerView.findFocus();
2485            final int focusedPos = getPositionByIndex(findImmediateChildIndex(focused));
2486            // Add focusables of focused item.
2487            if (focusedPos != NO_POSITION) {
2488                findViewByPosition(focusedPos).addFocusables(views,  direction, focusableMode);
2489            }
2490            final int focusedRow = mGrid != null && focusedPos != NO_POSITION ?
2491                    mGrid.getLocation(focusedPos).row : NO_POSITION;
2492            // Add focusables of next neighbor of same row on the focus search direction.
2493            if (mGrid != null) {
2494                final int focusableCount = views.size();
2495                for (int i = 0, count = getChildCount(); i < count; i++) {
2496                    int index = movement == NEXT_ITEM ? i : count - 1 - i;
2497                    final View child = getChildAt(index);
2498                    if (child.getVisibility() != View.VISIBLE || !child.hasFocusable()) {
2499                        continue;
2500                    }
2501                    int position = getPositionByIndex(index);
2502                    Grid.Location loc = mGrid.getLocation(position);
2503                    if (focusedRow == NO_POSITION || (loc != null && loc.row == focusedRow)) {
2504                        if (focusedPos == NO_POSITION ||
2505                                (movement == NEXT_ITEM && position > focusedPos)
2506                                || (movement == PREV_ITEM && position < focusedPos)) {
2507                            child.addFocusables(views,  direction, focusableMode);
2508                            if (views.size() > focusableCount) {
2509                                break;
2510                            }
2511                        }
2512                    }
2513                }
2514            }
2515        } else {
2516            if (mFocusScrollStrategy != BaseGridView.FOCUS_SCROLL_ALIGNED) {
2517                // adding views not overlapping padding area to avoid scrolling in gaining focus
2518                int left = mWindowAlignment.mainAxis().getPaddingLow();
2519                int right = mWindowAlignment.mainAxis().getClientSize() + left;
2520                int focusableCount = views.size();
2521                for (int i = 0, count = getChildCount(); i < count; i++) {
2522                    View child = getChildAt(i);
2523                    if (child.getVisibility() == View.VISIBLE) {
2524                        if (getViewMin(child) >= left && getViewMax(child) <= right) {
2525                            child.addFocusables(views, direction, focusableMode);
2526                        }
2527                    }
2528                }
2529                // if we cannot find any, then just add all children.
2530                if (views.size() == focusableCount) {
2531                    for (int i = 0, count = getChildCount(); i < count; i++) {
2532                        View child = getChildAt(i);
2533                        if (child.getVisibility() == View.VISIBLE) {
2534                            child.addFocusables(views, direction, focusableMode);
2535                        }
2536                    }
2537                    if (views.size() != focusableCount) {
2538                        return true;
2539                    }
2540                } else {
2541                    return true;
2542                }
2543                // if still cannot find any, fall through and add itself
2544            }
2545            if (recyclerView.isFocusable()) {
2546                views.add(recyclerView);
2547            }
2548        }
2549        return true;
2550    }
2551
2552    private boolean hasCreatedLastItem() {
2553        int count = mState.getItemCount();
2554        return count == 0 || findViewByPosition(count - 1) != null;
2555    }
2556
2557    private boolean hasCreatedFirstItem() {
2558        int count = mState.getItemCount();
2559        return count == 0 || findViewByPosition(0) != null;
2560    }
2561
2562    boolean canScrollTo(View view) {
2563        return view.getVisibility() == View.VISIBLE && (!hasFocus() || view.hasFocusable());
2564    }
2565
2566    @Override
2567    public View onFocusSearchFailed(View focused, int direction, Recycler recycler,
2568            RecyclerView.State state) {
2569        if (DEBUG) Log.v(getTag(), "onFocusSearchFailed direction " + direction);
2570
2571        View view = null;
2572        int movement = getMovement(direction);
2573        final boolean isScroll = mBaseGridView.getScrollState() != RecyclerView.SCROLL_STATE_IDLE;
2574        saveContext(recycler, state);
2575        if (movement == NEXT_ITEM) {
2576            if (isScroll || !mFocusOutEnd) {
2577                view = focused;
2578            }
2579            if (mScrollEnabled && !hasCreatedLastItem()) {
2580                processPendingMovement(true);
2581                view = focused;
2582            }
2583        } else if (movement == PREV_ITEM) {
2584            if (isScroll || !mFocusOutFront) {
2585                view = focused;
2586            }
2587            if (mScrollEnabled && !hasCreatedFirstItem()) {
2588                processPendingMovement(false);
2589                view = focused;
2590            }
2591        }
2592        leaveContext();
2593        if (DEBUG) Log.v(getTag(), "onFocusSearchFailed returning view " + view);
2594        return view;
2595    }
2596
2597    boolean gridOnRequestFocusInDescendants(RecyclerView recyclerView, int direction,
2598            Rect previouslyFocusedRect) {
2599        switch (mFocusScrollStrategy) {
2600        case BaseGridView.FOCUS_SCROLL_ALIGNED:
2601        default:
2602            return gridOnRequestFocusInDescendantsAligned(recyclerView,
2603                    direction, previouslyFocusedRect);
2604        case BaseGridView.FOCUS_SCROLL_PAGE:
2605        case BaseGridView.FOCUS_SCROLL_ITEM:
2606            return gridOnRequestFocusInDescendantsUnaligned(recyclerView,
2607                    direction, previouslyFocusedRect);
2608        }
2609    }
2610
2611    private boolean gridOnRequestFocusInDescendantsAligned(RecyclerView recyclerView,
2612            int direction, Rect previouslyFocusedRect) {
2613        View view = findViewByPosition(mFocusPosition);
2614        if (view != null) {
2615            boolean result = view.requestFocus(direction, previouslyFocusedRect);
2616            if (!result && DEBUG) {
2617                Log.w(getTag(), "failed to request focus on " + view);
2618            }
2619            return result;
2620        }
2621        return false;
2622    }
2623
2624    private boolean gridOnRequestFocusInDescendantsUnaligned(RecyclerView recyclerView,
2625            int direction, Rect previouslyFocusedRect) {
2626        // focus to view not overlapping padding area to avoid scrolling in gaining focus
2627        int index;
2628        int increment;
2629        int end;
2630        int count = getChildCount();
2631        if ((direction & View.FOCUS_FORWARD) != 0) {
2632            index = 0;
2633            increment = 1;
2634            end = count;
2635        } else {
2636            index = count - 1;
2637            increment = -1;
2638            end = -1;
2639        }
2640        int left = mWindowAlignment.mainAxis().getPaddingLow();
2641        int right = mWindowAlignment.mainAxis().getClientSize() + left;
2642        for (int i = index; i != end; i += increment) {
2643            View child = getChildAt(i);
2644            if (child.getVisibility() == View.VISIBLE) {
2645                if (getViewMin(child) >= left && getViewMax(child) <= right) {
2646                    if (child.requestFocus(direction, previouslyFocusedRect)) {
2647                        return true;
2648                    }
2649                }
2650            }
2651        }
2652        return false;
2653    }
2654
2655    private final static int PREV_ITEM = 0;
2656    private final static int NEXT_ITEM = 1;
2657    private final static int PREV_ROW = 2;
2658    private final static int NEXT_ROW = 3;
2659
2660    private int getMovement(int direction) {
2661        int movement = View.FOCUS_LEFT;
2662
2663        if (mOrientation == HORIZONTAL) {
2664            switch(direction) {
2665                case View.FOCUS_LEFT:
2666                    movement = (!mReverseFlowPrimary) ? PREV_ITEM : NEXT_ITEM;
2667                    break;
2668                case View.FOCUS_RIGHT:
2669                    movement = (!mReverseFlowPrimary) ? NEXT_ITEM : PREV_ITEM;
2670                    break;
2671                case View.FOCUS_UP:
2672                    movement = PREV_ROW;
2673                    break;
2674                case View.FOCUS_DOWN:
2675                    movement = NEXT_ROW;
2676                    break;
2677            }
2678         } else if (mOrientation == VERTICAL) {
2679             switch(direction) {
2680                 case View.FOCUS_LEFT:
2681                     movement = (!mReverseFlowPrimary) ? PREV_ROW : NEXT_ROW;
2682                     break;
2683                 case View.FOCUS_RIGHT:
2684                     movement = (!mReverseFlowPrimary) ? NEXT_ROW : PREV_ROW;
2685                     break;
2686                 case View.FOCUS_UP:
2687                     movement = PREV_ITEM;
2688                     break;
2689                 case View.FOCUS_DOWN:
2690                     movement = NEXT_ITEM;
2691                     break;
2692             }
2693         }
2694
2695        return movement;
2696    }
2697
2698    int getChildDrawingOrder(RecyclerView recyclerView, int childCount, int i) {
2699        View view = findViewByPosition(mFocusPosition);
2700        if (view == null) {
2701            return i;
2702        }
2703        int focusIndex = recyclerView.indexOfChild(view);
2704        // supposely 0 1 2 3 4 5 6 7 8 9, 4 is the center item
2705        // drawing order is 0 1 2 3 9 8 7 6 5 4
2706        if (i < focusIndex) {
2707            return i;
2708        } else if (i < childCount - 1) {
2709            return focusIndex + childCount - 1 - i;
2710        } else {
2711            return focusIndex;
2712        }
2713    }
2714
2715    @Override
2716    public void onAdapterChanged(RecyclerView.Adapter oldAdapter,
2717            RecyclerView.Adapter newAdapter) {
2718        if (DEBUG) Log.v(getTag(), "onAdapterChanged to " + newAdapter);
2719        if (oldAdapter != null) {
2720            discardLayoutInfo();
2721            mFocusPosition = NO_POSITION;
2722            mFocusPositionOffset = 0;
2723            mChildrenStates.clear();
2724        }
2725        super.onAdapterChanged(oldAdapter, newAdapter);
2726    }
2727
2728    private void discardLayoutInfo() {
2729        mGrid = null;
2730        mRowSizeSecondary = null;
2731        mRowSecondarySizeRefresh = false;
2732    }
2733
2734    public void setLayoutEnabled(boolean layoutEnabled) {
2735        if (mLayoutEnabled != layoutEnabled) {
2736            mLayoutEnabled = layoutEnabled;
2737            requestLayout();
2738        }
2739    }
2740
2741    void setChildrenVisibility(int visiblity) {
2742        mChildVisibility = visiblity;
2743        if (mChildVisibility != -1) {
2744            int count = getChildCount();
2745            for (int i= 0; i < count; i++) {
2746                getChildAt(i).setVisibility(mChildVisibility);
2747            }
2748        }
2749    }
2750
2751    final static class SavedState implements Parcelable {
2752
2753        int index; // index inside adapter of the current view
2754        Bundle childStates = Bundle.EMPTY;
2755
2756        @Override
2757        public void writeToParcel(Parcel out, int flags) {
2758            out.writeInt(index);
2759            out.writeBundle(childStates);
2760        }
2761
2762        @SuppressWarnings("hiding")
2763        public static final Parcelable.Creator<SavedState> CREATOR =
2764                new Parcelable.Creator<SavedState>() {
2765                    @Override
2766                    public SavedState createFromParcel(Parcel in) {
2767                        return new SavedState(in);
2768                    }
2769
2770                    @Override
2771                    public SavedState[] newArray(int size) {
2772                        return new SavedState[size];
2773                    }
2774                };
2775
2776        @Override
2777        public int describeContents() {
2778            return 0;
2779        }
2780
2781        SavedState(Parcel in) {
2782            index = in.readInt();
2783            childStates = in.readBundle(GridLayoutManager.class.getClassLoader());
2784        }
2785
2786        SavedState() {
2787        }
2788    }
2789
2790    @Override
2791    public Parcelable onSaveInstanceState() {
2792        if (DEBUG) Log.v(getTag(), "onSaveInstanceState getSelection() " + getSelection());
2793        SavedState ss = new SavedState();
2794        for (int i = 0, count = getChildCount(); i < count; i++) {
2795            View view = getChildAt(i);
2796            int position = getPositionByView(view);
2797            if (position != NO_POSITION) {
2798                mChildrenStates.saveOnScreenView(view, position);
2799            }
2800        }
2801        ss.index = getSelection();
2802        ss.childStates = mChildrenStates.saveAsBundle();
2803        return ss;
2804    }
2805
2806    void onChildRecycled(RecyclerView.ViewHolder holder) {
2807        final int position = holder.getAdapterPosition();
2808        if (position != NO_POSITION) {
2809            mChildrenStates.saveOffscreenView(holder.itemView, position);
2810        }
2811    }
2812
2813    @Override
2814    public void onRestoreInstanceState(Parcelable state) {
2815        if (!(state instanceof SavedState)) {
2816            return;
2817        }
2818        SavedState loadingState = (SavedState)state;
2819        mFocusPosition = loadingState.index;
2820        mFocusPositionOffset = 0;
2821        mChildrenStates.loadFromBundle(loadingState.childStates);
2822        mForceFullLayout = true;
2823        requestLayout();
2824        if (DEBUG) Log.v(getTag(), "onRestoreInstanceState mFocusPosition " + mFocusPosition);
2825    }
2826}
2827