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