GridLayoutManager.java revision 7538845719cd1e588185ce9ae60320377862ef8d
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() && mGrid.getFirstVisibleIndex() >= 0 &&
947                !mForceFullLayout && mGrid != null) {
948            updateScrollController();
949            updateScrollSecondAxis();
950            mGrid.setMargin(mMarginPrimary);
951            if (!focusViewWasInTree && mFocusPosition != NO_POSITION) {
952                mGrid.setStart(mFocusPosition);
953            }
954            return true;
955        } else {
956            mForceFullLayout = false;
957            int firstVisibleIndex = focusViewWasInTree ? mGrid.getFirstVisibleIndex() : 0;
958
959            if (mGrid == null || mNumRows != mGrid.getNumRows() ||
960                    mReverseFlowPrimary != mGrid.isReversedFlow()) {
961                mGrid = Grid.createStaggeredMultipleRows(mNumRows);
962                mGrid.setProvider(mGridProvider);
963                mGrid.setReversedFlow(mReverseFlowPrimary);
964            }
965            initScrollController();
966            updateScrollSecondAxis();
967            mGrid.setMargin(mMarginPrimary);
968            detachAndScrapAttachedViews(mRecycler);
969            mGrid.resetVisibleIndex();
970            if (mFocusPosition == NO_POSITION) {
971                mBaseGridView.clearFocus();
972            }
973            mWindowAlignment.mainAxis().invalidateScrollMin();
974            mWindowAlignment.mainAxis().invalidateScrollMax();
975            if (focusViewWasInTree && firstVisibleIndex <= mFocusPosition) {
976                // if focusView was in tree, we will add item from first visible item
977                mGrid.setStart(firstVisibleIndex);
978            } else {
979                // if focusView was not in tree, it's probably because focus position jumped
980                // far away from visible range,  so use mFocusPosition as start
981                mGrid.setStart(mFocusPosition);
982            }
983            return false;
984        }
985    }
986
987    private int getRowSizeSecondary(int rowIndex) {
988        if (mFixedRowSizeSecondary != 0) {
989            return mFixedRowSizeSecondary;
990        }
991        if (mRowSizeSecondary == null) {
992            return 0;
993        }
994        return mRowSizeSecondary[rowIndex];
995    }
996
997    private int getRowStartSecondary(int rowIndex) {
998        int start = 0;
999        // Iterate from left to right, which is a different index traversal
1000        // in RTL flow
1001        if (mReverseFlowSecondary) {
1002            for (int i = mNumRows-1; i > rowIndex; i--) {
1003                start += getRowSizeSecondary(i) + mMarginSecondary;
1004            }
1005        } else {
1006            for (int i = 0; i < rowIndex; i++) {
1007                start += getRowSizeSecondary(i) + mMarginSecondary;
1008            }
1009        }
1010        return start;
1011    }
1012
1013    private int getSizeSecondary() {
1014        int rightmostIndex = mReverseFlowSecondary ? 0 : mNumRows - 1;
1015        return getRowStartSecondary(rightmostIndex) + getRowSizeSecondary(rightmostIndex);
1016    }
1017
1018    private void measureScrapChild(int position, int widthSpec, int heightSpec,
1019            int[] measuredDimension) {
1020        View view = mRecycler.getViewForPosition(position);
1021        if (view != null) {
1022            LayoutParams p = (LayoutParams) view.getLayoutParams();
1023            int childWidthSpec = ViewGroup.getChildMeasureSpec(widthSpec,
1024                    getPaddingLeft() + getPaddingRight(), p.width);
1025            int childHeightSpec = ViewGroup.getChildMeasureSpec(heightSpec,
1026                    getPaddingTop() + getPaddingBottom(), p.height);
1027            view.measure(childWidthSpec, childHeightSpec);
1028            measuredDimension[0] = view.getMeasuredWidth();
1029            measuredDimension[1] = view.getMeasuredHeight();
1030            mRecycler.recycleView(view);
1031        }
1032    }
1033
1034    private boolean processRowSizeSecondary(boolean measure) {
1035        if (mFixedRowSizeSecondary != 0) {
1036            return false;
1037        }
1038
1039        if (TRACE) TraceHelper.beginSection("processRowSizeSecondary");
1040        CircularIntArray[] rows = mGrid == null ? null : mGrid.getItemPositionsInRows();
1041        boolean changed = false;
1042        int scrapChildWidth = -1;
1043        int scrapChildHeight = -1;
1044
1045        for (int rowIndex = 0; rowIndex < mNumRows; rowIndex++) {
1046            CircularIntArray row = rows == null ? null : rows[rowIndex];
1047            final int rowItemsPairCount = row == null ? 0 : row.size();
1048            int rowSize = -1;
1049            for (int rowItemPairIndex = 0; rowItemPairIndex < rowItemsPairCount;
1050                    rowItemPairIndex += 2) {
1051                final int rowIndexStart = row.get(rowItemPairIndex);
1052                final int rowIndexEnd = row.get(rowItemPairIndex + 1);
1053                for (int i = rowIndexStart; i <= rowIndexEnd; i++) {
1054                    final View view = findViewByPosition(i);
1055                    if (view == null) {
1056                        continue;
1057                    }
1058                    if (measure && view.isLayoutRequested()) {
1059                        measureChild(view);
1060                    }
1061                    final int secondarySize = mOrientation == HORIZONTAL ?
1062                            view.getMeasuredHeight() : view.getMeasuredWidth();
1063                    if (secondarySize > rowSize) {
1064                        rowSize = secondarySize;
1065                    }
1066                }
1067            }
1068
1069            final int itemCount = mState.getItemCount();
1070            if (measure && rowSize < 0 && itemCount > 0) {
1071                if (scrapChildWidth < 0 && scrapChildHeight < 0) {
1072                    int position;
1073                    if (mFocusPosition == NO_POSITION) {
1074                        position = 0;
1075                    } else if (mFocusPosition >= itemCount) {
1076                        position = itemCount - 1;
1077                    } else {
1078                        position = mFocusPosition;
1079                    }
1080                    measureScrapChild(position,
1081                            MeasureSpec.makeMeasureSpec(0, MeasureSpec.UNSPECIFIED),
1082                            MeasureSpec.makeMeasureSpec(0, MeasureSpec.UNSPECIFIED),
1083                            mMeasuredDimension);
1084                    scrapChildWidth = mMeasuredDimension[0];
1085                    scrapChildHeight = mMeasuredDimension[1];
1086                    if (DEBUG) Log.v(TAG, "measured scrap child: " + scrapChildWidth +
1087                            " " + scrapChildHeight);
1088                }
1089                rowSize = mOrientation == HORIZONTAL ? scrapChildHeight : scrapChildWidth;
1090            }
1091            if (rowSize < 0) {
1092                rowSize = 0;
1093            }
1094            if (mRowSizeSecondary[rowIndex] != rowSize) {
1095                if (DEBUG) Log.v(getTag(), "row size secondary changed: " + mRowSizeSecondary[rowIndex] +
1096                        ", " + rowSize);
1097                mRowSizeSecondary[rowIndex] = rowSize;
1098                changed = true;
1099            }
1100        }
1101
1102        if (TRACE) TraceHelper.endSection();
1103        return changed;
1104    }
1105
1106    /**
1107     * Checks if we need to update row secondary sizes.
1108     */
1109    private void updateRowSecondarySizeRefresh() {
1110        mRowSecondarySizeRefresh = processRowSizeSecondary(false);
1111        if (mRowSecondarySizeRefresh) {
1112            if (DEBUG) Log.v(getTag(), "mRowSecondarySizeRefresh now set");
1113            forceRequestLayout();
1114        }
1115    }
1116
1117    private void forceRequestLayout() {
1118        if (DEBUG) Log.v(getTag(), "forceRequestLayout");
1119        // RecyclerView prevents us from requesting layout in many cases
1120        // (during layout, during scroll, etc.)
1121        // For secondary row size wrap_content support we currently need a
1122        // second layout pass to update the measured size after having measured
1123        // and added child views in layoutChildren.
1124        // Force the second layout by posting a delayed runnable.
1125        // TODO: investigate allowing a second layout pass,
1126        // or move child add/measure logic to the measure phase.
1127        ViewCompat.postOnAnimation(mBaseGridView, mRequestLayoutRunnable);
1128    }
1129
1130    private final Runnable mRequestLayoutRunnable = new Runnable() {
1131        @Override
1132        public void run() {
1133            if (DEBUG) Log.v(getTag(), "request Layout from runnable");
1134            requestLayout();
1135        }
1136     };
1137
1138    @Override
1139    public void onMeasure(Recycler recycler, State state, int widthSpec, int heightSpec) {
1140        saveContext(recycler, state);
1141
1142        int sizePrimary, sizeSecondary, modeSecondary, paddingSecondary;
1143        int measuredSizeSecondary;
1144        if (mOrientation == HORIZONTAL) {
1145            sizePrimary = MeasureSpec.getSize(widthSpec);
1146            sizeSecondary = MeasureSpec.getSize(heightSpec);
1147            modeSecondary = MeasureSpec.getMode(heightSpec);
1148            paddingSecondary = getPaddingTop() + getPaddingBottom();
1149        } else {
1150            sizeSecondary = MeasureSpec.getSize(widthSpec);
1151            sizePrimary = MeasureSpec.getSize(heightSpec);
1152            modeSecondary = MeasureSpec.getMode(widthSpec);
1153            paddingSecondary = getPaddingLeft() + getPaddingRight();
1154        }
1155        if (DEBUG) Log.v(getTag(), "onMeasure widthSpec " + Integer.toHexString(widthSpec) +
1156                " heightSpec " + Integer.toHexString(heightSpec) +
1157                " modeSecondary " + Integer.toHexString(modeSecondary) +
1158                " sizeSecondary " + sizeSecondary + " " + this);
1159
1160        mMaxSizeSecondary = sizeSecondary;
1161
1162        if (mRowSizeSecondaryRequested == ViewGroup.LayoutParams.WRAP_CONTENT) {
1163            mNumRows = mNumRowsRequested == 0 ? 1 : mNumRowsRequested;
1164            mFixedRowSizeSecondary = 0;
1165
1166            if (mRowSizeSecondary == null || mRowSizeSecondary.length != mNumRows) {
1167                mRowSizeSecondary = new int[mNumRows];
1168            }
1169
1170            // Measure all current children and update cached row heights
1171            processRowSizeSecondary(true);
1172
1173            switch (modeSecondary) {
1174            case MeasureSpec.UNSPECIFIED:
1175                measuredSizeSecondary = getSizeSecondary() + paddingSecondary;
1176                break;
1177            case MeasureSpec.AT_MOST:
1178                measuredSizeSecondary = Math.min(getSizeSecondary() + paddingSecondary,
1179                        mMaxSizeSecondary);
1180                break;
1181            case MeasureSpec.EXACTLY:
1182                measuredSizeSecondary = mMaxSizeSecondary;
1183                break;
1184            default:
1185                throw new IllegalStateException("wrong spec");
1186            }
1187
1188        } else {
1189            switch (modeSecondary) {
1190            case MeasureSpec.UNSPECIFIED:
1191                if (mRowSizeSecondaryRequested == 0) {
1192                    if (mOrientation == HORIZONTAL) {
1193                        throw new IllegalStateException("Must specify rowHeight or view height");
1194                    } else {
1195                        throw new IllegalStateException("Must specify columnWidth or view width");
1196                    }
1197                }
1198                mFixedRowSizeSecondary = mRowSizeSecondaryRequested;
1199                mNumRows = mNumRowsRequested == 0 ? 1 : mNumRowsRequested;
1200                measuredSizeSecondary = mFixedRowSizeSecondary * mNumRows + mMarginSecondary
1201                    * (mNumRows - 1) + paddingSecondary;
1202                break;
1203            case MeasureSpec.AT_MOST:
1204            case MeasureSpec.EXACTLY:
1205                if (mNumRowsRequested == 0 && mRowSizeSecondaryRequested == 0) {
1206                    mNumRows = 1;
1207                    mFixedRowSizeSecondary = sizeSecondary - paddingSecondary;
1208                } else if (mNumRowsRequested == 0) {
1209                    mFixedRowSizeSecondary = mRowSizeSecondaryRequested;
1210                    mNumRows = (sizeSecondary + mMarginSecondary)
1211                        / (mRowSizeSecondaryRequested + mMarginSecondary);
1212                } else if (mRowSizeSecondaryRequested == 0) {
1213                    mNumRows = mNumRowsRequested;
1214                    mFixedRowSizeSecondary = (sizeSecondary - paddingSecondary - mMarginSecondary
1215                            * (mNumRows - 1)) / mNumRows;
1216                } else {
1217                    mNumRows = mNumRowsRequested;
1218                    mFixedRowSizeSecondary = mRowSizeSecondaryRequested;
1219                }
1220                measuredSizeSecondary = sizeSecondary;
1221                if (modeSecondary == MeasureSpec.AT_MOST) {
1222                    int childrenSize = mFixedRowSizeSecondary * mNumRows + mMarginSecondary
1223                        * (mNumRows - 1) + paddingSecondary;
1224                    if (childrenSize < measuredSizeSecondary) {
1225                        measuredSizeSecondary = childrenSize;
1226                    }
1227                }
1228                break;
1229            default:
1230                throw new IllegalStateException("wrong spec");
1231            }
1232        }
1233        if (mOrientation == HORIZONTAL) {
1234            setMeasuredDimension(sizePrimary, measuredSizeSecondary);
1235        } else {
1236            setMeasuredDimension(measuredSizeSecondary, sizePrimary);
1237        }
1238        if (DEBUG) {
1239            Log.v(getTag(), "onMeasure sizePrimary " + sizePrimary +
1240                    " measuredSizeSecondary " + measuredSizeSecondary +
1241                    " mFixedRowSizeSecondary " + mFixedRowSizeSecondary +
1242                    " mNumRows " + mNumRows);
1243        }
1244        leaveContext();
1245    }
1246
1247    private void measureChild(View child) {
1248        if (TRACE) TraceHelper.beginSection("measureChild");
1249        final ViewGroup.LayoutParams lp = child.getLayoutParams();
1250        final int secondarySpec = (mRowSizeSecondaryRequested == ViewGroup.LayoutParams.WRAP_CONTENT) ?
1251                MeasureSpec.makeMeasureSpec(0, MeasureSpec.UNSPECIFIED) :
1252                MeasureSpec.makeMeasureSpec(mFixedRowSizeSecondary, MeasureSpec.EXACTLY);
1253        int widthSpec, heightSpec;
1254
1255        if (mOrientation == HORIZONTAL) {
1256            widthSpec = ViewGroup.getChildMeasureSpec(
1257                    MeasureSpec.makeMeasureSpec(0, MeasureSpec.UNSPECIFIED),
1258                    0, lp.width);
1259            heightSpec = ViewGroup.getChildMeasureSpec(secondarySpec, 0, lp.height);
1260        } else {
1261            heightSpec = ViewGroup.getChildMeasureSpec(
1262                    MeasureSpec.makeMeasureSpec(0, MeasureSpec.UNSPECIFIED),
1263                    0, lp.height);
1264            widthSpec = ViewGroup.getChildMeasureSpec(secondarySpec, 0, lp.width);
1265        }
1266        child.measure(widthSpec, heightSpec);
1267        if (DEBUG) Log.v(getTag(), "measureChild secondarySpec " + Integer.toHexString(secondarySpec) +
1268                " widthSpec " + Integer.toHexString(widthSpec) +
1269                " heightSpec " + Integer.toHexString(heightSpec) +
1270                " measuredWidth " + child.getMeasuredWidth() +
1271                " measuredHeight " + child.getMeasuredHeight());
1272        if (DEBUG) Log.v(getTag(), "child lp width " + lp.width + " height " + lp.height);
1273        if (TRACE) TraceHelper.endSection();
1274    }
1275
1276    private Grid.Provider mGridProvider = new Grid.Provider() {
1277
1278        @Override
1279        public int getCount() {
1280            return mState.getItemCount();
1281        }
1282
1283        @Override
1284        public int createItem(int index, boolean append, Object[] item) {
1285            if (TRACE) TraceHelper.beginSection("createItem");
1286            if (TRACE) TraceHelper.beginSection("getview");
1287            View v = getViewForPosition(index);
1288            if (TRACE) TraceHelper.endSection();
1289            // See recyclerView docs:  we don't need re-add scraped view if it was removed.
1290            if (!((RecyclerView.LayoutParams) v.getLayoutParams()).isItemRemoved()) {
1291                if (TRACE) TraceHelper.beginSection("addView");
1292                if (append) {
1293                    addView(v);
1294                } else {
1295                    addView(v, 0);
1296                }
1297                if (TRACE) TraceHelper.endSection();
1298                if (mChildVisibility != -1) {
1299                    v.setVisibility(mChildVisibility);
1300                }
1301
1302                if (mPendingMoveSmoothScroller != null) {
1303                    mPendingMoveSmoothScroller.consumePendingMovesBeforeLayout();
1304                }
1305                if (!mInLayout) {
1306                    // when we are appending item during scroll pass and the item's position
1307                    // matches the mFocusPosition,  we should signal a childSelected event.
1308                    // However if we are still running PendingMoveSmoothScroller,  we defer and
1309                    // signal the event in PendingMoveSmoothScroller.onStop().  This can
1310                    // avoid lots of childSelected events during a long smooth scrolling and
1311                    // increase performance.
1312                    if (index == mFocusPosition && (mPendingMoveSmoothScroller == null
1313                            || mPendingMoveSmoothScroller.mPendingMoves == 0)) {
1314                        dispatchChildSelected();
1315                    }
1316                } else if (!mInFastRelayout) {
1317                    // fastRelayout will dispatch event at end of onLayoutChildren().
1318                    // For full layout, two situations here:
1319                    // 1. mInLayoutSearchFocus is false, dispatchChildSelected() at mFocusPosition.
1320                    // 2. mInLayoutSearchFocus is true:  dispatchChildSelected() on first child
1321                    //    equal to or after mFocusPosition that can take focus.
1322                    if (!mInLayoutSearchFocus && index == mFocusPosition) {
1323                        dispatchChildSelected();
1324                    } else if (mInLayoutSearchFocus && index >= mFocusPosition
1325                            && v.hasFocusable()) {
1326                        mFocusPosition = index;
1327                        mInLayoutSearchFocus = false;
1328                        dispatchChildSelected();
1329                    }
1330                }
1331                measureChild(v);
1332            }
1333            item[0] = v;
1334            return mOrientation == HORIZONTAL ? v.getMeasuredWidth() : v.getMeasuredHeight();
1335        }
1336
1337        @Override
1338        public void addItem(Object item, int index, int length, int rowIndex, int edge) {
1339            View v = (View) item;
1340            int start, end;
1341            if (edge == Integer.MIN_VALUE || edge == Integer.MAX_VALUE) {
1342                edge = !mGrid.isReversedFlow() ? mWindowAlignment.mainAxis().getPaddingLow()
1343                        : mWindowAlignment.mainAxis().getSize()
1344                                - mWindowAlignment.mainAxis().getPaddingHigh();
1345            }
1346            boolean edgeIsMin = !mGrid.isReversedFlow();
1347            if (edgeIsMin) {
1348                start = edge;
1349                end = edge + length;
1350            } else {
1351                start = edge - length;
1352                end = edge;
1353            }
1354            int startSecondary = getRowStartSecondary(rowIndex) - mScrollOffsetSecondary;
1355            mChildrenStates.loadView(v, index);
1356            layoutChild(rowIndex, v, start, end, startSecondary);
1357            if (DEBUG) {
1358                Log.d(getTag(), "addView " + index + " " + v);
1359            }
1360            if (TRACE) TraceHelper.endSection();
1361
1362            if (index == mGrid.getFirstVisibleIndex()) {
1363                if (!mGrid.isReversedFlow()) {
1364                    updateScrollMin();
1365                } else {
1366                    updateScrollMax();
1367                }
1368            }
1369            if (index == mGrid.getLastVisibleIndex()) {
1370                if (!mGrid.isReversedFlow()) {
1371                    updateScrollMax();
1372                } else {
1373                    updateScrollMin();
1374                }
1375            }
1376            if (!mInLayout && mPendingMoveSmoothScroller != null) {
1377                mPendingMoveSmoothScroller.consumePendingMovesAfterLayout();
1378            }
1379            if (mChildLaidOutListener != null) {
1380                RecyclerView.ViewHolder vh = mBaseGridView.getChildViewHolder(v);
1381                mChildLaidOutListener.onChildLaidOut(mBaseGridView, v, index,
1382                        vh == null ? NO_ID : vh.getItemId());
1383            }
1384        }
1385
1386        @Override
1387        public void removeItem(int index) {
1388            if (TRACE) TraceHelper.beginSection("removeItem");
1389            View v = findViewByPosition(index);
1390            if (mInLayout) {
1391                detachAndScrapView(v, mRecycler);
1392            } else {
1393                removeAndRecycleView(v, mRecycler);
1394            }
1395            if (TRACE) TraceHelper.endSection();
1396        }
1397
1398        @Override
1399        public int getEdge(int index) {
1400            if (mReverseFlowPrimary) {
1401                return getViewMax(findViewByPosition(index));
1402            } else {
1403                return getViewMin(findViewByPosition(index));
1404            }
1405        }
1406
1407        @Override
1408        public int getSize(int index) {
1409            final View v = findViewByPosition(index);
1410            return mOrientation == HORIZONTAL ? v.getMeasuredWidth() : v.getMeasuredHeight();
1411        }
1412    };
1413
1414    private void layoutChild(int rowIndex, View v, int start, int end, int startSecondary) {
1415        if (TRACE) TraceHelper.beginSection("layoutChild");
1416        int sizeSecondary = mOrientation == HORIZONTAL ? v.getMeasuredHeight()
1417                : v.getMeasuredWidth();
1418        if (mFixedRowSizeSecondary > 0) {
1419            sizeSecondary = Math.min(sizeSecondary, mFixedRowSizeSecondary);
1420        }
1421        final int verticalGravity = mGravity & Gravity.VERTICAL_GRAVITY_MASK;
1422        final int horizontalGravity = (mReverseFlowPrimary || mReverseFlowSecondary) ?
1423                Gravity.getAbsoluteGravity(mGravity & Gravity.RELATIVE_HORIZONTAL_GRAVITY_MASK, View.LAYOUT_DIRECTION_RTL) :
1424                mGravity & Gravity.HORIZONTAL_GRAVITY_MASK;
1425        if (mOrientation == HORIZONTAL && verticalGravity == Gravity.TOP
1426                || mOrientation == VERTICAL && horizontalGravity == Gravity.LEFT) {
1427            // do nothing
1428        } else if (mOrientation == HORIZONTAL && verticalGravity == Gravity.BOTTOM
1429                || mOrientation == VERTICAL && horizontalGravity == Gravity.RIGHT) {
1430            startSecondary += getRowSizeSecondary(rowIndex) - sizeSecondary;
1431        } else if (mOrientation == HORIZONTAL && verticalGravity == Gravity.CENTER_VERTICAL
1432                || mOrientation == VERTICAL && horizontalGravity == Gravity.CENTER_HORIZONTAL) {
1433            startSecondary += (getRowSizeSecondary(rowIndex) - sizeSecondary) / 2;
1434        }
1435        int left, top, right, bottom;
1436        if (mOrientation == HORIZONTAL) {
1437            left = start;
1438            top = startSecondary;
1439            right = end;
1440            bottom = startSecondary + sizeSecondary;
1441        } else {
1442            top = start;
1443            left = startSecondary;
1444            bottom = end;
1445            right = startSecondary + sizeSecondary;
1446        }
1447        v.layout(left, top, right, bottom);
1448        updateChildOpticalInsets(v, left, top, right, bottom);
1449        updateChildAlignments(v);
1450        if (TRACE) TraceHelper.endSection();
1451    }
1452
1453    private void updateChildOpticalInsets(View v, int left, int top, int right, int bottom) {
1454        LayoutParams p = (LayoutParams) v.getLayoutParams();
1455        p.setOpticalInsets(left - v.getLeft(), top - v.getTop(),
1456                v.getRight() - right, v.getBottom() - bottom);
1457    }
1458
1459    private void updateChildAlignments(View v) {
1460        LayoutParams p = (LayoutParams) v.getLayoutParams();
1461        p.setAlignX(mItemAlignment.horizontal.getAlignmentPosition(v));
1462        p.setAlignY(mItemAlignment.vertical.getAlignmentPosition(v));
1463    }
1464
1465    private void updateChildAlignments() {
1466        for (int i = 0, c = getChildCount(); i < c; i++) {
1467            updateChildAlignments(getChildAt(i));
1468        }
1469    }
1470
1471    private void removeInvisibleViewsAtEnd() {
1472        if (mPruneChild) {
1473            mGrid.removeInvisibleItemsAtEnd(mFocusPosition,
1474                    mReverseFlowPrimary ? 0 : mSizePrimary);
1475        }
1476    }
1477
1478    private void removeInvisibleViewsAtFront() {
1479        if (mPruneChild) {
1480            mGrid.removeInvisibleItemsAtFront(mFocusPosition,
1481                    mReverseFlowPrimary ? mSizePrimary : 0);
1482        }
1483    }
1484
1485    private boolean appendOneColumnVisibleItems() {
1486        return mGrid.appendOneColumnVisibleItems();
1487    }
1488
1489    private boolean prependOneColumnVisibleItems() {
1490        return mGrid.prependOneColumnVisibleItems();
1491    }
1492
1493    private void appendVisibleItems() {
1494        mGrid.appendVisibleItems(mReverseFlowPrimary ? 0 : mSizePrimary);
1495    }
1496
1497    private void prependVisibleItems() {
1498        mGrid.prependVisibleItems(mReverseFlowPrimary ? mSizePrimary : 0);
1499    }
1500
1501    /**
1502     * Fast layout when there is no structure change, adapter change, etc.
1503     * It will layout all views was layout requested or updated, until hit a view
1504     * with different size,  then it break and detachAndScrap all views after that.
1505     */
1506    private void fastRelayout() {
1507        boolean invalidateAfter = false;
1508        final int childCount = getChildCount();
1509        int position = -1;
1510        for (int index = 0; index < childCount; index++) {
1511            View view = getChildAt(index);
1512            position = getPositionByIndex(index);
1513            Grid.Location location = mGrid.getLocation(position);
1514            if (location == null) {
1515                if (DEBUG) Log.w(getTag(), "fastRelayout(): no Location at " + position);
1516                invalidateAfter = true;
1517                break;
1518            }
1519
1520            int startSecondary = getRowStartSecondary(location.row) - mScrollOffsetSecondary;
1521            int primarySize, end;
1522            int start = getViewMin(view);
1523            int oldPrimarySize = (mOrientation == HORIZONTAL) ?
1524                    view.getMeasuredWidth() :
1525                    view.getMeasuredHeight();
1526
1527            LayoutParams lp = (LayoutParams) view.getLayoutParams();
1528            if (lp.viewNeedsUpdate()) {
1529                int viewIndex = mBaseGridView.indexOfChild(view);
1530                detachAndScrapView(view, mRecycler);
1531                view = getViewForPosition(position);
1532                addView(view, viewIndex);
1533            }
1534
1535            if (view.isLayoutRequested()) {
1536                measureChild(view);
1537            }
1538            if (mOrientation == HORIZONTAL) {
1539                primarySize = view.getMeasuredWidth();
1540                end = start + primarySize;
1541            } else {
1542                primarySize = view.getMeasuredHeight();
1543                end = start + primarySize;
1544            }
1545            layoutChild(location.row, view, start, end, startSecondary);
1546            if (oldPrimarySize != primarySize) {
1547                // size changed invalidate remaining Locations
1548                if (DEBUG) Log.d(getTag(), "fastRelayout: view size changed at " + position);
1549                invalidateAfter = true;
1550                break;
1551            }
1552        }
1553        if (invalidateAfter) {
1554            final int savedLastPos = mGrid.getLastVisibleIndex();
1555            mGrid.invalidateItemsAfter(position);
1556            if (mPruneChild) {
1557                // in regular prune child mode, we just append items up to edge limit
1558                appendVisibleItems();
1559                if (mFocusPosition >= 0 && mFocusPosition <= savedLastPos) {
1560                    // make sure add focus view back:  the view might be outside edge limit
1561                    // when there is delta in onLayoutChildren().
1562                    while (mGrid.getLastVisibleIndex() < mFocusPosition) {
1563                        mGrid.appendOneColumnVisibleItems();
1564                    }
1565                }
1566            } else {
1567                // prune disabled(e.g. in RowsFragment transition): append all removed items
1568                while (mGrid.appendOneColumnVisibleItems()
1569                        && mGrid.getLastVisibleIndex() < savedLastPos);
1570            }
1571        }
1572        updateScrollMin();
1573        updateScrollMax();
1574        updateScrollSecondAxis();
1575    }
1576
1577    public void removeAndRecycleAllViews(RecyclerView.Recycler recycler) {
1578        if (TRACE) TraceHelper.beginSection("removeAndRecycleAllViews");
1579        if (DEBUG) Log.v(TAG, "removeAndRecycleAllViews " + getChildCount());
1580        for (int i = getChildCount() - 1; i >= 0; i--) {
1581            removeAndRecycleViewAt(i, recycler);
1582        }
1583        if (TRACE) TraceHelper.endSection();
1584    }
1585
1586    // Lays out items based on the current scroll position
1587    @Override
1588    public void onLayoutChildren(RecyclerView.Recycler recycler, RecyclerView.State state) {
1589        if (DEBUG) {
1590            Log.v(getTag(), "layoutChildren start numRows " + mNumRows + " mScrollOffsetSecondary "
1591                    + mScrollOffsetSecondary + " mScrollOffsetPrimary " + mScrollOffsetPrimary
1592                    + " inPreLayout " + state.isPreLayout()
1593                    + " didStructureChange " + state.didStructureChange()
1594                    + " mForceFullLayout " + mForceFullLayout);
1595            Log.v(getTag(), "width " + getWidth() + " height " + getHeight());
1596        }
1597
1598        if (mNumRows == 0) {
1599            // haven't done measure yet
1600            return;
1601        }
1602        final int itemCount = state.getItemCount();
1603        if (itemCount < 0) {
1604            return;
1605        }
1606
1607        if (!mLayoutEnabled) {
1608            discardLayoutInfo();
1609            removeAndRecycleAllViews(recycler);
1610            return;
1611        }
1612        mInLayout = true;
1613
1614        final boolean scrollToFocus = !isSmoothScrolling()
1615                && mFocusScrollStrategy == BaseGridView.FOCUS_SCROLL_ALIGNED;
1616        if (mFocusPosition != NO_POSITION && mFocusPositionOffset != Integer.MIN_VALUE) {
1617            mFocusPosition = mFocusPosition + mFocusPositionOffset;
1618            mFocusPositionOffset = 0;
1619        }
1620        saveContext(recycler, state);
1621
1622        // Track the old focus view so we can adjust our system scroll position
1623        // so that any scroll animations happening now will remain valid.
1624        // We must use same delta in Pre Layout (if prelayout exists) and second layout.
1625        // So we cache the deltas in PreLayout and use it in second layout.
1626        int delta = 0, deltaSecondary = 0;
1627        if (mFocusPosition != NO_POSITION && scrollToFocus
1628                && mBaseGridView.getScrollState() != RecyclerView.SCROLL_STATE_IDLE) {
1629            // FIXME: we should get the remaining scroll animation offset from RecyclerView
1630            View focusView = findViewByPosition(mFocusPosition);
1631            if (focusView != null) {
1632                delta = mWindowAlignment.mainAxis().getSystemScrollPos(mScrollOffsetPrimary
1633                        + getViewCenter(focusView), false, false) - mScrollOffsetPrimary;
1634                deltaSecondary = mWindowAlignment.secondAxis().getSystemScrollPos(
1635                        mScrollOffsetSecondary + getViewCenterSecondary(focusView),
1636                        false, false) - mScrollOffsetSecondary;
1637            }
1638        }
1639
1640        boolean hadFocus = mBaseGridView.hasFocus();
1641        int savedFocusPos = mFocusPosition;
1642        if (mInFastRelayout = layoutInit()) {
1643            fastRelayout();
1644            // appends items till focus position.
1645            if (mFocusPosition != NO_POSITION) {
1646                View focusView = findViewByPosition(mFocusPosition);
1647                if (focusView != null) {
1648                    if (scrollToFocus) {
1649                        scrollToView(focusView, false);
1650                    }
1651                    if (hadFocus) {
1652                        focusView.requestFocus();
1653                    }
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