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