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