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