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