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