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