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