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