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 mutliple 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     private final Runnable mAskFocusRunnable = new Runnable() {
1303         @Override
1304         public void run() {
1305             if (hasFocus()) {
1306                 return;
1307             }
1308             View view = findViewByPosition(mFocusPosition);
1309             if (view != null && view.hasFocusable()) {
1310                 mBaseGridView.focusableViewAvailable(view);
1311                 return;
1312             }
1313             for (int i = 0, count = getChildCount(); i < count; i++) {
1314                 view = getChildAt(i);
1315                 if (view != null && view.hasFocusable()) {
1316                     mBaseGridView.focusableViewAvailable(view);
1317                     break;
1318                 }
1319             }
1320         }
1321     };
1322 
1323     @Override
onMeasure(Recycler recycler, State state, int widthSpec, int heightSpec)1324     public void onMeasure(Recycler recycler, State state, int widthSpec, int heightSpec) {
1325         saveContext(recycler, state);
1326 
1327         int sizePrimary, sizeSecondary, modeSecondary, paddingSecondary;
1328         int measuredSizeSecondary;
1329         if (mOrientation == HORIZONTAL) {
1330             sizePrimary = MeasureSpec.getSize(widthSpec);
1331             sizeSecondary = MeasureSpec.getSize(heightSpec);
1332             modeSecondary = MeasureSpec.getMode(heightSpec);
1333             paddingSecondary = getPaddingTop() + getPaddingBottom();
1334         } else {
1335             sizeSecondary = MeasureSpec.getSize(widthSpec);
1336             sizePrimary = MeasureSpec.getSize(heightSpec);
1337             modeSecondary = MeasureSpec.getMode(widthSpec);
1338             paddingSecondary = getPaddingLeft() + getPaddingRight();
1339         }
1340         if (DEBUG) Log.v(getTag(), "onMeasure widthSpec " + Integer.toHexString(widthSpec) +
1341                 " heightSpec " + Integer.toHexString(heightSpec) +
1342                 " modeSecondary " + Integer.toHexString(modeSecondary) +
1343                 " sizeSecondary " + sizeSecondary + " " + this);
1344 
1345         mMaxSizeSecondary = sizeSecondary;
1346 
1347         if (mRowSizeSecondaryRequested == ViewGroup.LayoutParams.WRAP_CONTENT) {
1348             mNumRows = mNumRowsRequested == 0 ? 1 : mNumRowsRequested;
1349             mFixedRowSizeSecondary = 0;
1350 
1351             if (mRowSizeSecondary == null || mRowSizeSecondary.length != mNumRows) {
1352                 mRowSizeSecondary = new int[mNumRows];
1353             }
1354 
1355             // Measure all current children and update cached row heights
1356             processRowSizeSecondary(true);
1357 
1358             switch (modeSecondary) {
1359             case MeasureSpec.UNSPECIFIED:
1360                 measuredSizeSecondary = getSizeSecondary() + paddingSecondary;
1361                 break;
1362             case MeasureSpec.AT_MOST:
1363                 measuredSizeSecondary = Math.min(getSizeSecondary() + paddingSecondary,
1364                         mMaxSizeSecondary);
1365                 break;
1366             case MeasureSpec.EXACTLY:
1367                 measuredSizeSecondary = mMaxSizeSecondary;
1368                 break;
1369             default:
1370                 throw new IllegalStateException("wrong spec");
1371             }
1372 
1373         } else {
1374             switch (modeSecondary) {
1375             case MeasureSpec.UNSPECIFIED:
1376                 mFixedRowSizeSecondary = mRowSizeSecondaryRequested == 0 ?
1377                         sizeSecondary - paddingSecondary: mRowSizeSecondaryRequested;
1378                 mNumRows = mNumRowsRequested == 0 ? 1 : mNumRowsRequested;
1379                 measuredSizeSecondary = mFixedRowSizeSecondary * mNumRows + mMarginSecondary
1380                     * (mNumRows - 1) + paddingSecondary;
1381                 break;
1382             case MeasureSpec.AT_MOST:
1383             case MeasureSpec.EXACTLY:
1384                 if (mNumRowsRequested == 0 && mRowSizeSecondaryRequested == 0) {
1385                     mNumRows = 1;
1386                     mFixedRowSizeSecondary = sizeSecondary - paddingSecondary;
1387                 } else if (mNumRowsRequested == 0) {
1388                     mFixedRowSizeSecondary = mRowSizeSecondaryRequested;
1389                     mNumRows = (sizeSecondary + mMarginSecondary)
1390                         / (mRowSizeSecondaryRequested + mMarginSecondary);
1391                 } else if (mRowSizeSecondaryRequested == 0) {
1392                     mNumRows = mNumRowsRequested;
1393                     mFixedRowSizeSecondary = (sizeSecondary - paddingSecondary - mMarginSecondary
1394                             * (mNumRows - 1)) / mNumRows;
1395                 } else {
1396                     mNumRows = mNumRowsRequested;
1397                     mFixedRowSizeSecondary = mRowSizeSecondaryRequested;
1398                 }
1399                 measuredSizeSecondary = sizeSecondary;
1400                 if (modeSecondary == MeasureSpec.AT_MOST) {
1401                     int childrenSize = mFixedRowSizeSecondary * mNumRows + mMarginSecondary
1402                         * (mNumRows - 1) + paddingSecondary;
1403                     if (childrenSize < measuredSizeSecondary) {
1404                         measuredSizeSecondary = childrenSize;
1405                     }
1406                 }
1407                 break;
1408             default:
1409                 throw new IllegalStateException("wrong spec");
1410             }
1411         }
1412         if (mOrientation == HORIZONTAL) {
1413             setMeasuredDimension(sizePrimary, measuredSizeSecondary);
1414         } else {
1415             setMeasuredDimension(measuredSizeSecondary, sizePrimary);
1416         }
1417         if (DEBUG) {
1418             Log.v(getTag(), "onMeasure sizePrimary " + sizePrimary +
1419                     " measuredSizeSecondary " + measuredSizeSecondary +
1420                     " mFixedRowSizeSecondary " + mFixedRowSizeSecondary +
1421                     " mNumRows " + mNumRows);
1422         }
1423         leaveContext();
1424     }
1425 
measureChild(View child)1426     private void measureChild(View child) {
1427         if (TRACE) TraceHelper.beginSection("measureChild");
1428         final LayoutParams lp = (LayoutParams) child.getLayoutParams();
1429         calculateItemDecorationsForChild(child, sTempRect);
1430         int widthUsed = lp.leftMargin + lp.rightMargin + sTempRect.left + sTempRect.right;
1431         int heightUsed = lp.topMargin + lp.bottomMargin + sTempRect.top + sTempRect.bottom;
1432 
1433         final int secondarySpec = (mRowSizeSecondaryRequested == ViewGroup.LayoutParams.WRAP_CONTENT) ?
1434                 MeasureSpec.makeMeasureSpec(0, MeasureSpec.UNSPECIFIED) :
1435                 MeasureSpec.makeMeasureSpec(mFixedRowSizeSecondary, MeasureSpec.EXACTLY);
1436         int widthSpec, heightSpec;
1437 
1438         if (mOrientation == HORIZONTAL) {
1439             widthSpec = ViewGroup.getChildMeasureSpec(
1440                     MeasureSpec.makeMeasureSpec(0, MeasureSpec.UNSPECIFIED), widthUsed, lp.width);
1441             heightSpec = ViewGroup.getChildMeasureSpec(secondarySpec, heightUsed, lp.height);
1442         } else {
1443             heightSpec = ViewGroup.getChildMeasureSpec(
1444                     MeasureSpec.makeMeasureSpec(0, MeasureSpec.UNSPECIFIED), heightUsed, lp.height);
1445             widthSpec = ViewGroup.getChildMeasureSpec(secondarySpec, widthUsed, lp.width);
1446         }
1447         child.measure(widthSpec, heightSpec);
1448         if (DEBUG) Log.v(getTag(), "measureChild secondarySpec " + Integer.toHexString(secondarySpec) +
1449                 " widthSpec " + Integer.toHexString(widthSpec) +
1450                 " heightSpec " + Integer.toHexString(heightSpec) +
1451                 " measuredWidth " + child.getMeasuredWidth() +
1452                 " measuredHeight " + child.getMeasuredHeight());
1453         if (DEBUG) Log.v(getTag(), "child lp width " + lp.width + " height " + lp.height);
1454         if (TRACE) TraceHelper.endSection();
1455     }
1456 
1457     /**
1458      * Get facet from the ViewHolder or the viewType.
1459      */
getFacet(RecyclerView.ViewHolder vh, Class<? extends E> facetClass)1460     private <E> E getFacet(RecyclerView.ViewHolder vh, Class<? extends E> facetClass) {
1461         E facet = null;
1462         if (vh instanceof FacetProvider) {
1463             facet = (E) ((FacetProvider) vh).getFacet(facetClass);
1464         }
1465         if (facet == null && mFacetProviderAdapter != null) {
1466             FacetProvider p = mFacetProviderAdapter.getFacetProvider(vh.getItemViewType());
1467             if (p != null) {
1468                 facet = (E) p.getFacet(facetClass);
1469             }
1470         }
1471         return facet;
1472     }
1473 
1474     private Grid.Provider mGridProvider = new Grid.Provider() {
1475 
1476         @Override
1477         public int getCount() {
1478             return mState.getItemCount();
1479         }
1480 
1481         @Override
1482         public int createItem(int index, boolean append, Object[] item) {
1483             if (TRACE) TraceHelper.beginSection("createItem");
1484             if (TRACE) TraceHelper.beginSection("getview");
1485             View v = getViewForPosition(index);
1486             if (TRACE) TraceHelper.endSection();
1487             LayoutParams lp = (LayoutParams) v.getLayoutParams();
1488             RecyclerView.ViewHolder vh = mBaseGridView.getChildViewHolder(v);
1489             lp.setItemAlignmentFacet((ItemAlignmentFacet)getFacet(vh, ItemAlignmentFacet.class));
1490             // See recyclerView docs:  we don't need re-add scraped view if it was removed.
1491             if (!lp.isItemRemoved()) {
1492                 if (TRACE) TraceHelper.beginSection("addView");
1493                 if (append) {
1494                     addView(v);
1495                 } else {
1496                     addView(v, 0);
1497                 }
1498                 if (TRACE) TraceHelper.endSection();
1499                 if (mChildVisibility != -1) {
1500                     v.setVisibility(mChildVisibility);
1501                 }
1502 
1503                 if (mPendingMoveSmoothScroller != null) {
1504                     mPendingMoveSmoothScroller.consumePendingMovesBeforeLayout();
1505                 }
1506                 int subindex = getSubPositionByView(v, v.findFocus());
1507                 if (!mInLayout) {
1508                     // when we are appending item during scroll pass and the item's position
1509                     // matches the mFocusPosition,  we should signal a childSelected event.
1510                     // However if we are still running PendingMoveSmoothScroller,  we defer and
1511                     // signal the event in PendingMoveSmoothScroller.onStop().  This can
1512                     // avoid lots of childSelected events during a long smooth scrolling and
1513                     // increase performance.
1514                     if (index == mFocusPosition && subindex == mSubFocusPosition
1515                             && mPendingMoveSmoothScroller == null) {
1516                         dispatchChildSelected();
1517                     }
1518                 } else if (!mInFastRelayout) {
1519                     // fastRelayout will dispatch event at end of onLayoutChildren().
1520                     // For full layout, two situations here:
1521                     // 1. mInLayoutSearchFocus is false, dispatchChildSelected() at mFocusPosition.
1522                     // 2. mInLayoutSearchFocus is true:  dispatchChildSelected() on first child
1523                     //    equal to or after mFocusPosition that can take focus.
1524                     if (!mInLayoutSearchFocus && index == mFocusPosition
1525                             && subindex == mSubFocusPosition) {
1526                         dispatchChildSelected();
1527                     } else if (mInLayoutSearchFocus && index >= mFocusPosition
1528                             && v.hasFocusable()) {
1529                         mFocusPosition = index;
1530                         mSubFocusPosition = subindex;
1531                         mInLayoutSearchFocus = false;
1532                         dispatchChildSelected();
1533                     }
1534                 }
1535                 measureChild(v);
1536             }
1537             item[0] = v;
1538             return mOrientation == HORIZONTAL ? getDecoratedMeasuredWidthWithMargin(v)
1539                     : getDecoratedMeasuredHeightWithMargin(v);
1540         }
1541 
1542         @Override
1543         public void addItem(Object item, int index, int length, int rowIndex, int edge) {
1544             View v = (View) item;
1545             int start, end;
1546             if (edge == Integer.MIN_VALUE || edge == Integer.MAX_VALUE) {
1547                 edge = !mGrid.isReversedFlow() ? mWindowAlignment.mainAxis().getPaddingLow()
1548                         : mWindowAlignment.mainAxis().getSize()
1549                                 - mWindowAlignment.mainAxis().getPaddingHigh();
1550             }
1551             boolean edgeIsMin = !mGrid.isReversedFlow();
1552             if (edgeIsMin) {
1553                 start = edge;
1554                 end = edge + length;
1555             } else {
1556                 start = edge - length;
1557                 end = edge;
1558             }
1559             int startSecondary = getRowStartSecondary(rowIndex) - mScrollOffsetSecondary;
1560             mChildrenStates.loadView(v, index);
1561             layoutChild(rowIndex, v, start, end, startSecondary);
1562             if (DEBUG) {
1563                 Log.d(getTag(), "addView " + index + " " + v);
1564             }
1565             if (TRACE) TraceHelper.endSection();
1566 
1567             if (index == mGrid.getFirstVisibleIndex()) {
1568                 if (!mGrid.isReversedFlow()) {
1569                     updateScrollMin();
1570                 } else {
1571                     updateScrollMax();
1572                 }
1573             }
1574             if (index == mGrid.getLastVisibleIndex()) {
1575                 if (!mGrid.isReversedFlow()) {
1576                     updateScrollMax();
1577                 } else {
1578                     updateScrollMin();
1579                 }
1580             }
1581             if (!mInLayout && mPendingMoveSmoothScroller != null) {
1582                 mPendingMoveSmoothScroller.consumePendingMovesAfterLayout();
1583             }
1584             if (mChildLaidOutListener != null) {
1585                 RecyclerView.ViewHolder vh = mBaseGridView.getChildViewHolder(v);
1586                 mChildLaidOutListener.onChildLaidOut(mBaseGridView, v, index,
1587                         vh == null ? NO_ID : vh.getItemId());
1588             }
1589         }
1590 
1591         @Override
1592         public void removeItem(int index) {
1593             if (TRACE) TraceHelper.beginSection("removeItem");
1594             View v = findViewByPosition(index);
1595             if (mInLayout) {
1596                 detachAndScrapView(v, mRecycler);
1597             } else {
1598                 removeAndRecycleView(v, mRecycler);
1599             }
1600             if (TRACE) TraceHelper.endSection();
1601         }
1602 
1603         @Override
1604         public int getEdge(int index) {
1605             if (mReverseFlowPrimary) {
1606                 return getViewMax(findViewByPosition(index));
1607             } else {
1608                 return getViewMin(findViewByPosition(index));
1609             }
1610         }
1611 
1612         @Override
1613         public int getSize(int index) {
1614             return getViewPrimarySize(findViewByPosition(index));
1615         }
1616     };
1617 
layoutChild(int rowIndex, View v, int start, int end, int startSecondary)1618     private void layoutChild(int rowIndex, View v, int start, int end, int startSecondary) {
1619         if (TRACE) TraceHelper.beginSection("layoutChild");
1620         int sizeSecondary = mOrientation == HORIZONTAL ? getDecoratedMeasuredHeightWithMargin(v)
1621                 : getDecoratedMeasuredWidthWithMargin(v);
1622         if (mFixedRowSizeSecondary > 0) {
1623             sizeSecondary = Math.min(sizeSecondary, mFixedRowSizeSecondary);
1624         }
1625         final int verticalGravity = mGravity & Gravity.VERTICAL_GRAVITY_MASK;
1626         final int horizontalGravity = (mReverseFlowPrimary || mReverseFlowSecondary) ?
1627                 Gravity.getAbsoluteGravity(mGravity & Gravity.RELATIVE_HORIZONTAL_GRAVITY_MASK, View.LAYOUT_DIRECTION_RTL) :
1628                 mGravity & Gravity.HORIZONTAL_GRAVITY_MASK;
1629         if (mOrientation == HORIZONTAL && verticalGravity == Gravity.TOP
1630                 || mOrientation == VERTICAL && horizontalGravity == Gravity.LEFT) {
1631             // do nothing
1632         } else if (mOrientation == HORIZONTAL && verticalGravity == Gravity.BOTTOM
1633                 || mOrientation == VERTICAL && horizontalGravity == Gravity.RIGHT) {
1634             startSecondary += getRowSizeSecondary(rowIndex) - sizeSecondary;
1635         } else if (mOrientation == HORIZONTAL && verticalGravity == Gravity.CENTER_VERTICAL
1636                 || mOrientation == VERTICAL && horizontalGravity == Gravity.CENTER_HORIZONTAL) {
1637             startSecondary += (getRowSizeSecondary(rowIndex) - sizeSecondary) / 2;
1638         }
1639         int left, top, right, bottom;
1640         if (mOrientation == HORIZONTAL) {
1641             left = start;
1642             top = startSecondary;
1643             right = end;
1644             bottom = startSecondary + sizeSecondary;
1645         } else {
1646             top = start;
1647             left = startSecondary;
1648             bottom = end;
1649             right = startSecondary + sizeSecondary;
1650         }
1651         LayoutParams params = (LayoutParams) v.getLayoutParams();
1652         layoutDecoratedWithMargins(v, left, top, right, bottom);
1653         // Now super.getDecoratedBoundsWithMargins() includes the extra space for optical bounds,
1654         // subtracting it from value passed in layoutDecoratedWithMargins(), we can get the optical
1655         // bounds insets.
1656         super.getDecoratedBoundsWithMargins(v, sTempRect);
1657         params.setOpticalInsets(left - sTempRect.left, top - sTempRect.top,
1658                 sTempRect.right - right, sTempRect.bottom - bottom);
1659         updateChildAlignments(v);
1660         if (TRACE) TraceHelper.endSection();
1661     }
1662 
updateChildAlignments(View v)1663     private void updateChildAlignments(View v) {
1664         final LayoutParams p = (LayoutParams) v.getLayoutParams();
1665         if (p.getItemAlignmentFacet() == null) {
1666             // Fallback to global settings on grid view
1667             p.setAlignX(mItemAlignment.horizontal.getAlignmentPosition(v));
1668             p.setAlignY(mItemAlignment.vertical.getAlignmentPosition(v));
1669         } else {
1670             // Use ItemAlignmentFacet defined on specific ViewHolder
1671             p.calculateItemAlignments(mOrientation, v);
1672             if (mOrientation == HORIZONTAL) {
1673                 p.setAlignY(mItemAlignment.vertical.getAlignmentPosition(v));
1674             } else {
1675                 p.setAlignX(mItemAlignment.horizontal.getAlignmentPosition(v));
1676             }
1677         }
1678     }
1679 
updateChildAlignments()1680     private void updateChildAlignments() {
1681         for (int i = 0, c = getChildCount(); i < c; i++) {
1682             updateChildAlignments(getChildAt(i));
1683         }
1684     }
1685 
setExtraLayoutSpace(int extraLayoutSpace)1686     void setExtraLayoutSpace(int extraLayoutSpace) {
1687         if (mExtraLayoutSpace == extraLayoutSpace) {
1688             return;
1689         } else if (mExtraLayoutSpace < 0) {
1690             throw new IllegalArgumentException("ExtraLayoutSpace must >= 0");
1691         }
1692         mExtraLayoutSpace = extraLayoutSpace;
1693         requestLayout();
1694     }
1695 
getExtraLayoutSpace()1696     int getExtraLayoutSpace() {
1697         return mExtraLayoutSpace;
1698     }
1699 
removeInvisibleViewsAtEnd()1700     private void removeInvisibleViewsAtEnd() {
1701         if (mPruneChild) {
1702             mGrid.removeInvisibleItemsAtEnd(mFocusPosition,
1703                     mReverseFlowPrimary ? -mExtraLayoutSpace : mSizePrimary + mExtraLayoutSpace);
1704         }
1705     }
1706 
removeInvisibleViewsAtFront()1707     private void removeInvisibleViewsAtFront() {
1708         if (mPruneChild) {
1709             mGrid.removeInvisibleItemsAtFront(mFocusPosition,
1710                     mReverseFlowPrimary ? mSizePrimary + mExtraLayoutSpace: -mExtraLayoutSpace);
1711         }
1712     }
1713 
appendOneColumnVisibleItems()1714     private boolean appendOneColumnVisibleItems() {
1715         return mGrid.appendOneColumnVisibleItems();
1716     }
1717 
prependOneColumnVisibleItems()1718     private boolean prependOneColumnVisibleItems() {
1719         return mGrid.prependOneColumnVisibleItems();
1720     }
1721 
appendVisibleItems()1722     private void appendVisibleItems() {
1723         mGrid.appendVisibleItems(mReverseFlowPrimary ? -mExtraLayoutSpace
1724                 : mSizePrimary + mExtraLayoutSpace);
1725     }
1726 
prependVisibleItems()1727     private void prependVisibleItems() {
1728         mGrid.prependVisibleItems(mReverseFlowPrimary ? mSizePrimary + mExtraLayoutSpace
1729                 : -mExtraLayoutSpace);
1730     }
1731 
1732     /**
1733      * Fast layout when there is no structure change, adapter change, etc.
1734      * It will layout all views was layout requested or updated, until hit a view
1735      * with different size,  then it break and detachAndScrap all views after that.
1736      */
fastRelayout()1737     private void fastRelayout() {
1738         boolean invalidateAfter = false;
1739         final int childCount = getChildCount();
1740         int position = -1;
1741         for (int index = 0; index < childCount; index++) {
1742             View view = getChildAt(index);
1743             position = getPositionByIndex(index);
1744             Grid.Location location = mGrid.getLocation(position);
1745             if (location == null) {
1746                 if (DEBUG) Log.w(getTag(), "fastRelayout(): no Location at " + position);
1747                 invalidateAfter = true;
1748                 break;
1749             }
1750 
1751             int startSecondary = getRowStartSecondary(location.row) - mScrollOffsetSecondary;
1752             int primarySize, end;
1753             int start = getViewMin(view);
1754             int oldPrimarySize = getViewPrimarySize(view);
1755 
1756             LayoutParams lp = (LayoutParams) view.getLayoutParams();
1757             if (lp.viewNeedsUpdate()) {
1758                 int viewIndex = mBaseGridView.indexOfChild(view);
1759                 detachAndScrapView(view, mRecycler);
1760                 view = getViewForPosition(position);
1761                 addView(view, viewIndex);
1762             }
1763 
1764             measureChild(view);
1765             if (mOrientation == HORIZONTAL) {
1766                 primarySize = getDecoratedMeasuredWidthWithMargin(view);
1767                 end = start + primarySize;
1768             } else {
1769                 primarySize = getDecoratedMeasuredHeightWithMargin(view);
1770                 end = start + primarySize;
1771             }
1772             layoutChild(location.row, view, start, end, startSecondary);
1773             if (oldPrimarySize != primarySize) {
1774                 // size changed invalidate remaining Locations
1775                 if (DEBUG) Log.d(getTag(), "fastRelayout: view size changed at " + position);
1776                 invalidateAfter = true;
1777                 break;
1778             }
1779         }
1780         if (invalidateAfter) {
1781             final int savedLastPos = mGrid.getLastVisibleIndex();
1782             mGrid.invalidateItemsAfter(position);
1783             if (mPruneChild) {
1784                 // in regular prune child mode, we just append items up to edge limit
1785                 appendVisibleItems();
1786                 if (mFocusPosition >= 0 && mFocusPosition <= savedLastPos) {
1787                     // make sure add focus view back:  the view might be outside edge limit
1788                     // when there is delta in onLayoutChildren().
1789                     while (mGrid.getLastVisibleIndex() < mFocusPosition) {
1790                         mGrid.appendOneColumnVisibleItems();
1791                     }
1792                 }
1793             } else {
1794                 // prune disabled(e.g. in RowsFragment transition): append all removed items
1795                 while (mGrid.appendOneColumnVisibleItems()
1796                         && mGrid.getLastVisibleIndex() < savedLastPos);
1797             }
1798         }
1799         updateScrollMin();
1800         updateScrollMax();
1801         updateScrollSecondAxis();
1802     }
1803 
removeAndRecycleAllViews(RecyclerView.Recycler recycler)1804     public void removeAndRecycleAllViews(RecyclerView.Recycler recycler) {
1805         if (TRACE) TraceHelper.beginSection("removeAndRecycleAllViews");
1806         if (DEBUG) Log.v(TAG, "removeAndRecycleAllViews " + getChildCount());
1807         for (int i = getChildCount() - 1; i >= 0; i--) {
1808             removeAndRecycleViewAt(i, recycler);
1809         }
1810         if (TRACE) TraceHelper.endSection();
1811     }
1812 
1813     // Lays out items based on the current scroll position
1814     @Override
onLayoutChildren(RecyclerView.Recycler recycler, RecyclerView.State state)1815     public void onLayoutChildren(RecyclerView.Recycler recycler, RecyclerView.State state) {
1816         if (DEBUG) {
1817             Log.v(getTag(), "layoutChildren start numRows " + mNumRows + " mScrollOffsetSecondary "
1818                     + mScrollOffsetSecondary + " mScrollOffsetPrimary " + mScrollOffsetPrimary
1819                     + " inPreLayout " + state.isPreLayout()
1820                     + " didStructureChange " + state.didStructureChange()
1821                     + " mForceFullLayout " + mForceFullLayout);
1822             Log.v(getTag(), "width " + getWidth() + " height " + getHeight());
1823         }
1824 
1825         if (mNumRows == 0) {
1826             // haven't done measure yet
1827             return;
1828         }
1829         final int itemCount = state.getItemCount();
1830         if (itemCount < 0) {
1831             return;
1832         }
1833 
1834         if (!mLayoutEnabled) {
1835             discardLayoutInfo();
1836             removeAndRecycleAllViews(recycler);
1837             return;
1838         }
1839         mInLayout = true;
1840 
1841         if (state.didStructureChange()) {
1842             // didStructureChange() == true means attached item has been removed/added.
1843             // scroll animation: we are unable to continue a scroll animation,
1844             //    kill the scroll animation,  and let ItemAnimation move the item to new position.
1845             // position smooth scroller: kill the animation and stop at final position.
1846             // pending smooth scroller: stop and scroll to current focus position.
1847             mBaseGridView.stopScroll();
1848         }
1849         final boolean scrollToFocus = !isSmoothScrolling()
1850                 && mFocusScrollStrategy == BaseGridView.FOCUS_SCROLL_ALIGNED;
1851         if (mFocusPosition != NO_POSITION && mFocusPositionOffset != Integer.MIN_VALUE) {
1852             mFocusPosition = mFocusPosition + mFocusPositionOffset;
1853             mSubFocusPosition = 0;
1854         }
1855         mFocusPositionOffset = 0;
1856         saveContext(recycler, state);
1857 
1858         View savedFocusView = findViewByPosition(mFocusPosition);
1859         int savedFocusPos = mFocusPosition;
1860         int savedSubFocusPos = mSubFocusPosition;
1861         boolean hadFocus = mBaseGridView.hasFocus();
1862 
1863         // Track the old focus view so we can adjust our system scroll position
1864         // so that any scroll animations happening now will remain valid.
1865         // We must use same delta in Pre Layout (if prelayout exists) and second layout.
1866         // So we cache the deltas in PreLayout and use it in second layout.
1867         int delta = 0, deltaSecondary = 0;
1868         if (mFocusPosition != NO_POSITION && scrollToFocus
1869                 && mBaseGridView.getScrollState() != RecyclerView.SCROLL_STATE_IDLE) {
1870             // FIXME: we should get the remaining scroll animation offset from RecyclerView
1871             if (savedFocusView != null) {
1872                 if (getScrollPosition(savedFocusView, savedFocusView.findFocus(), sTwoInts)) {
1873                     delta = sTwoInts[0];
1874                     deltaSecondary = sTwoInts[1];
1875                 }
1876             }
1877         }
1878 
1879         if (mInFastRelayout = layoutInit()) {
1880             fastRelayout();
1881             // appends items till focus position.
1882             if (mFocusPosition != NO_POSITION) {
1883                 View focusView = findViewByPosition(mFocusPosition);
1884                 if (focusView != null) {
1885                     if (scrollToFocus) {
1886                         scrollToView(focusView, false);
1887                     }
1888                     if (hadFocus && !focusView.hasFocus()) {
1889                         focusView.requestFocus();
1890                     }
1891                 }
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                 View focusView = findViewByPosition(mFocusPosition);
1910                 // we need force to initialize the child view's position
1911                 scrollToView(focusView, false);
1912                 if (focusView != null && hadFocus && !focusView.hasFocus()) {
1913                     focusView.requestFocus();
1914                 }
1915                 appendVisibleItems();
1916                 prependVisibleItems();
1917                 removeInvisibleViewsAtFront();
1918                 removeInvisibleViewsAtEnd();
1919             } while (mGrid.getFirstVisibleIndex() != oldFirstVisible ||
1920                     mGrid.getLastVisibleIndex() != oldLastVisible);
1921         }
1922 
1923         if (scrollToFocus) {
1924             scrollDirectionPrimary(-delta);
1925             scrollDirectionSecondary(-deltaSecondary);
1926         }
1927         appendVisibleItems();
1928         prependVisibleItems();
1929         removeInvisibleViewsAtFront();
1930         removeInvisibleViewsAtEnd();
1931 
1932         if (DEBUG) {
1933             StringWriter sw = new StringWriter();
1934             PrintWriter pw = new PrintWriter(sw);
1935             mGrid.debugPrint(pw);
1936             Log.d(getTag(), sw.toString());
1937         }
1938 
1939         if (mRowSecondarySizeRefresh) {
1940             mRowSecondarySizeRefresh = false;
1941         } else {
1942             updateRowSecondarySizeRefresh();
1943         }
1944 
1945         // For fastRelayout, only dispatch event when focus position changes.
1946         if (mInFastRelayout && (mFocusPosition != savedFocusPos || mSubFocusPosition !=
1947                 savedSubFocusPos || findViewByPosition(mFocusPosition) != savedFocusView)) {
1948             dispatchChildSelected();
1949         } else if (!mInFastRelayout && mInLayoutSearchFocus) {
1950             // For full layout we dispatchChildSelected() in createItem() unless searched all
1951             // children and found none is focusable then dispatchChildSelected() here.
1952             dispatchChildSelected();
1953         }
1954 
1955         mInLayout = false;
1956         leaveContext();
1957         if (!hadFocus && !mInFastRelayout && mBaseGridView.hasFocusable()) {
1958             ViewCompat.postOnAnimation(mBaseGridView, mAskFocusRunnable);
1959         }
1960         if (DEBUG) Log.v(getTag(), "layoutChildren end");
1961     }
1962 
offsetChildrenSecondary(int increment)1963     private void offsetChildrenSecondary(int increment) {
1964         final int childCount = getChildCount();
1965         if (mOrientation == HORIZONTAL) {
1966             for (int i = 0; i < childCount; i++) {
1967                 getChildAt(i).offsetTopAndBottom(increment);
1968             }
1969         } else {
1970             for (int i = 0; i < childCount; i++) {
1971                 getChildAt(i).offsetLeftAndRight(increment);
1972             }
1973         }
1974     }
1975 
offsetChildrenPrimary(int increment)1976     private void offsetChildrenPrimary(int increment) {
1977         final int childCount = getChildCount();
1978         if (mOrientation == VERTICAL) {
1979             for (int i = 0; i < childCount; i++) {
1980                 getChildAt(i).offsetTopAndBottom(increment);
1981             }
1982         } else {
1983             for (int i = 0; i < childCount; i++) {
1984                 getChildAt(i).offsetLeftAndRight(increment);
1985             }
1986         }
1987     }
1988 
1989     @Override
scrollHorizontallyBy(int dx, Recycler recycler, RecyclerView.State state)1990     public int scrollHorizontallyBy(int dx, Recycler recycler, RecyclerView.State state) {
1991         if (DEBUG) Log.v(getTag(), "scrollHorizontallyBy " + dx);
1992         if (!mLayoutEnabled || !hasDoneFirstLayout()) {
1993             return 0;
1994         }
1995         saveContext(recycler, state);
1996         mInScroll = true;
1997         int result;
1998         if (mOrientation == HORIZONTAL) {
1999             result = scrollDirectionPrimary(dx);
2000         } else {
2001             result = scrollDirectionSecondary(dx);
2002         }
2003         leaveContext();
2004         mInScroll = false;
2005         return result;
2006     }
2007 
2008     @Override
scrollVerticallyBy(int dy, Recycler recycler, RecyclerView.State state)2009     public int scrollVerticallyBy(int dy, Recycler recycler, RecyclerView.State state) {
2010         if (DEBUG) Log.v(getTag(), "scrollVerticallyBy " + dy);
2011         if (!mLayoutEnabled || !hasDoneFirstLayout()) {
2012             return 0;
2013         }
2014         mInScroll = true;
2015         saveContext(recycler, state);
2016         int result;
2017         if (mOrientation == VERTICAL) {
2018             result = scrollDirectionPrimary(dy);
2019         } else {
2020             result = scrollDirectionSecondary(dy);
2021         }
2022         leaveContext();
2023         mInScroll = false;
2024         return result;
2025     }
2026 
2027     // scroll in main direction may add/prune views
scrollDirectionPrimary(int da)2028     private int scrollDirectionPrimary(int da) {
2029         if (TRACE) TraceHelper.beginSection("scrollPrimary");
2030         boolean isMaxUnknown = false, isMinUnknown = false;
2031         int minScroll = 0, maxScroll = 0;
2032         if (da > 0) {
2033             isMaxUnknown = mWindowAlignment.mainAxis().isMaxUnknown();
2034             if (!isMaxUnknown) {
2035                 maxScroll = mWindowAlignment.mainAxis().getMaxScroll();
2036                 if (mScrollOffsetPrimary + da > maxScroll) {
2037                     da = maxScroll - mScrollOffsetPrimary;
2038                 }
2039             }
2040         } else if (da < 0) {
2041             isMinUnknown = mWindowAlignment.mainAxis().isMinUnknown();
2042             if (!isMinUnknown) {
2043                 minScroll = mWindowAlignment.mainAxis().getMinScroll();
2044                 if (mScrollOffsetPrimary + da < minScroll) {
2045                     da = minScroll - mScrollOffsetPrimary;
2046                 }
2047             }
2048         }
2049         if (da == 0) {
2050             if (TRACE) TraceHelper.endSection();
2051             return 0;
2052         }
2053         offsetChildrenPrimary(-da);
2054         mScrollOffsetPrimary += da;
2055         if (mInLayout) {
2056             if (TRACE) TraceHelper.endSection();
2057             return da;
2058         }
2059 
2060         int childCount = getChildCount();
2061         boolean updated;
2062 
2063         if (mReverseFlowPrimary ? da > 0 : da < 0) {
2064             prependVisibleItems();
2065         } else {
2066             appendVisibleItems();
2067         }
2068         updated = getChildCount() > childCount;
2069         childCount = getChildCount();
2070 
2071         if (TRACE) TraceHelper.beginSection("remove");
2072         if (mReverseFlowPrimary ? da > 0 : da < 0) {
2073             removeInvisibleViewsAtEnd();
2074         } else {
2075             removeInvisibleViewsAtFront();
2076         }
2077         if (TRACE) TraceHelper.endSection();
2078         updated |= getChildCount() < childCount;
2079         if (updated) {
2080             updateRowSecondarySizeRefresh();
2081         }
2082 
2083         mBaseGridView.invalidate();
2084         if (TRACE) TraceHelper.endSection();
2085         return da;
2086     }
2087 
2088     // scroll in second direction will not add/prune views
2089     private int scrollDirectionSecondary(int dy) {
2090         if (dy == 0) {
2091             return 0;
2092         }
2093         offsetChildrenSecondary(-dy);
2094         mScrollOffsetSecondary += dy;
2095         mBaseGridView.invalidate();
2096         return dy;
2097     }
2098 
2099     private void updateScrollMax() {
2100         int highVisiblePos = (!mReverseFlowPrimary) ? mGrid.getLastVisibleIndex()
2101                 : mGrid.getFirstVisibleIndex();
2102         int highMaxPos = (!mReverseFlowPrimary) ? mState.getItemCount() - 1 : 0;
2103         if (highVisiblePos < 0) {
2104             return;
2105         }
2106         final boolean highAvailable = highVisiblePos == highMaxPos;
2107         final boolean maxUnknown = mWindowAlignment.mainAxis().isMaxUnknown();
2108         if (!highAvailable && maxUnknown) {
2109             return;
2110         }
2111         int maxEdge = mGrid.findRowMax(true, sTwoInts) + mScrollOffsetPrimary;
2112         int rowIndex = sTwoInts[0];
2113         int pos = sTwoInts[1];
2114         int savedMaxEdge = mWindowAlignment.mainAxis().getMaxEdge();
2115         mWindowAlignment.mainAxis().setMaxEdge(maxEdge);
2116         int maxScroll = getPrimarySystemScrollPositionOfChildMax(findViewByPosition(pos));
2117         mWindowAlignment.mainAxis().setMaxEdge(savedMaxEdge);
2118 
2119         if (highAvailable) {
2120             mWindowAlignment.mainAxis().setMaxEdge(maxEdge);
2121             mWindowAlignment.mainAxis().setMaxScroll(maxScroll);
2122             if (DEBUG) Log.v(getTag(), "updating scroll maxEdge to " + maxEdge +
2123                     " scrollMax to " + maxScroll);
2124         } else {
2125             mWindowAlignment.mainAxis().invalidateScrollMax();
2126             if (DEBUG) Log.v(getTag(), "Invalidate scrollMax since it should be "
2127                     + "greater than " + maxScroll);
2128         }
2129     }
2130 
2131     private void updateScrollMin() {
2132         int lowVisiblePos = (!mReverseFlowPrimary) ? mGrid.getFirstVisibleIndex()
2133                 : mGrid.getLastVisibleIndex();
2134         int lowMinPos = (!mReverseFlowPrimary) ? 0 : mState.getItemCount() - 1;
2135         if (lowVisiblePos < 0) {
2136             return;
2137         }
2138         final boolean lowAvailable = lowVisiblePos == lowMinPos;
2139         final boolean minUnknown = mWindowAlignment.mainAxis().isMinUnknown();
2140         if (!lowAvailable && minUnknown) {
2141             return;
2142         }
2143         int minEdge = mGrid.findRowMin(false, sTwoInts) + mScrollOffsetPrimary;
2144         int rowIndex = sTwoInts[0];
2145         int pos = sTwoInts[1];
2146         int savedMinEdge = mWindowAlignment.mainAxis().getMinEdge();
2147         mWindowAlignment.mainAxis().setMinEdge(minEdge);
2148         int minScroll = getPrimarySystemScrollPosition(findViewByPosition(pos));
2149         mWindowAlignment.mainAxis().setMinEdge(savedMinEdge);
2150 
2151         if (lowAvailable) {
2152             mWindowAlignment.mainAxis().setMinEdge(minEdge);
2153             mWindowAlignment.mainAxis().setMinScroll(minScroll);
2154             if (DEBUG) Log.v(getTag(), "updating scroll minEdge to " + minEdge +
2155                     " scrollMin to " + minScroll);
2156         } else {
2157             mWindowAlignment.mainAxis().invalidateScrollMin();
2158             if (DEBUG) Log.v(getTag(), "Invalidate scrollMin, since it should be "
2159                     + "less than " + minScroll);
2160         }
2161     }
2162 
2163     private void updateScrollSecondAxis() {
2164         mWindowAlignment.secondAxis().setMinEdge(0);
2165         mWindowAlignment.secondAxis().setMaxEdge(getSizeSecondary());
2166     }
2167 
2168     private void initScrollController() {
2169         mWindowAlignment.reset();
2170         mWindowAlignment.horizontal.setSize(getWidth());
2171         mWindowAlignment.vertical.setSize(getHeight());
2172         mWindowAlignment.horizontal.setPadding(getPaddingLeft(), getPaddingRight());
2173         mWindowAlignment.vertical.setPadding(getPaddingTop(), getPaddingBottom());
2174         mSizePrimary = mWindowAlignment.mainAxis().getSize();
2175         mScrollOffsetPrimary = -mWindowAlignment.mainAxis().getPaddingLow();
2176         mScrollOffsetSecondary = -mWindowAlignment.secondAxis().getPaddingLow();
2177 
2178         if (DEBUG) {
2179             Log.v(getTag(), "initScrollController mSizePrimary " + mSizePrimary
2180                     + " mWindowAlignment " + mWindowAlignment
2181                     + " mScrollOffsetPrimary " + mScrollOffsetPrimary);
2182         }
2183     }
2184 
2185     private void updateScrollController() {
2186         // mScrollOffsetPrimary and mScrollOffsetSecondary includes the padding.
2187         // e.g. when topPadding is 16 for horizontal grid view,  the initial
2188         // mScrollOffsetSecondary is -16.  fastRelayout() put views based on offsets(not padding),
2189         // when padding changes to 20,  we also need update mScrollOffsetSecondary to -20 before
2190         // fastRelayout() is performed
2191         int paddingPrimaryDiff, paddingSecondaryDiff;
2192         if (mOrientation == HORIZONTAL) {
2193             paddingPrimaryDiff = getPaddingLeft() - mWindowAlignment.horizontal.getPaddingLow();
2194             paddingSecondaryDiff = getPaddingTop() - mWindowAlignment.vertical.getPaddingLow();
2195         } else {
2196             paddingPrimaryDiff = getPaddingTop() - mWindowAlignment.vertical.getPaddingLow();
2197             paddingSecondaryDiff = getPaddingLeft() - mWindowAlignment.horizontal.getPaddingLow();
2198         }
2199         mScrollOffsetPrimary -= paddingPrimaryDiff;
2200         mScrollOffsetSecondary -= paddingSecondaryDiff;
2201 
2202         mWindowAlignment.horizontal.setSize(getWidth());
2203         mWindowAlignment.vertical.setSize(getHeight());
2204         mWindowAlignment.horizontal.setPadding(getPaddingLeft(), getPaddingRight());
2205         mWindowAlignment.vertical.setPadding(getPaddingTop(), getPaddingBottom());
2206         mSizePrimary = mWindowAlignment.mainAxis().getSize();
2207 
2208         if (DEBUG) {
2209             Log.v(getTag(), "updateScrollController mSizePrimary " + mSizePrimary
2210                     + " mWindowAlignment " + mWindowAlignment
2211                     + " mScrollOffsetPrimary " + mScrollOffsetPrimary);
2212         }
2213     }
2214 
2215     @Override
2216     public void scrollToPosition(int position) {
2217         setSelection(position, 0, false, 0);
2218     }
2219 
2220     public void setSelection(int position,
2221             int primaryScrollExtra) {
2222         setSelection(position, 0, false, primaryScrollExtra);
2223     }
2224 
2225     public void setSelectionSmooth(int position) {
2226         setSelection(position, 0, true, 0);
2227     }
2228 
2229     public void setSelectionWithSub(int position, int subposition,
2230             int primaryScrollExtra) {
2231         setSelection(position, subposition, false, primaryScrollExtra);
2232     }
2233 
2234     public void setSelectionSmoothWithSub(int position, int subposition) {
2235         setSelection(position, subposition, true, 0);
2236     }
2237 
2238     public int getSelection() {
2239         return mFocusPosition;
2240     }
2241 
2242     public int getSubSelection() {
2243         return mSubFocusPosition;
2244     }
2245 
2246     public void setSelection(int position, int subposition, boolean smooth,
2247             int primaryScrollExtra) {
2248         if (mFocusPosition != position && position != NO_POSITION
2249                 || subposition != mSubFocusPosition || primaryScrollExtra != mPrimaryScrollExtra) {
2250             scrollToSelection(position, subposition, smooth, primaryScrollExtra);
2251         }
2252     }
2253 
2254     private void scrollToSelection(int position, int subposition,
2255             boolean smooth, int primaryScrollExtra) {
2256         if (TRACE) TraceHelper.beginSection("scrollToSelection");
2257         mPrimaryScrollExtra = primaryScrollExtra;
2258         View view = findViewByPosition(position);
2259         if (view != null) {
2260             mInSelection = true;
2261             scrollToView(view, smooth);
2262             mInSelection = false;
2263         } else {
2264             mFocusPosition = position;
2265             mSubFocusPosition = subposition;
2266             mFocusPositionOffset = Integer.MIN_VALUE;
2267             if (!mLayoutEnabled) {
2268                 return;
2269             }
2270             if (smooth) {
2271                 if (!hasDoneFirstLayout()) {
2272                     Log.w(getTag(), "setSelectionSmooth should " +
2273                             "not be called before first layout pass");
2274                     return;
2275                 }
2276                 startPositionSmoothScroller(position);
2277             } else {
2278                 mForceFullLayout = true;
2279                 requestLayout();
2280             }
2281         }
2282         if (TRACE) TraceHelper.endSection();
2283     }
2284 
2285     void startPositionSmoothScroller(int position) {
2286         LinearSmoothScroller linearSmoothScroller = new GridLinearSmoothScroller() {
2287             @Override
2288             public PointF computeScrollVectorForPosition(int targetPosition) {
2289                 if (getChildCount() == 0) {
2290                     return null;
2291                 }
2292                 final int firstChildPos = getPosition(getChildAt(0));
2293                 // TODO We should be able to deduce direction from bounds of current and target
2294                 // focus, rather than making assumptions about positions and directionality
2295                 final boolean isStart = mReverseFlowPrimary ? targetPosition > firstChildPos
2296                         : targetPosition < firstChildPos;
2297                 final int direction = isStart ? -1 : 1;
2298                 if (mOrientation == HORIZONTAL) {
2299                     return new PointF(direction, 0);
2300                 } else {
2301                     return new PointF(0, direction);
2302                 }
2303             }
2304 
2305         };
2306         linearSmoothScroller.setTargetPosition(position);
2307         startSmoothScroll(linearSmoothScroller);
2308     }
2309 
2310     private void processPendingMovement(boolean forward) {
2311         if (forward ? hasCreatedLastItem() : hasCreatedFirstItem()) {
2312             return;
2313         }
2314         if (mPendingMoveSmoothScroller == null) {
2315             // Stop existing scroller and create a new PendingMoveSmoothScroller.
2316             mBaseGridView.stopScroll();
2317             PendingMoveSmoothScroller linearSmoothScroller = new PendingMoveSmoothScroller(
2318                     forward ? 1 : -1, mNumRows > 1);
2319             mFocusPositionOffset = 0;
2320             startSmoothScroll(linearSmoothScroller);
2321             if (linearSmoothScroller.isRunning()) {
2322                 mPendingMoveSmoothScroller = linearSmoothScroller;
2323             }
2324         } else {
2325             if (forward) {
2326                 mPendingMoveSmoothScroller.increasePendingMoves();
2327             } else {
2328                 mPendingMoveSmoothScroller.decreasePendingMoves();
2329             }
2330         }
2331     }
2332 
2333     @Override
2334     public void onItemsAdded(RecyclerView recyclerView, int positionStart, int itemCount) {
2335         if (DEBUG) Log.v(getTag(), "onItemsAdded positionStart "
2336                 + positionStart + " itemCount " + itemCount);
2337         if (mFocusPosition != NO_POSITION && mGrid != null && mGrid.getFirstVisibleIndex() >= 0
2338                 && mFocusPositionOffset != Integer.MIN_VALUE) {
2339             int pos = mFocusPosition + mFocusPositionOffset;
2340             if (positionStart <= pos) {
2341                 mFocusPositionOffset += itemCount;
2342             }
2343         }
2344         mChildrenStates.clear();
2345     }
2346 
2347     @Override
2348     public void onItemsChanged(RecyclerView recyclerView) {
2349         if (DEBUG) Log.v(getTag(), "onItemsChanged");
2350         mFocusPositionOffset = 0;
2351         mChildrenStates.clear();
2352     }
2353 
2354     @Override
2355     public void onItemsRemoved(RecyclerView recyclerView, int positionStart, int itemCount) {
2356         if (DEBUG) Log.v(getTag(), "onItemsRemoved positionStart "
2357                 + positionStart + " itemCount " + itemCount);
2358         if (mFocusPosition != NO_POSITION  && mGrid != null && mGrid.getFirstVisibleIndex() >= 0
2359             && mFocusPositionOffset != Integer.MIN_VALUE) {
2360             int pos = mFocusPosition + mFocusPositionOffset;
2361             if (positionStart <= pos) {
2362                 if (positionStart + itemCount > pos) {
2363                     // stop updating offset after the focus item was removed
2364                     mFocusPositionOffset = Integer.MIN_VALUE;
2365                 } else {
2366                     mFocusPositionOffset -= itemCount;
2367                 }
2368             }
2369         }
2370         mChildrenStates.clear();
2371     }
2372 
2373     @Override
2374     public void onItemsMoved(RecyclerView recyclerView, int fromPosition, int toPosition,
2375             int itemCount) {
2376         if (DEBUG) Log.v(getTag(), "onItemsMoved fromPosition "
2377                 + fromPosition + " toPosition " + toPosition);
2378         if (mFocusPosition != NO_POSITION && mFocusPositionOffset != Integer.MIN_VALUE) {
2379             int pos = mFocusPosition + mFocusPositionOffset;
2380             if (fromPosition <= pos && pos < fromPosition + itemCount) {
2381                 // moved items include focused position
2382                 mFocusPositionOffset += toPosition - fromPosition;
2383             } else if (fromPosition < pos && toPosition > pos - itemCount) {
2384                 // move items before focus position to after focused position
2385                 mFocusPositionOffset -= itemCount;
2386             } else if (fromPosition > pos && toPosition < pos) {
2387                 // move items after focus position to before focused position
2388                 mFocusPositionOffset += itemCount;
2389             }
2390         }
2391         mChildrenStates.clear();
2392     }
2393 
2394     @Override
2395     public void onItemsUpdated(RecyclerView recyclerView, int positionStart, int itemCount) {
2396         if (DEBUG) Log.v(getTag(), "onItemsUpdated positionStart "
2397                 + positionStart + " itemCount " + itemCount);
2398         for (int i = positionStart, end = positionStart + itemCount; i < end; i++) {
2399             mChildrenStates.remove(i);
2400         }
2401     }
2402 
2403     @Override
2404     public boolean onRequestChildFocus(RecyclerView parent, View child, View focused) {
2405         if (mFocusSearchDisabled) {
2406             return true;
2407         }
2408         if (getPositionByView(child) == NO_POSITION) {
2409             // This shouldn't happen, but in case it does be sure not to attempt a
2410             // scroll to a view whose item has been removed.
2411             return true;
2412         }
2413         if (!mInLayout && !mInSelection && !mInScroll) {
2414             scrollToView(child, focused, true);
2415         }
2416         return true;
2417     }
2418 
2419     @Override
2420     public boolean requestChildRectangleOnScreen(RecyclerView parent, View view, Rect rect,
2421             boolean immediate) {
2422         if (DEBUG) Log.v(getTag(), "requestChildRectangleOnScreen " + view + " " + rect);
2423         return false;
2424     }
2425 
2426     int getScrollOffsetX() {
2427         return mOrientation == HORIZONTAL ? mScrollOffsetPrimary : mScrollOffsetSecondary;
2428     }
2429 
2430     int getScrollOffsetY() {
2431         return mOrientation == HORIZONTAL ? mScrollOffsetSecondary : mScrollOffsetPrimary;
2432     }
2433 
2434     public void getViewSelectedOffsets(View view, int[] offsets) {
2435         if (mOrientation == HORIZONTAL) {
2436             offsets[0] = getPrimarySystemScrollPosition(view) - mScrollOffsetPrimary;
2437             offsets[1] = getSecondarySystemScrollPosition(view) - mScrollOffsetSecondary;
2438         } else {
2439             offsets[1] = getPrimarySystemScrollPosition(view) - mScrollOffsetPrimary;
2440             offsets[0] = getSecondarySystemScrollPosition(view) - mScrollOffsetSecondary;
2441         }
2442     }
2443 
2444     private int getPrimarySystemScrollPosition(View view) {
2445         final int viewCenterPrimary = mScrollOffsetPrimary + getViewCenter(view);
2446         final int viewMin = getViewMin(view);
2447         final int viewMax = getViewMax(view);
2448         // TODO: change to use State object in onRequestChildFocus()
2449         boolean isMin, isMax;
2450         if (!mReverseFlowPrimary) {
2451             isMin = mGrid.getFirstVisibleIndex() == 0;
2452             isMax = mGrid.getLastVisibleIndex() == (mState == null ?
2453                     getItemCount() : mState.getItemCount()) - 1;
2454         } else {
2455             isMax = mGrid.getFirstVisibleIndex() == 0;
2456             isMin = mGrid.getLastVisibleIndex() == (mState == null ?
2457                     getItemCount() : mState.getItemCount()) - 1;
2458         }
2459         for (int i = getChildCount() - 1; (isMin || isMax) && i >= 0; i--) {
2460             View v = getChildAt(i);
2461             if (v == view || v == null) {
2462                 continue;
2463             }
2464             if (isMin && getViewMin(v) < viewMin) {
2465                 isMin = false;
2466             }
2467             if (isMax && getViewMax(v) > viewMax) {
2468                 isMax = false;
2469             }
2470         }
2471         return mWindowAlignment.mainAxis().getSystemScrollPos(viewCenterPrimary, isMin, isMax);
2472     }
2473 
2474     private int getPrimarySystemScrollPositionOfChildMax(View view) {
2475         int scrollPosition = getPrimarySystemScrollPosition(view);
2476         final LayoutParams lp = (LayoutParams) view.getLayoutParams();
2477         int[] multipleAligns = lp.getAlignMultiple();
2478         if (multipleAligns != null && multipleAligns.length > 0) {
2479             scrollPosition += multipleAligns[multipleAligns.length - 1] - multipleAligns[0];
2480         }
2481         return scrollPosition;
2482     }
2483 
2484     /**
2485      * Get adjusted primary position for a given childView (if there is multiple ItemAlignment defined
2486      * on the view).
2487      */
2488     private int getAdjustedPrimaryScrollPosition(int scrollPrimary, View view, View childView) {
2489         int subindex = getSubPositionByView(view, childView);
2490         if (subindex != 0) {
2491             final LayoutParams lp = (LayoutParams) view.getLayoutParams();
2492             scrollPrimary += lp.getAlignMultiple()[subindex] - lp.getAlignMultiple()[0];
2493         }
2494         return scrollPrimary;
2495     }
2496 
2497     private int getSecondarySystemScrollPosition(View view) {
2498         int viewCenterSecondary = mScrollOffsetSecondary + getViewCenterSecondary(view);
2499         int pos = getPositionByView(view);
2500         Grid.Location location = mGrid.getLocation(pos);
2501         final int row = location.row;
2502         final boolean isMin, isMax;
2503         if (!mReverseFlowSecondary) {
2504             isMin = row == 0;
2505             isMax = row == mGrid.getNumRows() - 1;
2506         } else {
2507             isMax = row == 0;
2508             isMin = row == mGrid.getNumRows() - 1;
2509         }
2510         return mWindowAlignment.secondAxis().getSystemScrollPos(viewCenterSecondary, isMin, isMax);
2511     }
2512 
2513     /**
2514      * Scroll to a given child view and change mFocusPosition.
2515      */
2516     private void scrollToView(View view, boolean smooth) {
2517         scrollToView(view, view == null ? null : view.findFocus(), smooth);
2518     }
2519 
2520     /**
2521      * Scroll to a given child view and change mFocusPosition.
2522      */
2523     private void scrollToView(View view, View childView, boolean smooth) {
2524         int newFocusPosition = getPositionByView(view);
2525         int newSubFocusPosition = getSubPositionByView(view, childView);
2526         if (newFocusPosition != mFocusPosition || newSubFocusPosition != mSubFocusPosition) {
2527             mFocusPosition = newFocusPosition;
2528             mSubFocusPosition = newSubFocusPosition;
2529             mFocusPositionOffset = 0;
2530             if (!mInLayout) {
2531                 dispatchChildSelected();
2532             }
2533             if (mBaseGridView.isChildrenDrawingOrderEnabledInternal()) {
2534                 mBaseGridView.invalidate();
2535             }
2536         }
2537         if (view == null) {
2538             return;
2539         }
2540         if (!view.hasFocus() && mBaseGridView.hasFocus()) {
2541             // transfer focus to the child if it does not have focus yet (e.g. triggered
2542             // by setSelection())
2543             view.requestFocus();
2544         }
2545         if (!mScrollEnabled && smooth) {
2546             return;
2547         }
2548         if (getScrollPosition(view, childView, sTwoInts)) {
2549             scrollGrid(sTwoInts[0], sTwoInts[1], smooth);
2550         }
2551     }
2552 
2553     private boolean getScrollPosition(View view, View childView, int[] deltas) {
2554         switch (mFocusScrollStrategy) {
2555         case BaseGridView.FOCUS_SCROLL_ALIGNED:
2556         default:
2557             return getAlignedPosition(view, childView, deltas);
2558         case BaseGridView.FOCUS_SCROLL_ITEM:
2559         case BaseGridView.FOCUS_SCROLL_PAGE:
2560             return getNoneAlignedPosition(view, deltas);
2561         }
2562     }
2563 
2564     private boolean getNoneAlignedPosition(View view, int[] deltas) {
2565         int pos = getPositionByView(view);
2566         int viewMin = getViewMin(view);
2567         int viewMax = getViewMax(view);
2568         // we either align "firstView" to left/top padding edge
2569         // or align "lastView" to right/bottom padding edge
2570         View firstView = null;
2571         View lastView = null;
2572         int paddingLow = mWindowAlignment.mainAxis().getPaddingLow();
2573         int clientSize = mWindowAlignment.mainAxis().getClientSize();
2574         final int row = mGrid.getRowIndex(pos);
2575         if (viewMin < paddingLow) {
2576             // view enters low padding area:
2577             firstView = view;
2578             if (mFocusScrollStrategy == BaseGridView.FOCUS_SCROLL_PAGE) {
2579                 // scroll one "page" left/top,
2580                 // align first visible item of the "page" at the low padding edge.
2581                 while (prependOneColumnVisibleItems()) {
2582                     CircularIntArray positions =
2583                             mGrid.getItemPositionsInRows(mGrid.getFirstVisibleIndex(), pos)[row];
2584                     firstView = findViewByPosition(positions.get(0));
2585                     if (viewMax - getViewMin(firstView) > clientSize) {
2586                         if (positions.size() > 2) {
2587                             firstView = findViewByPosition(positions.get(2));
2588                         }
2589                         break;
2590                     }
2591                 }
2592             }
2593         } else if (viewMax > clientSize + paddingLow) {
2594             // view enters high padding area:
2595             if (mFocusScrollStrategy == BaseGridView.FOCUS_SCROLL_PAGE) {
2596                 // scroll whole one page right/bottom, align view at the low padding edge.
2597                 firstView = view;
2598                 do {
2599                     CircularIntArray positions =
2600                             mGrid.getItemPositionsInRows(pos, mGrid.getLastVisibleIndex())[row];
2601                     lastView = findViewByPosition(positions.get(positions.size() - 1));
2602                     if (getViewMax(lastView) - viewMin > clientSize) {
2603                         lastView = null;
2604                         break;
2605                     }
2606                 } while (appendOneColumnVisibleItems());
2607                 if (lastView != null) {
2608                     // however if we reached end,  we should align last view.
2609                     firstView = null;
2610                 }
2611             } else {
2612                 lastView = view;
2613             }
2614         }
2615         int scrollPrimary = 0;
2616         int scrollSecondary = 0;
2617         if (firstView != null) {
2618             scrollPrimary = getViewMin(firstView) - paddingLow;
2619         } else if (lastView != null) {
2620             scrollPrimary = getViewMax(lastView) - (paddingLow + clientSize);
2621         }
2622         View secondaryAlignedView;
2623         if (firstView != null) {
2624             secondaryAlignedView = firstView;
2625         } else if (lastView != null) {
2626             secondaryAlignedView = lastView;
2627         } else {
2628             secondaryAlignedView = view;
2629         }
2630         scrollSecondary = getSecondarySystemScrollPosition(secondaryAlignedView);
2631         scrollSecondary -= mScrollOffsetSecondary;
2632         if (scrollPrimary != 0 || scrollSecondary != 0) {
2633             deltas[0] = scrollPrimary;
2634             deltas[1] = scrollSecondary;
2635             return true;
2636         }
2637         return false;
2638     }
2639 
getAlignedPosition(View view, View childView, int[] deltas)2640     private boolean getAlignedPosition(View view, View childView, int[] deltas) {
2641         int scrollPrimary = getPrimarySystemScrollPosition(view);
2642         if (childView != null) {
2643             scrollPrimary = getAdjustedPrimaryScrollPosition(scrollPrimary, view, childView);
2644         }
2645         int scrollSecondary = getSecondarySystemScrollPosition(view);
2646         if (DEBUG) {
2647             Log.v(getTag(), "getAlignedPosition " + scrollPrimary + " " + scrollSecondary
2648                     + " " + mPrimaryScrollExtra + " " + mWindowAlignment);
2649             Log.v(getTag(), "getAlignedPosition " + mScrollOffsetPrimary + " " + mScrollOffsetSecondary);
2650         }
2651         scrollPrimary -= mScrollOffsetPrimary;
2652         scrollSecondary -= mScrollOffsetSecondary;
2653         scrollPrimary += mPrimaryScrollExtra;
2654         if (scrollPrimary != 0 || scrollSecondary != 0) {
2655             deltas[0] = scrollPrimary;
2656             deltas[1] = scrollSecondary;
2657             return true;
2658         }
2659         return false;
2660     }
2661 
scrollGrid(int scrollPrimary, int scrollSecondary, boolean smooth)2662     private void scrollGrid(int scrollPrimary, int scrollSecondary, boolean smooth) {
2663         if (mInLayout) {
2664             scrollDirectionPrimary(scrollPrimary);
2665             scrollDirectionSecondary(scrollSecondary);
2666         } else {
2667             int scrollX;
2668             int scrollY;
2669             if (mOrientation == HORIZONTAL) {
2670                 scrollX = scrollPrimary;
2671                 scrollY = scrollSecondary;
2672             } else {
2673                 scrollX = scrollSecondary;
2674                 scrollY = scrollPrimary;
2675             }
2676             if (smooth) {
2677                 mBaseGridView.smoothScrollBy(scrollX, scrollY);
2678             } else {
2679                 mBaseGridView.scrollBy(scrollX, scrollY);
2680             }
2681         }
2682     }
2683 
setPruneChild(boolean pruneChild)2684     public void setPruneChild(boolean pruneChild) {
2685         if (mPruneChild != pruneChild) {
2686             mPruneChild = pruneChild;
2687             if (mPruneChild) {
2688                 requestLayout();
2689             }
2690         }
2691     }
2692 
getPruneChild()2693     public boolean getPruneChild() {
2694         return mPruneChild;
2695     }
2696 
setScrollEnabled(boolean scrollEnabled)2697     public void setScrollEnabled(boolean scrollEnabled) {
2698         if (mScrollEnabled != scrollEnabled) {
2699             mScrollEnabled = scrollEnabled;
2700             if (mScrollEnabled && mFocusScrollStrategy == BaseGridView.FOCUS_SCROLL_ALIGNED
2701                     && mFocusPosition != NO_POSITION) {
2702                 scrollToSelection(mFocusPosition, mSubFocusPosition,
2703                         true, mPrimaryScrollExtra);
2704             }
2705         }
2706     }
2707 
isScrollEnabled()2708     public boolean isScrollEnabled() {
2709         return mScrollEnabled;
2710     }
2711 
findImmediateChildIndex(View view)2712     private int findImmediateChildIndex(View view) {
2713         if (mBaseGridView != null && view != mBaseGridView) {
2714             view = findContainingItemView(view);
2715             if (view != null) {
2716                 for (int i = 0, count = getChildCount(); i < count; i++) {
2717                     if (getChildAt(i) == view) {
2718                         return i;
2719                     }
2720                 }
2721             }
2722         }
2723         return NO_POSITION;
2724     }
2725 
onFocusChanged(boolean gainFocus, int direction, Rect previouslyFocusedRect)2726     void onFocusChanged(boolean gainFocus, int direction, Rect previouslyFocusedRect) {
2727         if (gainFocus) {
2728             // if gridview.requestFocus() is called, select first focusable child.
2729             for (int i = mFocusPosition; ;i++) {
2730                 View view = findViewByPosition(i);
2731                 if (view == null) {
2732                     break;
2733                 }
2734                 if (view.getVisibility() == View.VISIBLE && view.hasFocusable()) {
2735                     view.requestFocus();
2736                     break;
2737                 }
2738             }
2739         }
2740     }
2741 
setFocusSearchDisabled(boolean disabled)2742     void setFocusSearchDisabled(boolean disabled) {
2743         mFocusSearchDisabled = disabled;
2744     }
2745 
isFocusSearchDisabled()2746     boolean isFocusSearchDisabled() {
2747         return mFocusSearchDisabled;
2748     }
2749 
2750     @Override
onInterceptFocusSearch(View focused, int direction)2751     public View onInterceptFocusSearch(View focused, int direction) {
2752         if (mFocusSearchDisabled) {
2753             return focused;
2754         }
2755 
2756         final FocusFinder ff = FocusFinder.getInstance();
2757         View result = null;
2758         if (direction == View.FOCUS_FORWARD || direction == View.FOCUS_BACKWARD) {
2759             // convert direction to absolute direction and see if we have a view there and if not
2760             // tell LayoutManager to add if it can.
2761             if (canScrollVertically()) {
2762                 final int absDir =
2763                         direction == View.FOCUS_FORWARD ? View.FOCUS_DOWN : View.FOCUS_UP;
2764                 result = ff.findNextFocus(mBaseGridView, focused, absDir);
2765             }
2766             if (canScrollHorizontally()) {
2767                 boolean rtl = getLayoutDirection() == ViewCompat.LAYOUT_DIRECTION_RTL;
2768                 final int absDir = (direction == View.FOCUS_FORWARD) ^ rtl
2769                         ? View.FOCUS_RIGHT : View.FOCUS_LEFT;
2770                 result = ff.findNextFocus(mBaseGridView, focused, absDir);
2771             }
2772         } else {
2773             result = ff.findNextFocus(mBaseGridView, focused, direction);
2774         }
2775         if (result != null) {
2776             return result;
2777         }
2778 
2779         if (DEBUG) Log.v(getTag(), "regular focusSearch failed direction " + direction);
2780         int movement = getMovement(direction);
2781         final boolean isScroll = mBaseGridView.getScrollState() != RecyclerView.SCROLL_STATE_IDLE;
2782         if (movement == NEXT_ITEM) {
2783             if (isScroll || !mFocusOutEnd) {
2784                 result = focused;
2785             }
2786             if (mScrollEnabled && !hasCreatedLastItem()) {
2787                 processPendingMovement(true);
2788                 result = focused;
2789             }
2790         } else if (movement == PREV_ITEM) {
2791             if (isScroll || !mFocusOutFront) {
2792                 result = focused;
2793             }
2794             if (mScrollEnabled && !hasCreatedFirstItem()) {
2795                 processPendingMovement(false);
2796                 result = focused;
2797             }
2798         } else if (movement == NEXT_ROW) {
2799             if (isScroll || !mFocusOutSideEnd) {
2800                 result = focused;
2801             }
2802         } else if (movement == PREV_ROW) {
2803             if (isScroll || !mFocusOutSideStart) {
2804                 result = focused;
2805             }
2806         }
2807         if (result != null) {
2808             return result;
2809         }
2810 
2811         if (DEBUG) Log.v(getTag(), "now focusSearch in parent");
2812         result = mBaseGridView.getParent().focusSearch(focused, direction);
2813         if (result != null) {
2814             return result;
2815         }
2816         return focused != null ? focused : mBaseGridView;
2817     }
2818 
hasPreviousViewInSameRow(int pos)2819     boolean hasPreviousViewInSameRow(int pos) {
2820         if (mGrid == null || pos == NO_POSITION || mGrid.getFirstVisibleIndex() < 0) {
2821             return false;
2822         }
2823         if (mGrid.getFirstVisibleIndex() > 0) {
2824             return true;
2825         }
2826         final int focusedRow = mGrid.getLocation(pos).row;
2827         for (int i = getChildCount() - 1; i >= 0; i--) {
2828             int position = getPositionByIndex(i);
2829             Grid.Location loc = mGrid.getLocation(position);
2830             if (loc != null && loc.row == focusedRow) {
2831                 if (position < pos) {
2832                     return true;
2833                 }
2834             }
2835         }
2836         return false;
2837     }
2838 
2839     @Override
onAddFocusables(RecyclerView recyclerView, ArrayList<View> views, int direction, int focusableMode)2840     public boolean onAddFocusables(RecyclerView recyclerView,
2841             ArrayList<View> views, int direction, int focusableMode) {
2842         if (mFocusSearchDisabled) {
2843             return true;
2844         }
2845         // If this viewgroup or one of its children currently has focus then we
2846         // consider our children for focus searching in main direction on the same row.
2847         // If this viewgroup has no focus and using focus align, we want the system
2848         // to ignore our children and pass focus to the viewgroup, which will pass
2849         // focus on to its children appropriately.
2850         // If this viewgroup has no focus and not using focus align, we want to
2851         // consider the child that does not overlap with padding area.
2852         if (recyclerView.hasFocus()) {
2853             if (mPendingMoveSmoothScroller != null) {
2854                 // don't find next focusable if has pending movement.
2855                 return true;
2856             }
2857             final int movement = getMovement(direction);
2858             final View focused = recyclerView.findFocus();
2859             final int focusedIndex = findImmediateChildIndex(focused);
2860             final int focusedPos = getPositionByIndex(focusedIndex);
2861             // Add focusables of focused item.
2862             if (focusedPos != NO_POSITION) {
2863                 findViewByPosition(focusedPos).addFocusables(views,  direction, focusableMode);
2864             }
2865             if (mGrid == null || getChildCount() == 0) {
2866                 // no grid information, or no child, bail out.
2867                 return true;
2868             }
2869             if ((movement == NEXT_ROW || movement == PREV_ROW) && mGrid.getNumRows() <= 1) {
2870                 // For single row, cannot navigate to previous/next row.
2871                 return true;
2872             }
2873             // Add focusables of neighbor depending on the focus search direction.
2874             final int focusedRow = mGrid != null && focusedPos != NO_POSITION ?
2875                     mGrid.getLocation(focusedPos).row : NO_POSITION;
2876             final int focusableCount = views.size();
2877             int inc = movement == NEXT_ITEM || movement == NEXT_ROW ? 1 : -1;
2878             int loop_end = inc > 0 ? getChildCount() - 1 : 0;
2879             int loop_start;
2880             if (focusedIndex == NO_POSITION) {
2881                 loop_start = inc > 0 ? 0 : getChildCount() - 1;
2882             } else {
2883                 loop_start = focusedIndex + inc;
2884             }
2885             for (int i = loop_start; inc > 0 ? i <= loop_end : i >= loop_end; i += inc) {
2886                 final View child = getChildAt(i);
2887                 if (child.getVisibility() != View.VISIBLE || !child.hasFocusable()) {
2888                     continue;
2889                 }
2890                 // if there wasn't any focusing item,  add the very first focusable
2891                 // items and stop.
2892                 if (focusedPos == NO_POSITION) {
2893                     child.addFocusables(views,  direction, focusableMode);
2894                     if (views.size() > focusableCount) {
2895                         break;
2896                     }
2897                     continue;
2898                 }
2899                 int position = getPositionByIndex(i);
2900                 Grid.Location loc = mGrid.getLocation(position);
2901                 if (loc == null) {
2902                     continue;
2903                 }
2904                 if (movement == NEXT_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 == PREV_ITEM) {
2913                     // Add first focusable item on the same row
2914                     if (loc.row == focusedRow && position < focusedPos) {
2915                         child.addFocusables(views,  direction, focusableMode);
2916                         if (views.size() > focusableCount) {
2917                             break;
2918                         }
2919                     }
2920                 } else if (movement == NEXT_ROW) {
2921                     // Add all focusable items after this item whose row index is bigger
2922                     if (loc.row == focusedRow) {
2923                         continue;
2924                     } else if (loc.row < focusedRow) {
2925                         break;
2926                     }
2927                     child.addFocusables(views,  direction, focusableMode);
2928                 } else if (movement == PREV_ROW) {
2929                     // Add all focusable items before this item whose row index is smaller
2930                     if (loc.row == focusedRow) {
2931                         continue;
2932                     } else if (loc.row > focusedRow) {
2933                         break;
2934                     }
2935                     child.addFocusables(views,  direction, focusableMode);
2936                 }
2937             }
2938         } else {
2939             int focusableCount = views.size();
2940             if (mFocusScrollStrategy != BaseGridView.FOCUS_SCROLL_ALIGNED) {
2941                 // adding views not overlapping padding area to avoid scrolling in gaining focus
2942                 int left = mWindowAlignment.mainAxis().getPaddingLow();
2943                 int right = mWindowAlignment.mainAxis().getClientSize() + left;
2944                 for (int i = 0, count = getChildCount(); i < count; i++) {
2945                     View child = getChildAt(i);
2946                     if (child.getVisibility() == View.VISIBLE) {
2947                         if (getViewMin(child) >= left && getViewMax(child) <= right) {
2948                             child.addFocusables(views, direction, focusableMode);
2949                         }
2950                     }
2951                 }
2952                 // if we cannot find any, then just add all children.
2953                 if (views.size() == focusableCount) {
2954                     for (int i = 0, count = getChildCount(); i < count; i++) {
2955                         View child = getChildAt(i);
2956                         if (child.getVisibility() == View.VISIBLE) {
2957                             child.addFocusables(views, direction, focusableMode);
2958                         }
2959                     }
2960                 }
2961             } else {
2962                 View view = findViewByPosition(mFocusPosition);
2963                 if (view != null) {
2964                     view.addFocusables(views, direction, focusableMode);
2965                 }
2966             }
2967             // if still cannot find any, fall through and add itself
2968             if (views.size() != focusableCount) {
2969                 return true;
2970             }
2971             if (recyclerView.isFocusable()) {
2972                 views.add(recyclerView);
2973             }
2974         }
2975         return true;
2976     }
2977 
hasCreatedLastItem()2978     private boolean hasCreatedLastItem() {
2979         int count = getItemCount();
2980         return count == 0 || mBaseGridView.findViewHolderForAdapterPosition(count - 1) != null;
2981     }
2982 
hasCreatedFirstItem()2983     private boolean hasCreatedFirstItem() {
2984         int count = getItemCount();
2985         return count == 0 || mBaseGridView.findViewHolderForAdapterPosition(0) != null;
2986     }
2987 
canScrollTo(View view)2988     boolean canScrollTo(View view) {
2989         return view.getVisibility() == View.VISIBLE && (!hasFocus() || view.hasFocusable());
2990     }
2991 
gridOnRequestFocusInDescendants(RecyclerView recyclerView, int direction, Rect previouslyFocusedRect)2992     boolean gridOnRequestFocusInDescendants(RecyclerView recyclerView, int direction,
2993             Rect previouslyFocusedRect) {
2994         switch (mFocusScrollStrategy) {
2995         case BaseGridView.FOCUS_SCROLL_ALIGNED:
2996         default:
2997             return gridOnRequestFocusInDescendantsAligned(recyclerView,
2998                     direction, previouslyFocusedRect);
2999         case BaseGridView.FOCUS_SCROLL_PAGE:
3000         case BaseGridView.FOCUS_SCROLL_ITEM:
3001             return gridOnRequestFocusInDescendantsUnaligned(recyclerView,
3002                     direction, previouslyFocusedRect);
3003         }
3004     }
3005 
gridOnRequestFocusInDescendantsAligned(RecyclerView recyclerView, int direction, Rect previouslyFocusedRect)3006     private boolean gridOnRequestFocusInDescendantsAligned(RecyclerView recyclerView,
3007             int direction, Rect previouslyFocusedRect) {
3008         View view = findViewByPosition(mFocusPosition);
3009         if (view != null) {
3010             boolean result = view.requestFocus(direction, previouslyFocusedRect);
3011             if (!result && DEBUG) {
3012                 Log.w(getTag(), "failed to request focus on " + view);
3013             }
3014             return result;
3015         }
3016         return false;
3017     }
3018 
gridOnRequestFocusInDescendantsUnaligned(RecyclerView recyclerView, int direction, Rect previouslyFocusedRect)3019     private boolean gridOnRequestFocusInDescendantsUnaligned(RecyclerView recyclerView,
3020             int direction, Rect previouslyFocusedRect) {
3021         // focus to view not overlapping padding area to avoid scrolling in gaining focus
3022         int index;
3023         int increment;
3024         int end;
3025         int count = getChildCount();
3026         if ((direction & View.FOCUS_FORWARD) != 0) {
3027             index = 0;
3028             increment = 1;
3029             end = count;
3030         } else {
3031             index = count - 1;
3032             increment = -1;
3033             end = -1;
3034         }
3035         int left = mWindowAlignment.mainAxis().getPaddingLow();
3036         int right = mWindowAlignment.mainAxis().getClientSize() + left;
3037         for (int i = index; i != end; i += increment) {
3038             View child = getChildAt(i);
3039             if (child.getVisibility() == View.VISIBLE) {
3040                 if (getViewMin(child) >= left && getViewMax(child) <= right) {
3041                     if (child.requestFocus(direction, previouslyFocusedRect)) {
3042                         return true;
3043                     }
3044                 }
3045             }
3046         }
3047         return false;
3048     }
3049 
3050     private final static int PREV_ITEM = 0;
3051     private final static int NEXT_ITEM = 1;
3052     private final static int PREV_ROW = 2;
3053     private final static int NEXT_ROW = 3;
3054 
getMovement(int direction)3055     private int getMovement(int direction) {
3056         int movement = View.FOCUS_LEFT;
3057 
3058         if (mOrientation == HORIZONTAL) {
3059             switch(direction) {
3060                 case View.FOCUS_LEFT:
3061                     movement = (!mReverseFlowPrimary) ? PREV_ITEM : NEXT_ITEM;
3062                     break;
3063                 case View.FOCUS_RIGHT:
3064                     movement = (!mReverseFlowPrimary) ? NEXT_ITEM : PREV_ITEM;
3065                     break;
3066                 case View.FOCUS_UP:
3067                     movement = PREV_ROW;
3068                     break;
3069                 case View.FOCUS_DOWN:
3070                     movement = NEXT_ROW;
3071                     break;
3072             }
3073          } else if (mOrientation == VERTICAL) {
3074              switch(direction) {
3075                  case View.FOCUS_LEFT:
3076                      movement = (!mReverseFlowSecondary) ? PREV_ROW : NEXT_ROW;
3077                      break;
3078                  case View.FOCUS_RIGHT:
3079                      movement = (!mReverseFlowSecondary) ? NEXT_ROW : PREV_ROW;
3080                      break;
3081                  case View.FOCUS_UP:
3082                      movement = PREV_ITEM;
3083                      break;
3084                  case View.FOCUS_DOWN:
3085                      movement = NEXT_ITEM;
3086                      break;
3087              }
3088          }
3089 
3090         return movement;
3091     }
3092 
getChildDrawingOrder(RecyclerView recyclerView, int childCount, int i)3093     int getChildDrawingOrder(RecyclerView recyclerView, int childCount, int i) {
3094         View view = findViewByPosition(mFocusPosition);
3095         if (view == null) {
3096             return i;
3097         }
3098         int focusIndex = recyclerView.indexOfChild(view);
3099         // supposely 0 1 2 3 4 5 6 7 8 9, 4 is the center item
3100         // drawing order is 0 1 2 3 9 8 7 6 5 4
3101         if (i < focusIndex) {
3102             return i;
3103         } else if (i < childCount - 1) {
3104             return focusIndex + childCount - 1 - i;
3105         } else {
3106             return focusIndex;
3107         }
3108     }
3109 
3110     @Override
onAdapterChanged(RecyclerView.Adapter oldAdapter, RecyclerView.Adapter newAdapter)3111     public void onAdapterChanged(RecyclerView.Adapter oldAdapter,
3112             RecyclerView.Adapter newAdapter) {
3113         if (DEBUG) Log.v(getTag(), "onAdapterChanged to " + newAdapter);
3114         if (oldAdapter != null) {
3115             discardLayoutInfo();
3116             mFocusPosition = NO_POSITION;
3117             mFocusPositionOffset = 0;
3118             mChildrenStates.clear();
3119         }
3120         if (newAdapter instanceof FacetProviderAdapter) {
3121             mFacetProviderAdapter = (FacetProviderAdapter) newAdapter;
3122         } else {
3123             mFacetProviderAdapter = null;
3124         }
3125         super.onAdapterChanged(oldAdapter, newAdapter);
3126     }
3127 
discardLayoutInfo()3128     private void discardLayoutInfo() {
3129         mGrid = null;
3130         mRowSizeSecondary = null;
3131         mRowSecondarySizeRefresh = false;
3132     }
3133 
setLayoutEnabled(boolean layoutEnabled)3134     public void setLayoutEnabled(boolean layoutEnabled) {
3135         if (mLayoutEnabled != layoutEnabled) {
3136             mLayoutEnabled = layoutEnabled;
3137             requestLayout();
3138         }
3139     }
3140 
setChildrenVisibility(int visiblity)3141     void setChildrenVisibility(int visiblity) {
3142         mChildVisibility = visiblity;
3143         if (mChildVisibility != -1) {
3144             int count = getChildCount();
3145             for (int i= 0; i < count; i++) {
3146                 getChildAt(i).setVisibility(mChildVisibility);
3147             }
3148         }
3149     }
3150 
3151     final static class SavedState implements Parcelable {
3152 
3153         int index; // index inside adapter of the current view
3154         Bundle childStates = Bundle.EMPTY;
3155 
3156         @Override
writeToParcel(Parcel out, int flags)3157         public void writeToParcel(Parcel out, int flags) {
3158             out.writeInt(index);
3159             out.writeBundle(childStates);
3160         }
3161 
3162         @SuppressWarnings("hiding")
3163         public static final Parcelable.Creator<SavedState> CREATOR =
3164                 new Parcelable.Creator<SavedState>() {
3165                     @Override
3166                     public SavedState createFromParcel(Parcel in) {
3167                         return new SavedState(in);
3168                     }
3169 
3170                     @Override
3171                     public SavedState[] newArray(int size) {
3172                         return new SavedState[size];
3173                     }
3174                 };
3175 
3176         @Override
describeContents()3177         public int describeContents() {
3178             return 0;
3179         }
3180 
SavedState(Parcel in)3181         SavedState(Parcel in) {
3182             index = in.readInt();
3183             childStates = in.readBundle(GridLayoutManager.class.getClassLoader());
3184         }
3185 
SavedState()3186         SavedState() {
3187         }
3188     }
3189 
3190     @Override
onSaveInstanceState()3191     public Parcelable onSaveInstanceState() {
3192         if (DEBUG) Log.v(getTag(), "onSaveInstanceState getSelection() " + getSelection());
3193         SavedState ss = new SavedState();
3194         // save selected index
3195         ss.index = getSelection();
3196         // save offscreen child (state when they are recycled)
3197         Bundle bundle = mChildrenStates.saveAsBundle();
3198         // save views currently is on screen (TODO save cached views)
3199         for (int i = 0, count = getChildCount(); i < count; i++) {
3200             View view = getChildAt(i);
3201             int position = getPositionByView(view);
3202             if (position != NO_POSITION) {
3203                 bundle = mChildrenStates.saveOnScreenView(bundle, view, position);
3204             }
3205         }
3206         ss.childStates = bundle;
3207         return ss;
3208     }
3209 
onChildRecycled(RecyclerView.ViewHolder holder)3210     void onChildRecycled(RecyclerView.ViewHolder holder) {
3211         final int position = holder.getAdapterPosition();
3212         if (position != NO_POSITION) {
3213             mChildrenStates.saveOffscreenView(holder.itemView, position);
3214         }
3215     }
3216 
3217     @Override
onRestoreInstanceState(Parcelable state)3218     public void onRestoreInstanceState(Parcelable state) {
3219         if (!(state instanceof SavedState)) {
3220             return;
3221         }
3222         SavedState loadingState = (SavedState)state;
3223         mFocusPosition = loadingState.index;
3224         mFocusPositionOffset = 0;
3225         mChildrenStates.loadFromBundle(loadingState.childStates);
3226         mForceFullLayout = true;
3227         requestLayout();
3228         if (DEBUG) Log.v(getTag(), "onRestoreInstanceState mFocusPosition " + mFocusPosition);
3229     }
3230 
3231     @Override
getRowCountForAccessibility(RecyclerView.Recycler recycler, RecyclerView.State state)3232     public int getRowCountForAccessibility(RecyclerView.Recycler recycler,
3233             RecyclerView.State state) {
3234         if (mOrientation == HORIZONTAL && mGrid != null) {
3235             return mGrid.getNumRows();
3236         }
3237         return super.getRowCountForAccessibility(recycler, state);
3238     }
3239 
3240     @Override
getColumnCountForAccessibility(RecyclerView.Recycler recycler, RecyclerView.State state)3241     public int getColumnCountForAccessibility(RecyclerView.Recycler recycler,
3242             RecyclerView.State state) {
3243         if (mOrientation == VERTICAL && mGrid != null) {
3244             return mGrid.getNumRows();
3245         }
3246         return super.getColumnCountForAccessibility(recycler, state);
3247     }
3248 
3249     @Override
onInitializeAccessibilityNodeInfoForItem(RecyclerView.Recycler recycler, RecyclerView.State state, View host, AccessibilityNodeInfoCompat info)3250     public void onInitializeAccessibilityNodeInfoForItem(RecyclerView.Recycler recycler,
3251             RecyclerView.State state, View host, AccessibilityNodeInfoCompat info) {
3252         ViewGroup.LayoutParams lp = host.getLayoutParams();
3253         if (mGrid == null || !(lp instanceof LayoutParams)) {
3254             super.onInitializeAccessibilityNodeInfoForItem(recycler, state, host, info);
3255             return;
3256         }
3257         LayoutParams glp = (LayoutParams) lp;
3258         int position = glp.getViewLayoutPosition();
3259         int rowIndex = mGrid.getRowIndex(position);
3260         int guessSpanIndex = position / mGrid.getNumRows();
3261         if (mOrientation == HORIZONTAL) {
3262             info.setCollectionItemInfo(AccessibilityNodeInfoCompat.CollectionItemInfoCompat.obtain(
3263                     rowIndex, 1, guessSpanIndex, 1, false, false));
3264         } else {
3265             info.setCollectionItemInfo(AccessibilityNodeInfoCompat.CollectionItemInfoCompat.obtain(
3266                     guessSpanIndex, 1, rowIndex, 1, false, false));
3267         }
3268     }
3269 
3270     /*
3271      * Leanback widget is different than the default implementation because the "scroll" is driven
3272      * by selection change.
3273      */
3274     @Override
performAccessibilityAction(Recycler recycler, State state, int action, Bundle args)3275     public boolean performAccessibilityAction(Recycler recycler, State state, int action,
3276             Bundle args) {
3277         saveContext(recycler, state);
3278         switch (action) {
3279             case AccessibilityNodeInfoCompat.ACTION_SCROLL_BACKWARD:
3280                 // try to focus all the way to the last visible item on the same row.
3281                 processSelectionMoves(false, -mState.getItemCount());
3282                 break;
3283             case AccessibilityNodeInfoCompat.ACTION_SCROLL_FORWARD:
3284                 processSelectionMoves(false, mState.getItemCount());
3285                 break;
3286         }
3287         leaveContext();
3288         return true;
3289     }
3290 
3291     /*
3292      * Move mFocusPosition multiple steps on the same row in main direction.
3293      * Stops when moves are all consumed or reach first/last visible item.
3294      * Returning remaining moves.
3295      */
processSelectionMoves(boolean preventScroll, int moves)3296     private int processSelectionMoves(boolean preventScroll, int moves) {
3297         if (mGrid == null) {
3298             return moves;
3299         }
3300         int focusPosition = mFocusPosition;
3301         int focusedRow = focusPosition != NO_POSITION ?
3302                 mGrid.getRowIndex(focusPosition) : NO_POSITION;
3303         View newSelected = null;
3304         for (int i = 0, count = getChildCount(); i < count && moves != 0; i++) {
3305             int index = moves > 0 ? i : count - 1 - i;
3306             final View child = getChildAt(index);
3307             if (!canScrollTo(child)) {
3308                 continue;
3309             }
3310             int position = getPositionByIndex(index);
3311             int rowIndex = mGrid.getRowIndex(position);
3312             if (focusedRow == NO_POSITION) {
3313                 focusPosition = position;
3314                 newSelected = child;
3315                 focusedRow = rowIndex;
3316             } else if (rowIndex == focusedRow) {
3317                 if ((moves > 0 && position > focusPosition)
3318                         || (moves < 0 && position < focusPosition)) {
3319                     focusPosition = position;
3320                     newSelected = child;
3321                     if (moves > 0) {
3322                         moves--;
3323                     } else {
3324                         moves++;
3325                     }
3326                 }
3327             }
3328         }
3329         if (newSelected != null) {
3330             if (preventScroll) {
3331                 if (hasFocus()) {
3332                     mInSelection = true;
3333                     newSelected.requestFocus();
3334                     mInSelection = false;
3335                 }
3336                 mFocusPosition = focusPosition;
3337                 mSubFocusPosition = 0;
3338             } else {
3339                 scrollToView(newSelected, true);
3340             }
3341         }
3342         return moves;
3343     }
3344 
3345     @Override
onInitializeAccessibilityNodeInfo(Recycler recycler, State state, AccessibilityNodeInfoCompat info)3346     public void onInitializeAccessibilityNodeInfo(Recycler recycler, State state,
3347             AccessibilityNodeInfoCompat info) {
3348         saveContext(recycler, state);
3349         if (mScrollEnabled && !hasCreatedFirstItem()) {
3350             info.addAction(AccessibilityNodeInfoCompat.ACTION_SCROLL_BACKWARD);
3351             info.setScrollable(true);
3352         }
3353         if (mScrollEnabled && !hasCreatedLastItem()) {
3354             info.addAction(AccessibilityNodeInfoCompat.ACTION_SCROLL_FORWARD);
3355             info.setScrollable(true);
3356         }
3357         final AccessibilityNodeInfoCompat.CollectionInfoCompat collectionInfo
3358                 = AccessibilityNodeInfoCompat.CollectionInfoCompat
3359                 .obtain(getRowCountForAccessibility(recycler, state),
3360                         getColumnCountForAccessibility(recycler, state),
3361                         isLayoutHierarchical(recycler, state),
3362                         getSelectionModeForAccessibility(recycler, state));
3363         info.setCollectionInfo(collectionInfo);
3364         leaveContext();
3365     }
3366 }
3367