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