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