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