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