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