GridLayoutManager.java revision d586ba8825b418d9589436725bfdead30f0dc075
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            // the maxScroll for currently last visible item is larger,
1758            // so we must invalidate the max scroll value.
1759            if (maxScroll > mWindowAlignment.mainAxis().getMaxScroll()) {
1760                mWindowAlignment.mainAxis().invalidateScrollMax();
1761                if (DEBUG) Log.v(getTag(), "Invalidate scrollMax since it should be "
1762                        + "greater than " + maxScroll);
1763            }
1764        }
1765    }
1766
1767    private void updateScrollMin() {
1768        int lowVisiblePos = (!mReverseFlowPrimary) ? mGrid.getFirstVisibleIndex()
1769                : mGrid.getLastVisibleIndex();
1770        int lowMinPos = (!mReverseFlowPrimary) ? 0 : mState.getItemCount() - 1;
1771        if (lowVisiblePos < 0) {
1772            return;
1773        }
1774        final boolean lowAvailable = lowVisiblePos == lowMinPos;
1775        final boolean minUnknown = mWindowAlignment.mainAxis().isMinUnknown();
1776        if (!lowAvailable && minUnknown) {
1777            return;
1778        }
1779        int minEdge = mGrid.findRowMin(false, sTwoInts) + mScrollOffsetPrimary;
1780        int rowIndex = sTwoInts[0];
1781        int pos = sTwoInts[1];
1782        int savedMinEdge = mWindowAlignment.mainAxis().getMinEdge();
1783        mWindowAlignment.mainAxis().setMinEdge(minEdge);
1784        int minScroll = getPrimarySystemScrollPosition(findViewByPosition(pos));
1785        mWindowAlignment.mainAxis().setMinEdge(savedMinEdge);
1786
1787        if (lowAvailable) {
1788            mWindowAlignment.mainAxis().setMinEdge(minEdge);
1789            mWindowAlignment.mainAxis().setMinScroll(minScroll);
1790            if (DEBUG) Log.v(getTag(), "updating scroll minEdge to " + minEdge +
1791                    " scrollMin to " + minScroll);
1792        } else {
1793            // the minScroll for currently first visible item is smaller,
1794            // so we must invalidate the min scroll value.
1795            if (minScroll < mWindowAlignment.mainAxis().getMinScroll()) {
1796                mWindowAlignment.mainAxis().invalidateScrollMin();
1797                if (DEBUG) Log.v(getTag(), "Invalidate scrollMin, since it should be "
1798                        + "less than " + minScroll);
1799            }
1800        }
1801    }
1802
1803    private void updateScrollSecondAxis() {
1804        mWindowAlignment.secondAxis().setMinEdge(0);
1805        mWindowAlignment.secondAxis().setMaxEdge(getSizeSecondary());
1806    }
1807
1808    private void initScrollController() {
1809        mWindowAlignment.reset();
1810        mWindowAlignment.horizontal.setSize(getWidth());
1811        mWindowAlignment.vertical.setSize(getHeight());
1812        mWindowAlignment.horizontal.setPadding(getPaddingLeft(), getPaddingRight());
1813        mWindowAlignment.vertical.setPadding(getPaddingTop(), getPaddingBottom());
1814        mSizePrimary = mWindowAlignment.mainAxis().getSize();
1815        mScrollOffsetPrimary = -mWindowAlignment.mainAxis().getPaddingLow();
1816        mScrollOffsetSecondary = -mWindowAlignment.secondAxis().getPaddingLow();
1817
1818        if (DEBUG) {
1819            Log.v(getTag(), "initScrollController mSizePrimary " + mSizePrimary
1820                    + " mWindowAlignment " + mWindowAlignment
1821                    + " mScrollOffsetPrimary " + mScrollOffsetPrimary);
1822        }
1823    }
1824
1825    private void updateScrollController() {
1826        // mScrollOffsetPrimary and mScrollOffsetSecondary includes the padding.
1827        // e.g. when topPadding is 16 for horizontal grid view,  the initial
1828        // mScrollOffsetSecondary is -16.  fastRelayout() put views based on offsets(not padding),
1829        // when padding changes to 20,  we also need update mScrollOffsetSecondary to -20 before
1830        // fastRelayout() is performed
1831        int paddingPrimaryDiff, paddingSecondaryDiff;
1832        if (mOrientation == HORIZONTAL) {
1833            paddingPrimaryDiff = getPaddingLeft() - mWindowAlignment.horizontal.getPaddingLow();
1834            paddingSecondaryDiff = getPaddingTop() - mWindowAlignment.vertical.getPaddingLow();
1835        } else {
1836            paddingPrimaryDiff = getPaddingTop() - mWindowAlignment.vertical.getPaddingLow();
1837            paddingSecondaryDiff = getPaddingLeft() - mWindowAlignment.horizontal.getPaddingLow();
1838        }
1839        mScrollOffsetPrimary -= paddingPrimaryDiff;
1840        mScrollOffsetSecondary -= paddingSecondaryDiff;
1841
1842        mWindowAlignment.horizontal.setSize(getWidth());
1843        mWindowAlignment.vertical.setSize(getHeight());
1844        mWindowAlignment.horizontal.setPadding(getPaddingLeft(), getPaddingRight());
1845        mWindowAlignment.vertical.setPadding(getPaddingTop(), getPaddingBottom());
1846        mSizePrimary = mWindowAlignment.mainAxis().getSize();
1847
1848        if (DEBUG) {
1849            Log.v(getTag(), "updateScrollController mSizePrimary " + mSizePrimary
1850                    + " mWindowAlignment " + mWindowAlignment
1851                    + " mScrollOffsetPrimary " + mScrollOffsetPrimary);
1852        }
1853    }
1854
1855    public void setSelection(RecyclerView parent, int position) {
1856        setSelection(parent, position, false);
1857    }
1858
1859    public void setSelectionSmooth(RecyclerView parent, int position) {
1860        setSelection(parent, position, true);
1861    }
1862
1863    public int getSelection() {
1864        return mFocusPosition;
1865    }
1866
1867    public void setSelection(RecyclerView parent, int position, boolean smooth) {
1868        if (mFocusPosition != position && position != NO_POSITION) {
1869            scrollToSelection(parent, position, smooth);
1870        }
1871    }
1872
1873    private void scrollToSelection(RecyclerView parent, int position, boolean smooth) {
1874        if (TRACE) TraceHelper.beginSection("scrollToSelection");
1875        View view = findViewByPosition(position);
1876        if (view != null) {
1877            mInSelection = true;
1878            scrollToView(view, smooth);
1879            mInSelection = false;
1880        } else {
1881            mFocusPosition = position;
1882            mFocusPositionOffset = 0;
1883            if (!mLayoutEnabled) {
1884                return;
1885            }
1886            if (smooth) {
1887                if (!hasDoneFirstLayout()) {
1888                    Log.w(getTag(), "setSelectionSmooth should " +
1889                            "not be called before first layout pass");
1890                    return;
1891                }
1892                startPositionSmoothScroller(position);
1893            } else {
1894                mForceFullLayout = true;
1895                parent.requestLayout();
1896            }
1897        }
1898        if (TRACE) TraceHelper.endSection();
1899    }
1900
1901    void startPositionSmoothScroller(int position) {
1902        LinearSmoothScroller linearSmoothScroller =
1903                new GridLinearSmoothScroller() {
1904            @Override
1905            public PointF computeScrollVectorForPosition(int targetPosition) {
1906                if (getChildCount() == 0) {
1907                    return null;
1908                }
1909                final int firstChildPos = getPosition(getChildAt(0));
1910                // TODO We should be able to deduce direction from bounds of current and target
1911                // focus, rather than making assumptions about positions and directionality
1912                final boolean isStart = mReverseFlowPrimary ? targetPosition > firstChildPos
1913                        : targetPosition < firstChildPos;
1914                final int direction = isStart ? -1 : 1;
1915                if (mOrientation == HORIZONTAL) {
1916                    return new PointF(direction, 0);
1917                } else {
1918                    return new PointF(0, direction);
1919                }
1920            }
1921
1922        };
1923        linearSmoothScroller.setTargetPosition(position);
1924        startSmoothScroll(linearSmoothScroller);
1925    }
1926
1927    private void processPendingMovement(boolean forward) {
1928        if (forward ? hasCreatedLastItem() : hasCreatedFirstItem()) {
1929            return;
1930        }
1931        if (mPendingMoveSmoothScroller == null) {
1932            // Stop existing scroller and create a new PendingMoveSmoothScroller.
1933            mBaseGridView.stopScroll();
1934            PendingMoveSmoothScroller linearSmoothScroller = new PendingMoveSmoothScroller(
1935                    forward ? 1 : -1);
1936            mFocusPositionOffset = 0;
1937            startSmoothScroll(linearSmoothScroller);
1938            if (linearSmoothScroller.isRunning()) {
1939                mPendingMoveSmoothScroller = linearSmoothScroller;
1940            }
1941        } else {
1942            if (forward) {
1943                mPendingMoveSmoothScroller.increasePendingMoves();
1944            } else {
1945                mPendingMoveSmoothScroller.decreasePendingMoves();
1946            }
1947        }
1948    }
1949
1950    @Override
1951    public void onItemsAdded(RecyclerView recyclerView, int positionStart, int itemCount) {
1952        if (DEBUG) Log.v(getTag(), "onItemsAdded positionStart "
1953                + positionStart + " itemCount " + itemCount);
1954        if (mFocusPosition != NO_POSITION && mFocusPositionOffset != Integer.MIN_VALUE) {
1955            int pos = mFocusPosition + mFocusPositionOffset;
1956            if (positionStart <= pos) {
1957                mFocusPositionOffset += itemCount;
1958            }
1959        }
1960        mChildrenStates.clear();
1961    }
1962
1963    @Override
1964    public void onItemsChanged(RecyclerView recyclerView) {
1965        if (DEBUG) Log.v(getTag(), "onItemsChanged");
1966        mFocusPositionOffset = 0;
1967        mChildrenStates.clear();
1968    }
1969
1970    @Override
1971    public void onItemsRemoved(RecyclerView recyclerView, int positionStart, int itemCount) {
1972        if (DEBUG) Log.v(getTag(), "onItemsRemoved positionStart "
1973                + positionStart + " itemCount " + itemCount);
1974        if (mFocusPosition != NO_POSITION && mFocusPositionOffset != Integer.MIN_VALUE) {
1975            int pos = mFocusPosition + mFocusPositionOffset;
1976            if (positionStart <= pos) {
1977                if (positionStart + itemCount > pos) {
1978                    // stop updating offset after the focus item was removed
1979                    mFocusPositionOffset = Integer.MIN_VALUE;
1980                } else {
1981                    mFocusPositionOffset -= itemCount;
1982                }
1983            }
1984        }
1985        mChildrenStates.clear();
1986    }
1987
1988    @Override
1989    public void onItemsMoved(RecyclerView recyclerView, int fromPosition, int toPosition,
1990            int itemCount) {
1991        if (DEBUG) Log.v(getTag(), "onItemsMoved fromPosition "
1992                + fromPosition + " toPosition " + toPosition);
1993        if (mFocusPosition != NO_POSITION && mFocusPositionOffset != Integer.MIN_VALUE) {
1994            int pos = mFocusPosition + mFocusPositionOffset;
1995            if (fromPosition <= pos && pos < fromPosition + itemCount) {
1996                // moved items include focused position
1997                mFocusPositionOffset += toPosition - fromPosition;
1998            } else if (fromPosition < pos && toPosition > pos - itemCount) {
1999                // move items before focus position to after focused position
2000                mFocusPositionOffset -= itemCount;
2001            } else if (fromPosition > pos && toPosition < pos) {
2002                // move items after focus position to before focused position
2003                mFocusPositionOffset += itemCount;
2004            }
2005        }
2006        mChildrenStates.clear();
2007    }
2008
2009    @Override
2010    public void onItemsUpdated(RecyclerView recyclerView, int positionStart, int itemCount) {
2011        if (DEBUG) Log.v(getTag(), "onItemsUpdated positionStart "
2012                + positionStart + " itemCount " + itemCount);
2013        for (int i = positionStart, end = positionStart + itemCount; i < end; i++) {
2014            mChildrenStates.remove(i);
2015        }
2016    }
2017
2018    @Override
2019    public boolean onRequestChildFocus(RecyclerView parent, View child, View focused) {
2020        if (mFocusSearchDisabled) {
2021            return true;
2022        }
2023        if (getPositionByView(child) == NO_POSITION) {
2024            // This shouldn't happen, but in case it does be sure not to attempt a
2025            // scroll to a view whose item has been removed.
2026            return true;
2027        }
2028        if (!mInLayout && !mInSelection) {
2029            scrollToView(child, true);
2030        }
2031        return true;
2032    }
2033
2034    @Override
2035    public boolean requestChildRectangleOnScreen(RecyclerView parent, View view, Rect rect,
2036            boolean immediate) {
2037        if (DEBUG) Log.v(getTag(), "requestChildRectangleOnScreen " + view + " " + rect);
2038        return false;
2039    }
2040
2041    int getScrollOffsetX() {
2042        return mOrientation == HORIZONTAL ? mScrollOffsetPrimary : mScrollOffsetSecondary;
2043    }
2044
2045    int getScrollOffsetY() {
2046        return mOrientation == HORIZONTAL ? mScrollOffsetSecondary : mScrollOffsetPrimary;
2047    }
2048
2049    public void getViewSelectedOffsets(View view, int[] offsets) {
2050        if (mOrientation == HORIZONTAL) {
2051            offsets[0] = getPrimarySystemScrollPosition(view) - mScrollOffsetPrimary;
2052            offsets[1] = getSecondarySystemScrollPosition(view) - mScrollOffsetSecondary;
2053        } else {
2054            offsets[1] = getPrimarySystemScrollPosition(view) - mScrollOffsetPrimary;
2055            offsets[0] = getSecondarySystemScrollPosition(view) - mScrollOffsetSecondary;
2056        }
2057    }
2058
2059    private int getPrimarySystemScrollPosition(View view) {
2060        final int viewCenterPrimary = mScrollOffsetPrimary + getViewCenter(view);
2061        final int viewMin = getViewMin(view);
2062        final int viewMax = getViewMax(view);
2063        // TODO: change to use State object in onRequestChildFocus()
2064        boolean isMin, isMax;
2065        if (!mReverseFlowPrimary) {
2066            isMin = mGrid.getFirstVisibleIndex() == 0;
2067            isMax = mGrid.getLastVisibleIndex() == (mState == null ?
2068                    getItemCount() : mState.getItemCount()) - 1;
2069        } else {
2070            isMax = mGrid.getFirstVisibleIndex() == 0;
2071            isMin = mGrid.getLastVisibleIndex() == (mState == null ?
2072                    getItemCount() : mState.getItemCount()) - 1;
2073        }
2074        for (int i = getChildCount() - 1; (isMin || isMax) && i >= 0; i--) {
2075            View v = getChildAt(i);
2076            if (v == view || v == null) {
2077                continue;
2078            }
2079            if (isMin && getViewMin(v) < viewMin) {
2080                isMin = false;
2081            }
2082            if (isMax && getViewMax(v) > viewMax) {
2083                isMax = false;
2084            }
2085        }
2086        return mWindowAlignment.mainAxis().getSystemScrollPos(viewCenterPrimary, isMin, isMax);
2087    }
2088
2089    private int getSecondarySystemScrollPosition(View view) {
2090        int viewCenterSecondary = mScrollOffsetSecondary + getViewCenterSecondary(view);
2091        int pos = getPositionByView(view);
2092        Grid.Location location = mGrid.getLocation(pos);
2093        final int row = location.row;
2094        final boolean isMin, isMax;
2095        if (!mReverseFlowSecondary) {
2096            isMin = row == 0;
2097            isMax = row == mGrid.getNumRows() - 1;
2098        } else {
2099            isMax = row == 0;
2100            isMin = row == mGrid.getNumRows() - 1;
2101        }
2102        return mWindowAlignment.secondAxis().getSystemScrollPos(viewCenterSecondary, isMin, isMax);
2103    }
2104
2105    /**
2106     * Scroll to a given child view and change mFocusPosition.
2107     */
2108    private void scrollToView(View view, boolean smooth) {
2109        int newFocusPosition = getPositionByView(view);
2110        if (newFocusPosition != mFocusPosition) {
2111            mFocusPosition = newFocusPosition;
2112            mFocusPositionOffset = 0;
2113            if (!mInLayout) {
2114                dispatchChildSelected();
2115            }
2116        }
2117        if (mBaseGridView.isChildrenDrawingOrderEnabledInternal()) {
2118            mBaseGridView.invalidate();
2119        }
2120        if (view == null) {
2121            return;
2122        }
2123        if (!view.hasFocus() && mBaseGridView.hasFocus()) {
2124            // transfer focus to the child if it does not have focus yet (e.g. triggered
2125            // by setSelection())
2126            view.requestFocus();
2127        }
2128        if (!mScrollEnabled && smooth) {
2129            return;
2130        }
2131        if (getScrollPosition(view, sTwoInts)) {
2132            scrollGrid(sTwoInts[0], sTwoInts[1], smooth);
2133        }
2134    }
2135
2136    private boolean getScrollPosition(View view, int[] deltas) {
2137        switch (mFocusScrollStrategy) {
2138        case BaseGridView.FOCUS_SCROLL_ALIGNED:
2139        default:
2140            return getAlignedPosition(view, deltas);
2141        case BaseGridView.FOCUS_SCROLL_ITEM:
2142        case BaseGridView.FOCUS_SCROLL_PAGE:
2143            return getNoneAlignedPosition(view, deltas);
2144        }
2145    }
2146
2147    private boolean getNoneAlignedPosition(View view, int[] deltas) {
2148        int pos = getPositionByView(view);
2149        int viewMin = getViewMin(view);
2150        int viewMax = getViewMax(view);
2151        // we either align "firstView" to left/top padding edge
2152        // or align "lastView" to right/bottom padding edge
2153        View firstView = null;
2154        View lastView = null;
2155        int paddingLow = mWindowAlignment.mainAxis().getPaddingLow();
2156        int clientSize = mWindowAlignment.mainAxis().getClientSize();
2157        final int row = mGrid.getRowIndex(pos);
2158        if (viewMin < paddingLow) {
2159            // view enters low padding area:
2160            firstView = view;
2161            if (mFocusScrollStrategy == BaseGridView.FOCUS_SCROLL_PAGE) {
2162                // scroll one "page" left/top,
2163                // align first visible item of the "page" at the low padding edge.
2164                while (prependOneColumnVisibleItems()) {
2165                    CircularIntArray positions =
2166                            mGrid.getItemPositionsInRows(mGrid.getFirstVisibleIndex(), pos)[row];
2167                    firstView = findViewByPosition(positions.get(0));
2168                    if (viewMax - getViewMin(firstView) > clientSize) {
2169                        if (positions.size() > 2) {
2170                            firstView = findViewByPosition(positions.get(2));
2171                        }
2172                        break;
2173                    }
2174                }
2175            }
2176        } else if (viewMax > clientSize + paddingLow) {
2177            // view enters high padding area:
2178            if (mFocusScrollStrategy == BaseGridView.FOCUS_SCROLL_PAGE) {
2179                // scroll whole one page right/bottom, align view at the low padding edge.
2180                firstView = view;
2181                do {
2182                    CircularIntArray positions =
2183                            mGrid.getItemPositionsInRows(pos, mGrid.getLastVisibleIndex())[row];
2184                    lastView = findViewByPosition(positions.get(positions.size() - 1));
2185                    if (getViewMax(lastView) - viewMin > clientSize) {
2186                        lastView = null;
2187                        break;
2188                    }
2189                } while (appendOneColumnVisibleItems());
2190                if (lastView != null) {
2191                    // however if we reached end,  we should align last view.
2192                    firstView = null;
2193                }
2194            } else {
2195                lastView = view;
2196            }
2197        }
2198        int scrollPrimary = 0;
2199        int scrollSecondary = 0;
2200        if (firstView != null) {
2201            scrollPrimary = getViewMin(firstView) - paddingLow;
2202        } else if (lastView != null) {
2203            scrollPrimary = getViewMax(lastView) - (paddingLow + clientSize);
2204        }
2205        View secondaryAlignedView;
2206        if (firstView != null) {
2207            secondaryAlignedView = firstView;
2208        } else if (lastView != null) {
2209            secondaryAlignedView = lastView;
2210        } else {
2211            secondaryAlignedView = view;
2212        }
2213        scrollSecondary = getSecondarySystemScrollPosition(secondaryAlignedView);
2214        scrollSecondary -= mScrollOffsetSecondary;
2215        if (scrollPrimary != 0 || scrollSecondary != 0) {
2216            deltas[0] = scrollPrimary;
2217            deltas[1] = scrollSecondary;
2218            return true;
2219        }
2220        return false;
2221    }
2222
2223    private boolean getAlignedPosition(View view, int[] deltas) {
2224        int scrollPrimary = getPrimarySystemScrollPosition(view);
2225        int scrollSecondary = getSecondarySystemScrollPosition(view);
2226        if (DEBUG) {
2227            Log.v(getTag(), "getAlignedPosition " + scrollPrimary + " " + scrollSecondary
2228                    + " " + mWindowAlignment);
2229            Log.v(getTag(), "getAlignedPosition " + mScrollOffsetPrimary + " " + mScrollOffsetSecondary);
2230        }
2231        scrollPrimary -= mScrollOffsetPrimary;
2232        scrollSecondary -= mScrollOffsetSecondary;
2233        if (scrollPrimary != 0 || scrollSecondary != 0) {
2234            deltas[0] = scrollPrimary;
2235            deltas[1] = scrollSecondary;
2236            return true;
2237        }
2238        return false;
2239    }
2240
2241    private void scrollGrid(int scrollPrimary, int scrollSecondary, boolean smooth) {
2242        if (mInLayout) {
2243            scrollDirectionPrimary(scrollPrimary);
2244            scrollDirectionSecondary(scrollSecondary);
2245        } else {
2246            int scrollX;
2247            int scrollY;
2248            if (mOrientation == HORIZONTAL) {
2249                scrollX = scrollPrimary;
2250                scrollY = scrollSecondary;
2251            } else {
2252                scrollX = scrollSecondary;
2253                scrollY = scrollPrimary;
2254            }
2255            if (smooth) {
2256                mBaseGridView.smoothScrollBy(scrollX, scrollY);
2257            } else {
2258                mBaseGridView.scrollBy(scrollX, scrollY);
2259            }
2260        }
2261    }
2262
2263    public void setPruneChild(boolean pruneChild) {
2264        if (mPruneChild != pruneChild) {
2265            mPruneChild = pruneChild;
2266            if (mPruneChild) {
2267                requestLayout();
2268            }
2269        }
2270    }
2271
2272    public boolean getPruneChild() {
2273        return mPruneChild;
2274    }
2275
2276    public void setScrollEnabled(boolean scrollEnabled) {
2277        if (mScrollEnabled != scrollEnabled) {
2278            mScrollEnabled = scrollEnabled;
2279            if (mScrollEnabled && mFocusScrollStrategy == BaseGridView.FOCUS_SCROLL_ALIGNED
2280                    && mFocusPosition != NO_POSITION) {
2281                scrollToSelection(mBaseGridView, mFocusPosition, true);
2282            }
2283        }
2284    }
2285
2286    public boolean isScrollEnabled() {
2287        return mScrollEnabled;
2288    }
2289
2290    private int findImmediateChildIndex(View view) {
2291        while (view != null && view != mBaseGridView) {
2292            int index = mBaseGridView.indexOfChild(view);
2293            if (index >= 0) {
2294                return index;
2295            }
2296            view = (View) view.getParent();
2297        }
2298        return NO_POSITION;
2299    }
2300
2301    void setFocusSearchDisabled(boolean disabled) {
2302        mFocusSearchDisabled = disabled;
2303    }
2304
2305    boolean isFocusSearchDisabled() {
2306        return mFocusSearchDisabled;
2307    }
2308
2309    @Override
2310    public View onInterceptFocusSearch(View focused, int direction) {
2311        if (mFocusSearchDisabled) {
2312            return focused;
2313        }
2314        return null;
2315    }
2316
2317    boolean hasPreviousViewInSameRow(int pos) {
2318        if (mGrid == null || pos == NO_POSITION || mGrid.getFirstVisibleIndex() < 0) {
2319            return false;
2320        }
2321        if (mGrid.getFirstVisibleIndex() > 0) {
2322            return true;
2323        }
2324        final int focusedRow = mGrid.getLocation(pos).row;
2325        for (int i = getChildCount() - 1; i >= 0; i--) {
2326            int position = getPositionByIndex(i);
2327            Grid.Location loc = mGrid.getLocation(position);
2328            if (loc != null && loc.row == focusedRow) {
2329                if (position < pos) {
2330                    return true;
2331                }
2332            }
2333        }
2334        return false;
2335    }
2336
2337    @Override
2338    public boolean onAddFocusables(RecyclerView recyclerView,
2339            ArrayList<View> views, int direction, int focusableMode) {
2340        if (mFocusSearchDisabled) {
2341            return true;
2342        }
2343        // If this viewgroup or one of its children currently has focus then we
2344        // consider our children for focus searching in main direction on the same row.
2345        // If this viewgroup has no focus and using focus align, we want the system
2346        // to ignore our children and pass focus to the viewgroup, which will pass
2347        // focus on to its children appropriately.
2348        // If this viewgroup has no focus and not using focus align, we want to
2349        // consider the child that does not overlap with padding area.
2350        if (recyclerView.hasFocus()) {
2351            final int movement = getMovement(direction);
2352            if (movement != PREV_ITEM && movement != NEXT_ITEM) {
2353                // Move on secondary direction uses default addFocusables().
2354                return false;
2355            }
2356            if (mPendingMoveSmoothScroller != null) {
2357                // don't find next focusable if has pending movement.
2358                return true;
2359            }
2360            final View focused = recyclerView.findFocus();
2361            final int focusedPos = getPositionByIndex(findImmediateChildIndex(focused));
2362            // Add focusables of focused item.
2363            if (focusedPos != NO_POSITION) {
2364                findViewByPosition(focusedPos).addFocusables(views,  direction, focusableMode);
2365            }
2366            final int focusedRow = mGrid != null && focusedPos != NO_POSITION ?
2367                    mGrid.getLocation(focusedPos).row : NO_POSITION;
2368            // Add focusables of next neighbor of same row on the focus search direction.
2369            if (mGrid != null) {
2370                final int focusableCount = views.size();
2371                for (int i = 0, count = getChildCount(); i < count; i++) {
2372                    int index = movement == NEXT_ITEM ? i : count - 1 - i;
2373                    final View child = getChildAt(index);
2374                    if (child.getVisibility() != View.VISIBLE) {
2375                        continue;
2376                    }
2377                    int position = getPositionByIndex(index);
2378                    Grid.Location loc = mGrid.getLocation(position);
2379                    if (focusedRow == NO_POSITION || (loc != null && loc.row == focusedRow)) {
2380                        if (focusedPos == NO_POSITION ||
2381                                (movement == NEXT_ITEM && position > focusedPos)
2382                                || (movement == PREV_ITEM && position < focusedPos)) {
2383                            child.addFocusables(views,  direction, focusableMode);
2384                            if (views.size() > focusableCount) {
2385                                break;
2386                            }
2387                        }
2388                    }
2389                }
2390            }
2391        } else {
2392            if (mFocusScrollStrategy != BaseGridView.FOCUS_SCROLL_ALIGNED) {
2393                // adding views not overlapping padding area to avoid scrolling in gaining focus
2394                int left = mWindowAlignment.mainAxis().getPaddingLow();
2395                int right = mWindowAlignment.mainAxis().getClientSize() + left;
2396                int focusableCount = views.size();
2397                for (int i = 0, count = getChildCount(); i < count; i++) {
2398                    View child = getChildAt(i);
2399                    if (child.getVisibility() == View.VISIBLE) {
2400                        if (getViewMin(child) >= left && getViewMax(child) <= right) {
2401                            child.addFocusables(views, direction, focusableMode);
2402                        }
2403                    }
2404                }
2405                // if we cannot find any, then just add all children.
2406                if (views.size() == focusableCount) {
2407                    for (int i = 0, count = getChildCount(); i < count; i++) {
2408                        View child = getChildAt(i);
2409                        if (child.getVisibility() == View.VISIBLE) {
2410                            child.addFocusables(views, direction, focusableMode);
2411                        }
2412                    }
2413                    if (views.size() != focusableCount) {
2414                        return true;
2415                    }
2416                } else {
2417                    return true;
2418                }
2419                // if still cannot find any, fall through and add itself
2420            }
2421            if (recyclerView.isFocusable()) {
2422                views.add(recyclerView);
2423            }
2424        }
2425        return true;
2426    }
2427
2428    private boolean hasCreatedLastItem() {
2429        int count = mState.getItemCount();
2430        return count == 0 || findViewByPosition(count - 1) != null;
2431    }
2432
2433    private boolean hasCreatedFirstItem() {
2434        int count = mState.getItemCount();
2435        return count == 0 || findViewByPosition(0) != null;
2436    }
2437
2438    @Override
2439    public View onFocusSearchFailed(View focused, int direction, Recycler recycler,
2440            RecyclerView.State state) {
2441        if (DEBUG) Log.v(getTag(), "onFocusSearchFailed direction " + direction);
2442
2443        View view = null;
2444        int movement = getMovement(direction);
2445        final boolean isScroll = mBaseGridView.getScrollState() != RecyclerView.SCROLL_STATE_IDLE;
2446        // We still treat single-row (or TODO non-staggered) Grid special using position because:
2447        // we know exactly if an item should be selected (based on index) *before* it is
2448        // added to hierarchy. Child view can change layout when it is selected.
2449        // Knowing this ahead can avoid a second layout after view is inserted into tree.
2450        // We can reduce choppiness of vertical scrolling BrowseFragment where row view has
2451        // different layout padding when it is selected.
2452        // Multiple-rows Grid is different case:  we don't know if the item should be selected
2453        // until we add it to hierarchy and measure it.  Grid algorithm choose a row to put
2454        // the item based on the item size.  FocusSearch mechanism will rely which row the item
2455        // is laid out then makes choice whether to select it.  This can cause a second layout
2456        // if selected child has a different layout.
2457        if (mNumRows == 1) {
2458            if (movement == NEXT_ITEM) {
2459                int newPos = mFocusPosition + mNumRows;
2460                if (newPos < getItemCount() && mScrollEnabled && getChildCount() > 0) {
2461                    int lastChildPos = getPosition(getChildAt(getChildCount() - 1));
2462                    if (newPos < lastChildPos + mNumRows * MAX_PENDING_MOVES) {
2463                        setSelectionSmooth(mBaseGridView, newPos);
2464                    }
2465                    view = focused;
2466                } else {
2467                    if (isScroll || !mFocusOutEnd) {
2468                        view = focused;
2469                    }
2470                }
2471            } else if (movement == PREV_ITEM) {
2472                int newPos = mFocusPosition - mNumRows;
2473                if (newPos >= 0 && mScrollEnabled && getChildCount() > 0) {
2474                    int firstChildPos = getPosition(getChildAt(0));
2475                    if (newPos > firstChildPos - mNumRows * MAX_PENDING_MOVES) {
2476                        setSelectionSmooth(mBaseGridView, newPos);
2477                    }
2478                    view = focused;
2479                } else {
2480                    if (isScroll || !mFocusOutFront) {
2481                        view = focused;
2482                    }
2483                }
2484            }
2485        } else if (mNumRows > 1) {
2486            saveContext(recycler, state);
2487            if (movement == NEXT_ITEM) {
2488                if (isScroll || !mFocusOutEnd) {
2489                    view = focused;
2490                }
2491                if (mScrollEnabled) {
2492                    processPendingMovement(true);
2493                }
2494            } else if (movement == PREV_ITEM) {
2495                if (isScroll || !mFocusOutFront) {
2496                    view = focused;
2497                }
2498                if (mScrollEnabled) {
2499                    processPendingMovement(false);
2500                }
2501            }
2502            leaveContext();
2503        }
2504        if (DEBUG) Log.v(getTag(), "returning view " + view);
2505        return view;
2506    }
2507
2508    boolean gridOnRequestFocusInDescendants(RecyclerView recyclerView, int direction,
2509            Rect previouslyFocusedRect) {
2510        switch (mFocusScrollStrategy) {
2511        case BaseGridView.FOCUS_SCROLL_ALIGNED:
2512        default:
2513            return gridOnRequestFocusInDescendantsAligned(recyclerView,
2514                    direction, previouslyFocusedRect);
2515        case BaseGridView.FOCUS_SCROLL_PAGE:
2516        case BaseGridView.FOCUS_SCROLL_ITEM:
2517            return gridOnRequestFocusInDescendantsUnaligned(recyclerView,
2518                    direction, previouslyFocusedRect);
2519        }
2520    }
2521
2522    private boolean gridOnRequestFocusInDescendantsAligned(RecyclerView recyclerView,
2523            int direction, Rect previouslyFocusedRect) {
2524        View view = findViewByPosition(mFocusPosition);
2525        if (view != null) {
2526            boolean result = view.requestFocus(direction, previouslyFocusedRect);
2527            if (!result && DEBUG) {
2528                Log.w(getTag(), "failed to request focus on " + view);
2529            }
2530            return result;
2531        }
2532        return false;
2533    }
2534
2535    private boolean gridOnRequestFocusInDescendantsUnaligned(RecyclerView recyclerView,
2536            int direction, Rect previouslyFocusedRect) {
2537        // focus to view not overlapping padding area to avoid scrolling in gaining focus
2538        int index;
2539        int increment;
2540        int end;
2541        int count = getChildCount();
2542        if ((direction & View.FOCUS_FORWARD) != 0) {
2543            index = 0;
2544            increment = 1;
2545            end = count;
2546        } else {
2547            index = count - 1;
2548            increment = -1;
2549            end = -1;
2550        }
2551        int left = mWindowAlignment.mainAxis().getPaddingLow();
2552        int right = mWindowAlignment.mainAxis().getClientSize() + left;
2553        for (int i = index; i != end; i += increment) {
2554            View child = getChildAt(i);
2555            if (child.getVisibility() == View.VISIBLE) {
2556                if (getViewMin(child) >= left && getViewMax(child) <= right) {
2557                    if (child.requestFocus(direction, previouslyFocusedRect)) {
2558                        return true;
2559                    }
2560                }
2561            }
2562        }
2563        return false;
2564    }
2565
2566    private final static int PREV_ITEM = 0;
2567    private final static int NEXT_ITEM = 1;
2568    private final static int PREV_ROW = 2;
2569    private final static int NEXT_ROW = 3;
2570
2571    private int getMovement(int direction) {
2572        int movement = View.FOCUS_LEFT;
2573
2574        if (mOrientation == HORIZONTAL) {
2575            switch(direction) {
2576                case View.FOCUS_LEFT:
2577                    movement = (!mReverseFlowPrimary) ? PREV_ITEM : NEXT_ITEM;
2578                    break;
2579                case View.FOCUS_RIGHT:
2580                    movement = (!mReverseFlowPrimary) ? NEXT_ITEM : PREV_ITEM;
2581                    break;
2582                case View.FOCUS_UP:
2583                    movement = PREV_ROW;
2584                    break;
2585                case View.FOCUS_DOWN:
2586                    movement = NEXT_ROW;
2587                    break;
2588            }
2589         } else if (mOrientation == VERTICAL) {
2590             switch(direction) {
2591                 case View.FOCUS_LEFT:
2592                     movement = (!mReverseFlowPrimary) ? PREV_ROW : NEXT_ROW;
2593                     break;
2594                 case View.FOCUS_RIGHT:
2595                     movement = (!mReverseFlowPrimary) ? NEXT_ROW : PREV_ROW;
2596                     break;
2597                 case View.FOCUS_UP:
2598                     movement = PREV_ITEM;
2599                     break;
2600                 case View.FOCUS_DOWN:
2601                     movement = NEXT_ITEM;
2602                     break;
2603             }
2604         }
2605
2606        return movement;
2607    }
2608
2609    int getChildDrawingOrder(RecyclerView recyclerView, int childCount, int i) {
2610        View view = findViewByPosition(mFocusPosition);
2611        if (view == null) {
2612            return i;
2613        }
2614        int focusIndex = recyclerView.indexOfChild(view);
2615        // supposely 0 1 2 3 4 5 6 7 8 9, 4 is the center item
2616        // drawing order is 0 1 2 3 9 8 7 6 5 4
2617        if (i < focusIndex) {
2618            return i;
2619        } else if (i < childCount - 1) {
2620            return focusIndex + childCount - 1 - i;
2621        } else {
2622            return focusIndex;
2623        }
2624    }
2625
2626    @Override
2627    public void onAdapterChanged(RecyclerView.Adapter oldAdapter,
2628            RecyclerView.Adapter newAdapter) {
2629        if (DEBUG) Log.v(getTag(), "onAdapterChanged to " + newAdapter);
2630        if (oldAdapter != null) {
2631            discardLayoutInfo();
2632            mFocusPosition = NO_POSITION;
2633            mFocusPositionOffset = 0;
2634            mChildrenStates.clear();
2635        }
2636        super.onAdapterChanged(oldAdapter, newAdapter);
2637    }
2638
2639    private void discardLayoutInfo() {
2640        mGrid = null;
2641        mRowSizeSecondary = null;
2642        mRowSecondarySizeRefresh = false;
2643    }
2644
2645    public void setLayoutEnabled(boolean layoutEnabled) {
2646        if (mLayoutEnabled != layoutEnabled) {
2647            mLayoutEnabled = layoutEnabled;
2648            requestLayout();
2649        }
2650    }
2651
2652    void setChildrenVisibility(int visiblity) {
2653        mChildVisibility = visiblity;
2654        if (mChildVisibility != -1) {
2655            int count = getChildCount();
2656            for (int i= 0; i < count; i++) {
2657                getChildAt(i).setVisibility(mChildVisibility);
2658            }
2659        }
2660    }
2661
2662    final static class SavedState implements Parcelable {
2663
2664        int index; // index inside adapter of the current view
2665        Bundle childStates = Bundle.EMPTY;
2666
2667        @Override
2668        public void writeToParcel(Parcel out, int flags) {
2669            out.writeInt(index);
2670            out.writeBundle(childStates);
2671        }
2672
2673        @SuppressWarnings("hiding")
2674        public static final Parcelable.Creator<SavedState> CREATOR =
2675                new Parcelable.Creator<SavedState>() {
2676                    @Override
2677                    public SavedState createFromParcel(Parcel in) {
2678                        return new SavedState(in);
2679                    }
2680
2681                    @Override
2682                    public SavedState[] newArray(int size) {
2683                        return new SavedState[size];
2684                    }
2685                };
2686
2687        @Override
2688        public int describeContents() {
2689            return 0;
2690        }
2691
2692        SavedState(Parcel in) {
2693            index = in.readInt();
2694            childStates = in.readBundle(GridLayoutManager.class.getClassLoader());
2695        }
2696
2697        SavedState() {
2698        }
2699    }
2700
2701    @Override
2702    public Parcelable onSaveInstanceState() {
2703        if (DEBUG) Log.v(getTag(), "onSaveInstanceState getSelection() " + getSelection());
2704        SavedState ss = new SavedState();
2705        for (int i = 0, count = getChildCount(); i < count; i++) {
2706            View view = getChildAt(i);
2707            int position = getPositionByView(view);
2708            if (position != NO_POSITION) {
2709                mChildrenStates.saveOnScreenView(view, position);
2710            }
2711        }
2712        ss.index = getSelection();
2713        ss.childStates = mChildrenStates.saveAsBundle();
2714        return ss;
2715    }
2716
2717    void onChildDetachedFromWindow(RecyclerView.ViewHolder holder) {
2718     // FIXME: use getAdapterPosition() once RV can return position after view is detached.
2719        final int position = holder.getLayoutPosition();
2720        if (position != NO_POSITION) {
2721            mChildrenStates.saveOffscreenView(holder.itemView, position);
2722        }
2723    }
2724
2725    @Override
2726    public void onRestoreInstanceState(Parcelable state) {
2727        if (!(state instanceof SavedState)) {
2728            return;
2729        }
2730        SavedState loadingState = (SavedState)state;
2731        mFocusPosition = loadingState.index;
2732        mFocusPositionOffset = 0;
2733        mChildrenStates.loadFromBundle(loadingState.childStates);
2734        mForceFullLayout = true;
2735        requestLayout();
2736        if (DEBUG) Log.v(getTag(), "onRestoreInstanceState mFocusPosition " + mFocusPosition);
2737    }
2738}
2739