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