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