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