GridLayoutManager.java revision b56bf9a6ae8a8490ff440ed8c136a72ee6398157
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 */
14package android.support.v17.leanback.widget;
15
16import android.graphics.Rect;
17import android.support.v7.widget.RecyclerView;
18import android.support.v7.widget.RecyclerView.Adapter;
19import android.support.v7.widget.RecyclerView.Recycler;
20
21import static android.support.v7.widget.RecyclerView.NO_ID;
22import static android.support.v7.widget.RecyclerView.NO_POSITION;
23import static android.support.v7.widget.RecyclerView.HORIZONTAL;
24import static android.support.v7.widget.RecyclerView.VERTICAL;
25
26import android.support.v17.leanback.R;
27import android.util.Log;
28import android.view.FocusFinder;
29import android.view.View;
30import android.view.View.MeasureSpec;
31import android.view.ViewGroup;
32import android.view.animation.DecelerateInterpolator;
33import android.view.animation.Interpolator;
34
35import java.io.PrintWriter;
36import java.io.StringWriter;
37import java.util.ArrayList;
38
39final class GridLayoutManager extends RecyclerView.LayoutManager {
40
41    private static final String TAG = "GridLayoutManager";
42    private static final boolean DEBUG = false;
43
44    private static final Interpolator sDefaultAnimationChildLayoutInterpolator
45            = new DecelerateInterpolator();
46
47    private static final long DEFAULT_CHILD_ANIMATION_DURATION_MS = 250;
48
49    private String getTag() {
50        return TAG + ":" + mBaseListView.getId();
51    }
52
53    private final BaseListView mBaseListView;
54
55    /**
56     * The orientation of a "row".
57     */
58    private int mOrientation = HORIZONTAL;
59
60    private RecyclerView.Adapter mAdapter;
61    private RecyclerView.Recycler mRecycler;
62
63    private boolean mInLayout = false;
64
65    private OnChildSelectedListener mChildSelectedListener = null;
66
67    /**
68     * The focused position, it's not the currently visually aligned position
69     * but it is the final position that we intend to focus on. If there are
70     * multiple setSelection() called, mFocusPosition saves last value.
71     */
72    private int mFocusPosition = NO_POSITION;
73
74    /**
75     * Force a full layout under certain situations.
76     */
77    private boolean mForceFullLayout;
78
79    /**
80     * The scroll offsets of the viewport relative to the entire view.
81     */
82    private int mScrollOffsetPrimary;
83    private int mScrollOffsetSecondary;
84
85    /**
86     * User-specified fixed size of each grid item in the secondary direction, can be
87     * 0 to be determined by parent size and number of rows.
88     */
89    private int mItemLengthSecondaryRequested;
90    /**
91     * The fixed size of each grid item in the secondary direction. This corresponds to
92     * the row height, equal for all rows. Grid items may have variable length
93     * in the primary direction.
94     *
95     */
96    private int mItemLengthSecondary;
97
98    /**
99     * Margin between items.
100     */
101    private int mHorizontalMargin;
102    /**
103     * Margin between items vertically.
104     */
105    private int mVerticalMargin;
106    /**
107     * Margin in main direction.
108     */
109    private int mMarginPrimary;
110    /**
111     * Margin in second direction.
112     */
113    private int mMarginSecondary;
114
115    /**
116     * The number of rows in the grid.
117     */
118    private int mNumRows;
119    /**
120     * Number of rows requested, can be 0 to be determined by parent size and
121     * rowHeight.
122     */
123    private int mNumRowsRequested = 1;
124
125    /**
126     * Tracking start/end position of each row for visible items.
127     */
128    private StaggeredGrid.Row[] mRows;
129
130    /**
131     * Saves grid information of each view.
132     */
133    private StaggeredGrid mGrid;
134    /**
135     * Position of first item (included) that has attached views.
136     */
137    private int mFirstVisiblePos;
138    /**
139     * Position of last item (included) that has attached views.
140     */
141    private int mLastVisiblePos;
142
143    /**
144     * Defines how item view is aligned in the window.
145     */
146    private final WindowAlignment mWindowAlignment = new WindowAlignment();
147
148    /**
149     * Defines how item view is aligned.
150     */
151    private final ItemAlignment mItemAlignment = new ItemAlignment();
152
153    /**
154     * Dimensions of the view, width or height depending on orientation.
155     */
156    private int mSizePrimary;
157
158    /**
159     *  Allow DPAD key to navigate out at the front of the View (where position = 0),
160     *  default is false.
161     */
162    private boolean mFocusOutFront;
163
164    /**
165     * Allow DPAD key to navigate out at the end of the view, default is false.
166     */
167    private boolean mFocusOutEnd;
168
169    /**
170     * Animate layout changes from a child resizing or adding/removing a child.
171     */
172    private boolean mAnimateChildLayout = true;
173
174    /**
175     * Interpolator used to animate layout of children.
176     */
177    private Interpolator mAnimateLayoutChildInterpolator = sDefaultAnimationChildLayoutInterpolator;
178
179    /**
180     * Duration used to animate layout of children.
181     */
182    private long mAnimateLayoutChildDuration = DEFAULT_CHILD_ANIMATION_DURATION_MS;
183
184    public GridLayoutManager(BaseListView baseListView) {
185        mBaseListView = baseListView;
186    }
187
188    public void setOrientation(int orientation) {
189        if (orientation != HORIZONTAL && orientation != VERTICAL) {
190            if (DEBUG) Log.v(getTag(), "invalid orientation: " + orientation);
191            return;
192        }
193
194        mOrientation = orientation;
195        mWindowAlignment.setOrientation(orientation);
196        mItemAlignment.setOrientation(orientation);
197        mForceFullLayout = true;
198    }
199
200    public void setWindowAlignment(int windowAlignment) {
201        mWindowAlignment.mainAxis().setWindowAlignment(windowAlignment);
202    }
203
204    public int getWindowAlignment() {
205        return mWindowAlignment.mainAxis().getWindowAlignment();
206    }
207
208    public void setWindowAlignmentOffset(int alignmentOffset) {
209        mWindowAlignment.mainAxis().setWindowAlignmentOffset(alignmentOffset);
210    }
211
212    public int getWindowAlignmentOffset() {
213        return mWindowAlignment.mainAxis().getWindowAlignmentOffset();
214    }
215
216    public void setWindowAlignmentOffsetPercent(float offsetPercent) {
217        mWindowAlignment.mainAxis().setWindowAlignmentOffsetPercent(offsetPercent);
218    }
219
220    public float getWindowAlignmentOffsetPercent() {
221        return mWindowAlignment.mainAxis().getWindowAlignmentOffsetPercent();
222    }
223
224    public void setItemAlignmentOffset(int alignmentOffset) {
225        mItemAlignment.mainAxis().setItemAlignmentOffset(alignmentOffset);
226        updateChildAlignments();
227    }
228
229    public int getItemAlignmentOffset() {
230        return mItemAlignment.mainAxis().getItemAlignmentOffset();
231    }
232
233    public void setItemAlignmentOffsetPercent(float offsetPercent) {
234        mItemAlignment.mainAxis().setItemAlignmentOffsetPercent(offsetPercent);
235        updateChildAlignments();
236    }
237
238    public float getItemAlignmentOffsetPercent() {
239        return mItemAlignment.mainAxis().getItemAlignmentOffsetPercent();
240    }
241
242    public void setItemAlignmentViewId(int viewId) {
243        mItemAlignment.mainAxis().setItemAlignmentViewId(viewId);
244        updateChildAlignments();
245    }
246
247    public int getItemAlignmentViewId() {
248        return mItemAlignment.mainAxis().getItemAlignmentViewId();
249    }
250
251    public void setFocusOutAllowed(boolean throughFront, boolean throughEnd) {
252        mFocusOutFront = throughFront;
253        mFocusOutEnd = throughEnd;
254    }
255
256    public void setNumRows(int numRows) {
257        if (numRows < 0) throw new IllegalArgumentException();
258        mNumRowsRequested = numRows;
259        mForceFullLayout = true;
260    }
261
262    public void setRowHeight(int height) {
263        if (height < 0) throw new IllegalArgumentException();
264        mItemLengthSecondaryRequested = height;
265    }
266
267    public void setItemMargin(int margin) {
268        mVerticalMargin = mHorizontalMargin = margin;
269        mMarginPrimary = mMarginSecondary = margin;
270    }
271
272    public void setVerticalMargin(int margin) {
273        if (mOrientation == HORIZONTAL) {
274            mMarginSecondary = mVerticalMargin = margin;
275        } else {
276            mMarginPrimary = mVerticalMargin = margin;
277        }
278    }
279
280    public void setHorizontalMargin(int margin) {
281        if (mOrientation == HORIZONTAL) {
282            mMarginPrimary = mHorizontalMargin = margin;
283        } else {
284            mMarginSecondary = mHorizontalMargin = margin;
285        }
286    }
287
288    public int getVerticalMargin() {
289        return mVerticalMargin;
290    }
291
292    public int getHorizontalMargin() {
293        return mHorizontalMargin;
294    }
295
296    protected int getMeasuredLengthPrimary(View v) {
297        if (mOrientation == HORIZONTAL) {
298            float aspectRatio = (float) v.getMeasuredWidth() / (float) v.getMeasuredHeight();
299            return (int) (aspectRatio * mItemLengthSecondary);
300        } else {
301            float aspectRatio = (float) v.getMeasuredHeight() / (float) v.getMeasuredWidth();
302            return (int) (aspectRatio * mItemLengthSecondary);
303        }
304    }
305
306    protected boolean hasDoneFirstLayout() {
307        return mGrid != null;
308    }
309
310    public void setOnChildSelectedListener(OnChildSelectedListener listener) {
311        mChildSelectedListener = listener;
312    }
313
314    private int getPositionByView(View view) {
315        return getPositionByIndex(mBaseListView.indexOfChild(view));
316    }
317
318    private int getPositionByIndex(int index) {
319        if (index < 0) {
320            return NO_POSITION;
321        }
322        return mFirstVisiblePos + index;
323    }
324
325    private View getViewByPosition(int position) {
326        int index = getIndexByPosition(position);
327        if (index < 0) {
328            return null;
329        }
330        return getChildAt(index);
331    }
332
333    private int getIndexByPosition(int position) {
334        if (mFirstVisiblePos < 0 ||
335                position < mFirstVisiblePos || position > mLastVisiblePos) {
336            return -1;
337        }
338        return position - mFirstVisiblePos;
339    }
340
341    private void dispatchChildSelected() {
342        if (mChildSelectedListener == null) {
343            return;
344        }
345
346        View view = getViewByPosition(mFocusPosition);
347
348        if (mFocusPosition != NO_POSITION) {
349            mChildSelectedListener.onChildSelected(mBaseListView, view, mFocusPosition,
350                    mAdapter.getItemId(mFocusPosition));
351        } else {
352            mChildSelectedListener.onChildSelected(mBaseListView, null, NO_POSITION, NO_ID);
353        }
354    }
355
356    @Override
357    public boolean canScrollHorizontally() {
358        // We can scroll horizontally if we have horizontal orientation, or if
359        // we are vertical and have more than one column.
360        return mOrientation == HORIZONTAL || mNumRows > 1;
361    }
362
363    @Override
364    public boolean canScrollVertically() {
365        // We can scroll vertically if we have vertical orientation, or if we
366        // are horizontal and have more than one row.
367        return mOrientation == VERTICAL || mNumRows > 1;
368    }
369
370    @Override
371    public RecyclerView.LayoutParams generateDefaultLayoutParams() {
372        return new RecyclerView.LayoutParams(ViewGroup.LayoutParams.WRAP_CONTENT,
373                ViewGroup.LayoutParams.WRAP_CONTENT);
374    }
375
376    private static GridLayoutManagerChildTag getViewTag(View v) {
377        return (GridLayoutManagerChildTag) v.getTag(R.id.lb_gridlayoutmanager_tag);
378    }
379
380    protected View getViewForPosition(int position) {
381        View v = mRecycler.getViewForPosition(mAdapter, position);
382        if (v != null) {
383            GridLayoutManagerChildTag tag = getViewTag(v);
384            if (tag == null) {
385                tag = new GridLayoutManagerChildTag();
386                v.setTag(R.id.lb_gridlayoutmanager_tag, tag);
387            }
388            tag.attach(this, v);
389        }
390        return v;
391    }
392
393    private int getViewMin(View view) {
394        GridLayoutManagerChildTag tag = getViewTag(view);
395        return (mOrientation == HORIZONTAL) ? tag.getOpticalLeft() : tag.getOpticalTop();
396    }
397
398    private int getViewMax(View view) {
399        GridLayoutManagerChildTag tag = getViewTag(view);
400        return (mOrientation == HORIZONTAL) ? tag.getOpticalRight() : tag.getOpticalBottom();
401    }
402
403    private int getViewCenter(View view) {
404        return (mOrientation == HORIZONTAL) ? getViewCenterX(view) : getViewCenterY(view);
405    }
406
407    private int getViewCenterSecondary(View view) {
408        return (mOrientation == HORIZONTAL) ? getViewCenterY(view) : getViewCenterX(view);
409    }
410
411    private int getViewCenterX(View view) {
412        GridLayoutManagerChildTag tag = getViewTag(view);
413        return tag.getOpticalLeft() + tag.getAlignX();
414    }
415
416    private int getViewCenterY(View view) {
417        GridLayoutManagerChildTag tag = getViewTag(view);
418        return tag.getOpticalTop() + tag.getAlignY();
419    }
420
421    /**
422     * Re-initialize data structures for a data change or handling invisible
423     * selection. The method tries its best to preserve position information so
424     * that staggered grid looks same before and after re-initialize.
425     * @param focusPosition The initial focusPosition that we would like to
426     *        focus on.
427     * @return Actual position that can be focused on.
428     */
429    private int init(RecyclerView.Adapter adapter, RecyclerView.Recycler recycler,
430            int focusPosition) {
431
432        final int newItemCount = adapter.getItemCount();
433
434        if (focusPosition == NO_POSITION && newItemCount > 0) {
435            // if focus position is never set before,  initialize it to 0
436            focusPosition = 0;
437        }
438        // If adapter has changed then caches are invalid; otherwise,
439        // we try to maintain each row's position if number of rows keeps the same
440        // and existing mGrid contains the focusPosition.
441        if (mRows != null && mNumRows == mRows.length &&
442                mGrid != null && mGrid.getSize() > 0 && focusPosition >= 0 &&
443                focusPosition >= mGrid.getFirstIndex() &&
444                focusPosition <= mGrid.getLastIndex()) {
445            // strip mGrid to a subset (like a column) that contains focusPosition
446            mGrid.stripDownTo(focusPosition);
447            // make sure that remaining items do not exceed new adapter size
448            int firstIndex = mGrid.getFirstIndex();
449            int lastIndex = mGrid.getLastIndex();
450            if (DEBUG) {
451                Log .v(getTag(), "mGrid firstIndex " + firstIndex + " lastIndex " + lastIndex);
452            }
453            for (int i = lastIndex; i >=firstIndex; i--) {
454                if (i >= newItemCount) {
455                    mGrid.removeLast();
456                }
457            }
458            if (mGrid.getSize() == 0) {
459                focusPosition = newItemCount - 1;
460                // initialize row start locations
461                for (int i = 0; i < mNumRows; i++) {
462                    mRows[i].low = 0;
463                    mRows[i].high = 0;
464                }
465                if (DEBUG) Log.v(getTag(), "mGrid zero size");
466            } else {
467                // initialize row start locations
468                for (int i = 0; i < mNumRows; i++) {
469                    mRows[i].low = Integer.MAX_VALUE;
470                    mRows[i].high = Integer.MIN_VALUE;
471                }
472                firstIndex = mGrid.getFirstIndex();
473                lastIndex = mGrid.getLastIndex();
474                if (focusPosition > lastIndex) {
475                    focusPosition = mGrid.getLastIndex();
476                }
477                if (DEBUG) {
478                    Log.v(getTag(), "mGrid firstIndex " + firstIndex + " lastIndex "
479                        + lastIndex + " focusPosition " + focusPosition);
480                }
481                // fill rows with minimal view positions of the subset
482                for (int i = firstIndex; i <= lastIndex; i++) {
483                    View v = getViewByPosition(i);
484                    if (v == null) {
485                        continue;
486                    }
487                    int row = mGrid.getLocation(i).row;
488                    int low = getViewMin(v) + mScrollOffsetPrimary;
489                    if (low < mRows[row].low) {
490                        mRows[row].low = mRows[row].high = low;
491                    }
492                }
493                // fill other rows that does not include the subset using first item
494                int firstItemRowPosition = mRows[mGrid.getLocation(firstIndex).row].low;
495                if (firstItemRowPosition == Integer.MAX_VALUE) {
496                    firstItemRowPosition = 0;
497                }
498                for (int i = 0; i < mNumRows; i++) {
499                    if (mRows[i].low == Integer.MAX_VALUE) {
500                        mRows[i].low = mRows[i].high = firstItemRowPosition;
501                    }
502                }
503            }
504
505            // Same adapter, we can reuse any attached views
506            detachAndScrapAttachedViews(recycler);
507
508        } else {
509            // otherwise recreate data structure
510            mRows = new StaggeredGrid.Row[mNumRows];
511            for (int i = 0; i < mNumRows; i++) {
512                mRows[i] = new StaggeredGrid.Row();
513            }
514            mGrid = new StaggeredGridDefault();
515            if (newItemCount == 0) {
516                focusPosition = NO_POSITION;
517            } else if (focusPosition >= newItemCount) {
518                focusPosition = newItemCount - 1;
519            }
520
521            // Adapter may have changed so remove all attached views permanently
522            removeAllViews();
523
524            mScrollOffsetPrimary = 0;
525            mScrollOffsetSecondary = 0;
526            mWindowAlignment.reset();
527        }
528
529        mAdapter = adapter;
530        mRecycler = recycler;
531        mGrid.setProvider(mGridProvider);
532        // mGrid share the same Row array information
533        mGrid.setRows(mRows);
534        mFirstVisiblePos = mLastVisiblePos = -1;
535
536        initScrollController();
537
538        return focusPosition;
539    }
540
541    // TODO: use recyclerview support for measuring the whole container, once
542    // it's available.
543    void onMeasure(int widthSpec, int heightSpec, int[] result) {
544        int sizePrimary, sizeSecondary, modeSecondary, paddingSecondary;
545        int measuredSizeSecondary;
546        if (mOrientation == HORIZONTAL) {
547            sizePrimary = MeasureSpec.getSize(widthSpec);
548            sizeSecondary = MeasureSpec.getSize(heightSpec);
549            modeSecondary = MeasureSpec.getMode(heightSpec);
550            paddingSecondary = getPaddingTop() + getPaddingBottom();
551        } else {
552            sizeSecondary = MeasureSpec.getSize(widthSpec);
553            sizePrimary = MeasureSpec.getSize(heightSpec);
554            modeSecondary = MeasureSpec.getMode(widthSpec);
555            paddingSecondary = getPaddingLeft() + getPaddingRight();
556        }
557        switch (modeSecondary) {
558        case MeasureSpec.UNSPECIFIED:
559            if (mItemLengthSecondaryRequested == 0) {
560                if (mOrientation == HORIZONTAL) {
561                    throw new IllegalStateException("Must specify rowHeight or view height");
562                } else {
563                    throw new IllegalStateException("Must specify columnWidth or view width");
564                }
565            }
566            mItemLengthSecondary = mItemLengthSecondaryRequested;
567            if (mNumRowsRequested == 0) {
568                mNumRows = 1;
569            } else {
570                mNumRows = mNumRowsRequested;
571            }
572            measuredSizeSecondary = mItemLengthSecondary * mNumRows + mMarginSecondary
573                * (mNumRows - 1) + paddingSecondary;
574            break;
575        case MeasureSpec.AT_MOST:
576        case MeasureSpec.EXACTLY:
577            if (mNumRowsRequested == 0 && mItemLengthSecondaryRequested == 0) {
578                mNumRows = 1;
579                mItemLengthSecondary = sizeSecondary - paddingSecondary;
580            } else if (mNumRowsRequested == 0) {
581                mItemLengthSecondary = mItemLengthSecondaryRequested;
582                mNumRows = (sizeSecondary + mMarginSecondary)
583                    / (mItemLengthSecondaryRequested + mMarginSecondary);
584            } else if (mItemLengthSecondaryRequested == 0) {
585                mNumRows = mNumRowsRequested;
586                mItemLengthSecondary = (sizeSecondary - paddingSecondary - mMarginSecondary
587                        * (mNumRows - 1)) / mNumRows;
588            } else {
589                mNumRows = mNumRowsRequested;
590                mItemLengthSecondary = mItemLengthSecondaryRequested;
591            }
592            measuredSizeSecondary = sizeSecondary;
593            if (modeSecondary == MeasureSpec.AT_MOST) {
594                int childrenSize = mItemLengthSecondary * mNumRows + mMarginSecondary
595                    * (mNumRows - 1) + paddingSecondary;
596                if (childrenSize < measuredSizeSecondary) {
597                    measuredSizeSecondary = childrenSize;
598                }
599            }
600            break;
601        default:
602            throw new IllegalStateException("wrong spec");
603        }
604        if (mOrientation == HORIZONTAL) {
605            result[0] = sizePrimary;
606            result[1] = measuredSizeSecondary;
607        } else {
608            result[0] = measuredSizeSecondary;
609            result[1] = sizePrimary;
610        }
611        if (DEBUG) {
612            Log.v(getTag(), "onMeasure result " + result[0] + ", " + result[1]
613                    + " mItemLengthSecondary " + mItemLengthSecondary + " mNumRows " + mNumRows);
614        }
615    }
616
617    private void measureChild(View child) {
618        final ViewGroup.LayoutParams lp = child.getLayoutParams();
619
620        int widthSpec, heightSpec;
621        if (mOrientation == HORIZONTAL) {
622            if (lp.width >= 0) {
623                widthSpec = MeasureSpec.makeMeasureSpec(lp.width, MeasureSpec.EXACTLY);
624            } else {
625                widthSpec = MeasureSpec.makeMeasureSpec(0, MeasureSpec.UNSPECIFIED);
626            }
627            heightSpec = MeasureSpec.makeMeasureSpec(mItemLengthSecondary, MeasureSpec.EXACTLY);
628        } else {
629            if (lp.height >= 0) {
630                heightSpec = MeasureSpec.makeMeasureSpec(lp.height, MeasureSpec.EXACTLY);
631            } else {
632                heightSpec = MeasureSpec.makeMeasureSpec(0, MeasureSpec.UNSPECIFIED);
633            }
634            widthSpec = MeasureSpec.makeMeasureSpec(mItemLengthSecondary, MeasureSpec.EXACTLY);
635        }
636
637        child.measure(widthSpec, heightSpec);
638    }
639
640    private StaggeredGrid.Provider mGridProvider = new StaggeredGrid.Provider() {
641
642        @Override
643        public int getCount() {
644            return mAdapter.getItemCount();
645        }
646
647        @Override
648        public void createItem(int index, int rowIndex, boolean append) {
649            View v = getViewForPosition(index);
650            if (mFirstVisiblePos >= 0) {
651                // when StaggeredGrid append or prepend item, we must guarantee
652                // that sibling item has created views already.
653                if (append && index != mLastVisiblePos + 1) {
654                    throw new RuntimeException();
655                } else if (!append && index != mFirstVisiblePos - 1) {
656                    throw new RuntimeException();
657                }
658            }
659
660            if (append) {
661                addView(v);
662            } else {
663                addView(v, 0);
664            }
665
666            measureChild(v);
667
668            int length = getMeasuredLengthPrimary(v);
669            int start, end;
670            if (append) {
671                start = mRows[rowIndex].high;
672                if (start != mRows[rowIndex].low) {
673                    // if there are existing item in the row,  add margin between
674                    start += mMarginPrimary;
675                }
676                end = start + length;
677                mRows[rowIndex].high = end;
678            } else {
679                end = mRows[rowIndex].low;
680                if (end != mRows[rowIndex].high) {
681                    end -= mMarginPrimary;
682                }
683                start = end - length;
684                mRows[rowIndex].low = start;
685            }
686            if (mFirstVisiblePos < 0) {
687                mFirstVisiblePos = mLastVisiblePos = index;
688            } else {
689                if (append) {
690                    mLastVisiblePos++;
691                } else {
692                    mFirstVisiblePos--;
693                }
694            }
695            int startSecondary = rowIndex * (mItemLengthSecondary + mMarginSecondary);
696            layoutChild(v, start - mScrollOffsetPrimary, end - mScrollOffsetPrimary,
697                    startSecondary - mScrollOffsetSecondary);
698            if (DEBUG) {
699                Log.d(getTag(), "addView " + index + " " + v);
700            }
701            updateScrollMin();
702            updateScrollMax();
703        }
704    };
705
706    private void layoutChild(View v, int start, int end, int startSecondary) {
707        if (mOrientation == HORIZONTAL) {
708            v.layout(start, startSecondary, end, startSecondary + mItemLengthSecondary);
709        } else {
710            v.layout(startSecondary, start, startSecondary + mItemLengthSecondary, end);
711        }
712        updateChildAlignments(v);
713    }
714
715    private void updateChildAlignments(View v) {
716        GridLayoutManagerChildTag tag = getViewTag(v);
717        tag.setAlignX(mItemAlignment.horizontal.getAlignmentPosition(v, tag));
718        tag.setAlignY(mItemAlignment.vertical.getAlignmentPosition(v, tag));
719    }
720
721    private void updateChildAlignments() {
722        for (int i = 0, c = getChildCount(); i < c; i++) {
723            updateChildAlignments(getChildAt(i));
724        }
725    }
726
727    /**
728     * append invisible view of saved location
729     */
730    private void appendViewWithSavedLocation() {
731        int index = mLastVisiblePos + 1;
732        mGridProvider.createItem(index, mGrid.getLocation(index).row, true);
733    }
734
735    /**
736     * prepend invisible view of saved location
737     */
738    private void prependViewWithSavedLocation() {
739        int index = mFirstVisiblePos - 1;
740        mGridProvider.createItem(index, mGrid.getLocation(index).row, false);
741    }
742
743    private boolean needsAppendVisibleItem() {
744        if (mLastVisiblePos < mFocusPosition) {
745            return true;
746        }
747        int right = mScrollOffsetPrimary + mSizePrimary;
748        for (int i = 0; i < mNumRows; i++) {
749            if (mRows[i].low == mRows[i].high) {
750                if (mRows[i].high < right) {
751                    return true;
752                }
753            } else if (mRows[i].high < right - mMarginPrimary) {
754                return true;
755            }
756        }
757        return false;
758    }
759
760    private boolean needsPrependVisibleItem() {
761        if (mFirstVisiblePos > mFocusPosition) {
762            return true;
763        }
764        for (int i = 0; i < mNumRows; i++) {
765            if (mRows[i].low == mRows[i].high) {
766                if (mRows[i].low > mScrollOffsetPrimary) {
767                    return true;
768                }
769            } else if (mRows[i].low - mMarginPrimary > mScrollOffsetPrimary) {
770                return true;
771            }
772        }
773        return false;
774    }
775
776    // Append one column if possible and return true if reach end.
777    private boolean appendOneVisibleItem() {
778        if (mLastVisiblePos >= 0 && mLastVisiblePos < mGrid.getLastIndex()) {
779            appendViewWithSavedLocation();
780        } else if (mLastVisiblePos < mAdapter.getItemCount() - 1) {
781            mGrid.appendItems(mScrollOffsetPrimary + mSizePrimary);
782        } else {
783            return true;
784        }
785        return false;
786    }
787
788    private void appendVisibleItems() {
789        while (needsAppendVisibleItem()) {
790            if (appendOneVisibleItem()) {
791                break;
792            }
793        }
794    }
795
796    // Prepend one column if possible and return true if reach end.
797    private boolean prependOneVisibleItem() {
798        if (mFirstVisiblePos > 0) {
799            if (mFirstVisiblePos > mGrid.getFirstIndex()) {
800                prependViewWithSavedLocation();
801            } else {
802                mGrid.prependItems(mScrollOffsetPrimary);
803            }
804        } else {
805            return true;
806        }
807        return false;
808    }
809
810    private void prependVisibleItems() {
811        while (needsPrependVisibleItem()) {
812            if (prependOneVisibleItem()) {
813                break;
814            }
815        }
816    }
817
818    // TODO: use removeAndRecycleViewAt() once we stop using tags.
819    private void removeChildAt(int position) {
820        View v = getViewByPosition(position);
821        if (v != null) {
822            if (DEBUG) {
823                Log.d(getTag(), "detachAndScrape " + position);
824            }
825            getViewTag(v).detach();
826            removeAndRecycleViewAt(getIndexByPosition(position), mRecycler);
827        }
828    }
829
830    private void removeInvisibleViewsAtEnd() {
831        boolean update = false;
832        while(mLastVisiblePos > mFirstVisiblePos && mLastVisiblePos > mFocusPosition) {
833            View view = getViewByPosition(mLastVisiblePos);
834            if (getViewMin(view) > mSizePrimary) {
835                removeChildAt(mLastVisiblePos);
836                mLastVisiblePos--;
837                update = true;
838            } else {
839                break;
840            }
841        }
842        if (update) {
843            updateRowsMinMax();
844        }
845    }
846
847    private void removeInvisibleViewsAtFront() {
848        boolean update = false;
849        while(mLastVisiblePos > mFirstVisiblePos && mFirstVisiblePos < mFocusPosition) {
850            View view = getViewByPosition(mFirstVisiblePos);
851            if (getViewMax(view) < 0) {
852                removeChildAt(mFirstVisiblePos);
853                mFirstVisiblePos++;
854                update = true;
855            } else {
856                break;
857            }
858        }
859        if (update) {
860            updateRowsMinMax();
861        }
862    }
863
864    private void updateRowsMinMax() {
865        if (mFirstVisiblePos < 0) {
866            return;
867        }
868        for (int i = 0; i < mNumRows; i++) {
869            mRows[i].low = Integer.MAX_VALUE;
870            mRows[i].high = Integer.MIN_VALUE;
871        }
872        for (int i = mFirstVisiblePos; i <= mLastVisiblePos; i++) {
873            View view = getViewByPosition(i);
874            int row = mGrid.getLocation(i).row;
875            int low = getViewMin(view) + mScrollOffsetPrimary;
876            if (low < mRows[row].low) {
877                mRows[row].low = low;
878            }
879            int high = getViewMax(view) + mScrollOffsetPrimary;
880            if (high > mRows[row].high) {
881                mRows[row].high = high;
882            }
883        }
884    }
885
886    /**
887     * Relayout and re-positioning child for a possible new size and/or a new
888     * start.
889     *
890     * @param view View to measure and layout.
891     * @param start New start of the view or Integer.MIN_VALUE for not change.
892     * @return New start of next view.
893     */
894    private int updateChildView(View view, int start, int startSecondary) {
895        if (start == Integer.MIN_VALUE) {
896            start = getViewMin(view);
897        }
898        int end;
899        if (mOrientation == HORIZONTAL) {
900            if (view.isLayoutRequested() || view.getMeasuredHeight() != mItemLengthSecondary) {
901                measureChild(view);
902            }
903            end = start + view.getMeasuredWidth();
904        } else {
905            if (view.isLayoutRequested() || view.getMeasuredWidth() != mItemLengthSecondary) {
906                measureChild(view);
907            }
908            end = start + view.getMeasuredHeight();
909        }
910
911        layoutChild(view, start, end, startSecondary);
912        return end + mMarginPrimary;
913    }
914
915    // create a temporary structure that remembers visible items from left to
916    // right on each row
917    private ArrayList<Integer>[] buildRows() {
918        ArrayList<Integer>[] rows = new ArrayList[mNumRows];
919        for (int i = 0; i < mNumRows; i++) {
920            rows[i] = new ArrayList();
921        }
922        if (mFirstVisiblePos >= 0) {
923            for (int i = mFirstVisiblePos; i <= mLastVisiblePos; i++) {
924                rows[mGrid.getLocation(i).row].add(i);
925            }
926        }
927        return rows;
928    }
929
930    // Fast layout when there is no structure change, adapter change, etc.
931    protected void fastRelayout() {
932        initScrollController();
933
934        ArrayList<Integer>[] rows = buildRows();
935
936        // relayout and repositioning views on each row
937        for (int i = 0; i < mNumRows; i++) {
938            ArrayList<Integer> row = rows[i];
939            int start = Integer.MIN_VALUE;
940            int startSecondary =
941                i * (mItemLengthSecondary + mMarginSecondary) - mScrollOffsetSecondary;
942            for (int j = 0, size = row.size(); j < size; j++) {
943                int position = row.get(j);
944                start = updateChildView(getViewByPosition(position), start, startSecondary);
945            }
946        }
947
948        appendVisibleItems();
949        prependVisibleItems();
950
951        updateRowsMinMax();
952        updateScrollMin();
953        updateScrollMax();
954
955        View focusView = getViewByPosition(mFocusPosition == NO_POSITION ? 0 : mFocusPosition);
956        if (focusView != null) {
957            scrollToView(focusView, false);
958        }
959    }
960
961    // Lays out items based on the current scroll position
962    @Override
963    public void layoutChildren(RecyclerView.Adapter adapter, RecyclerView.Recycler recycler,
964            boolean structureChanged) {
965        if (DEBUG) {
966            Log.v(getTag(), "layoutChildren start numRows " + mNumRows + " mScrollOffsetSecondary "
967                    + mScrollOffsetSecondary + " mScrollOffsetPrimary " + mScrollOffsetPrimary
968                    + " structureChanged " + structureChanged
969                    + " mForceFullLayout " + mForceFullLayout);
970            Log.v(getTag(), "width " + getWidth() + " height " + getHeight());
971        }
972
973        if (mNumRows == 0) {
974            // haven't done measure yet
975            return;
976        }
977        final int itemCount = adapter.getItemCount();
978        if (itemCount < 0) {
979            return;
980        }
981
982        mInLayout = true;
983
984        // Track the old focus view so we can adjust our system scroll position
985        // so that any scroll animations happening now will remain valid.
986        int delta = 0, deltaSecondary = 0;
987        if (mFocusPosition != NO_POSITION) {
988            View focusView = getViewByPosition(mFocusPosition);
989            if (focusView != null) {
990                delta = mWindowAlignment.mainAxis().getSystemScrollPos(
991                        getViewCenter(focusView) + mScrollOffsetPrimary) - mScrollOffsetPrimary;
992                deltaSecondary =
993                    mWindowAlignment.secondAxis().getSystemScrollPos(
994                            getViewCenterSecondary(focusView) + mScrollOffsetSecondary)
995                    - mScrollOffsetSecondary;
996            }
997        }
998
999        boolean hasDoneFirstLayout = hasDoneFirstLayout();
1000        if (!structureChanged && !mForceFullLayout && hasDoneFirstLayout) {
1001            fastRelayout();
1002        } else {
1003            boolean hadFocus = mBaseListView.hasFocus();
1004
1005            int newFocusPosition = init(adapter, recycler, mFocusPosition);
1006            if (DEBUG) {
1007                Log.v(getTag(), "mFocusPosition " + mFocusPosition + " newFocusPosition "
1008                    + newFocusPosition);
1009            }
1010
1011            // depending on result of init(), either recreating everything
1012            // or try to reuse the row start positions near mFocusPosition
1013            if (mGrid.getSize() == 0) {
1014                // this is a fresh creating all items, starting from
1015                // mFocusPosition with a estimated row index.
1016                mGrid.setStart(newFocusPosition, StaggeredGrid.START_DEFAULT);
1017
1018                // Can't track the old focus view
1019                delta = deltaSecondary = 0;
1020
1021            } else {
1022                // mGrid remembers Locations for the column that
1023                // contains mFocusePosition and also mRows remembers start
1024                // positions of each row.
1025                // Manually re-create child views for that column
1026                int firstIndex = mGrid.getFirstIndex();
1027                int lastIndex = mGrid.getLastIndex();
1028                for (int i = firstIndex; i <= lastIndex; i++) {
1029                    mGridProvider.createItem(i, mGrid.getLocation(i).row, true);
1030                }
1031            }
1032            // add visible views at end until reach the end of window
1033            appendVisibleItems();
1034            // add visible views at front until reach the start of window
1035            prependVisibleItems();
1036            // multiple rounds: scrollToView of first round may drag first/last child into
1037            // "visible window" and we update scrollMin/scrollMax then run second scrollToView
1038            int oldFirstVisible;
1039            int oldLastVisible;
1040            do {
1041                oldFirstVisible = mFirstVisiblePos;
1042                oldLastVisible = mLastVisiblePos;
1043                View focusView = getViewByPosition(newFocusPosition);
1044                // we need force to initialize the child view's position
1045                scrollToView(focusView, false);
1046                if (focusView != null && hadFocus) {
1047                    focusView.requestFocus();
1048                }
1049                appendVisibleItems();
1050                prependVisibleItems();
1051                removeInvisibleViewsAtFront();
1052                removeInvisibleViewsAtEnd();
1053            } while (mFirstVisiblePos != oldFirstVisible || mLastVisiblePos != oldLastVisible);
1054        }
1055        mForceFullLayout = false;
1056
1057        scrollDirectionPrimary(-delta);
1058        scrollDirectionSecondary(-deltaSecondary);
1059        appendVisibleItems();
1060        prependVisibleItems();
1061        removeInvisibleViewsAtFront();
1062        removeInvisibleViewsAtEnd();
1063
1064        if (DEBUG) {
1065            StringWriter sw = new StringWriter();
1066            PrintWriter pw = new PrintWriter(sw);
1067            mGrid.debugPrint(pw);
1068            Log.d(getTag(), sw.toString());
1069        }
1070
1071        removeAndRecycleScrap(recycler);
1072        attemptAnimateLayoutChild();
1073
1074        if (!hasDoneFirstLayout) {
1075            dispatchChildSelected();
1076        }
1077        mInLayout = false;
1078        if (DEBUG) Log.v(getTag(), "layoutChildren end");
1079    }
1080
1081    private void offsetChildrenSecondary(int increment) {
1082        final int childCount = getChildCount();
1083        if (mOrientation == HORIZONTAL) {
1084            for (int i = 0; i < childCount; i++) {
1085                getChildAt(i).offsetTopAndBottom(increment);
1086            }
1087        } else {
1088            for (int i = 0; i < childCount; i++) {
1089                getChildAt(i).offsetLeftAndRight(increment);
1090            }
1091        }
1092        mScrollOffsetSecondary -= increment;
1093    }
1094
1095    private void offsetChildrenPrimary(int increment) {
1096        final int childCount = getChildCount();
1097        if (mOrientation == VERTICAL) {
1098            for (int i = 0; i < childCount; i++) {
1099                getChildAt(i).offsetTopAndBottom(increment);
1100            }
1101        } else {
1102            for (int i = 0; i < childCount; i++) {
1103                getChildAt(i).offsetLeftAndRight(increment);
1104            }
1105        }
1106        mScrollOffsetPrimary -= increment;
1107    }
1108
1109    @Override
1110    public int scrollHorizontallyBy(int dx, Adapter adapter, Recycler recycler) {
1111        if (DEBUG) Log.v(TAG, "scrollHorizontallyBy " + dx);
1112
1113        if (mOrientation == HORIZONTAL) {
1114            return scrollDirectionPrimary(dx);
1115        } else {
1116            return scrollDirectionSecondary(dx);
1117        }
1118    }
1119
1120    @Override
1121    public int scrollVerticallyBy(int dy, Adapter adapter, Recycler recycler) {
1122        if (DEBUG) Log.v(TAG, "scrollVerticallyBy " + dy);
1123        if (mOrientation == VERTICAL) {
1124            return scrollDirectionPrimary(dy);
1125        } else {
1126            return scrollDirectionSecondary(dy);
1127        }
1128    }
1129
1130    // scroll in main direction may add/prune views
1131    private int scrollDirectionPrimary(int da) {
1132        offsetChildrenPrimary(-da);
1133        if (mInLayout) {
1134            return da;
1135        }
1136        if (da > 0) {
1137            appendVisibleItems();
1138            removeInvisibleViewsAtFront();
1139        } else if (da < 0) {
1140            prependVisibleItems();
1141            removeInvisibleViewsAtEnd();
1142        }
1143        attemptAnimateLayoutChild();
1144        mBaseListView.invalidate();
1145        return da;
1146    }
1147
1148    // scroll in second direction will not add/prune views
1149    private int scrollDirectionSecondary(int dy) {
1150        offsetChildrenSecondary(-dy);
1151        mBaseListView.invalidate();
1152        return dy;
1153    }
1154
1155    private void updateScrollMax() {
1156        if (mLastVisiblePos >= 0 && mLastVisiblePos == mAdapter.getItemCount() - 1) {
1157            int maxEdge = Integer.MIN_VALUE;
1158            for (int i = 0; i < mRows.length; i++) {
1159                if (mRows[i].high > maxEdge) {
1160                    maxEdge = mRows[i].high;
1161                }
1162            }
1163            mWindowAlignment.mainAxis().setMaxEdge(maxEdge);
1164            if (DEBUG) Log.v(getTag(), "updating scroll maxEdge to " + maxEdge);
1165        }
1166    }
1167
1168    private void updateScrollMin() {
1169        if (mFirstVisiblePos == 0) {
1170            int minEdge = Integer.MAX_VALUE;
1171            for (int i = 0; i < mRows.length; i++) {
1172                if (mRows[i].low < minEdge) {
1173                    minEdge = mRows[i].low;
1174                }
1175            }
1176            mWindowAlignment.mainAxis().setMinEdge(minEdge);
1177            if (DEBUG) Log.v(getTag(), "updating scroll minEdge to " + minEdge);
1178        }
1179    }
1180
1181    private void initScrollController() {
1182        mWindowAlignment.horizontal.setSize(getWidth());
1183        mWindowAlignment.horizontal.setPadding(getPaddingLeft(), getPaddingRight());
1184        mWindowAlignment.vertical.setSize(getHeight());
1185        mWindowAlignment.vertical.setPadding(getPaddingTop(), getPaddingBottom());
1186        mSizePrimary = mWindowAlignment.mainAxis().getSize();
1187        mWindowAlignment.mainAxis().invalidateScrollMin();
1188        mWindowAlignment.mainAxis().invalidateScrollMax();
1189
1190        // second axis min/max is determined at initialization, the mainAxis
1191        // min/max is determined later when we scroll to first or last item
1192        mWindowAlignment.secondAxis().setMinEdge(0);
1193        mWindowAlignment.secondAxis().setMaxEdge(mItemLengthSecondary * mNumRows + mMarginSecondary
1194                * (mNumRows - 1));
1195
1196        if (DEBUG) {
1197            Log.v(getTag(), "initScrollController mSizePrimary " + mSizePrimary + " "
1198                + " mItemLengthSecondary " + mItemLengthSecondary + " " + mWindowAlignment);
1199        }
1200    }
1201
1202    public void setSelection(RecyclerView parent, int position) {
1203        setSelection(parent, position, false);
1204    }
1205
1206    public void setSelectionSmooth(RecyclerView parent, int position) {
1207        setSelection(parent, position, true);
1208    }
1209
1210    public int getSelection() {
1211        return mFocusPosition;
1212    }
1213
1214    public void setSelection(RecyclerView parent, int position, boolean smooth) {
1215        if (mFocusPosition == position) {
1216            return;
1217        }
1218        View view = getViewByPosition(position);
1219        if (view != null) {
1220            scrollToView(view, smooth);
1221        } else {
1222            boolean right = position > mFocusPosition;
1223            mFocusPosition = position;
1224            if (smooth) {
1225                if (!hasDoneFirstLayout()) {
1226                    Log.w(getTag(), "setSelectionSmooth should " +
1227                            "not be called before first layout pass");
1228                    return;
1229                }
1230                if (right) {
1231                    appendVisibleItems();
1232                } else {
1233                    prependVisibleItems();
1234                }
1235                view = getViewByPosition(position);
1236                if (view != null) {
1237                    scrollToView(view, smooth);
1238                }
1239            } else {
1240                mForceFullLayout = true;
1241                parent.requestLayout();
1242            }
1243        }
1244    }
1245
1246    @Override
1247    public void onItemsAdded(RecyclerView recyclerView, int positionStart, int itemCount) {
1248        boolean needsLayout = false;
1249        if (itemCount != 0) {
1250            if (mFirstVisiblePos < 0) {
1251                needsLayout = true;
1252            } else if (!(positionStart > mLastVisiblePos + 1 ||
1253                    positionStart + itemCount < mFirstVisiblePos - 1)) {
1254                needsLayout = true;
1255            }
1256        }
1257        if (needsLayout) {
1258            recyclerView.requestLayout();
1259        }
1260    }
1261
1262    @Override
1263    public boolean onRequestChildFocus(RecyclerView parent, View child, View focused) {
1264        if (!mInLayout) {
1265            scrollToView(child, true);
1266        }
1267        return true;
1268    }
1269
1270    @Override
1271    public boolean requestChildRectangleOnScreen(RecyclerView parent, View view, Rect rect,
1272            boolean immediate) {
1273        if (DEBUG) Log.v(getTag(), "requestChildRectangleOnScreen " + view + " " + rect);
1274        return false;
1275    }
1276
1277    int getScrollOffsetX() {
1278        return mOrientation == HORIZONTAL ? mScrollOffsetPrimary : mScrollOffsetSecondary;
1279    }
1280
1281    int getScrollOffsetY() {
1282        return mOrientation == HORIZONTAL ? mScrollOffsetSecondary : mScrollOffsetPrimary;
1283    }
1284
1285    public void getViewSelectedOffsets(View view, int[] offsets) {
1286        int scrollOffsetX = getScrollOffsetX();
1287        int scrollOffsetY = getScrollOffsetY();
1288        int viewCenterX = scrollOffsetX + getViewCenterX(view);
1289        int viewCenterY = scrollOffsetY + getViewCenterY(view);
1290        offsets[0] = mWindowAlignment.horizontal.getSystemScrollPos(viewCenterX) - scrollOffsetX;
1291        offsets[1] = mWindowAlignment.vertical.getSystemScrollPos(viewCenterY) - scrollOffsetY;
1292    }
1293
1294    /**
1295     * Scroll to a given child view and change mFocusPosition.
1296     */
1297    private void scrollToView(View view, boolean smooth) {
1298        int newFocusPosition = getPositionByView(view);
1299        if (mInLayout || newFocusPosition != mFocusPosition) {
1300            mFocusPosition = newFocusPosition;
1301            dispatchChildSelected();
1302        }
1303        if (view == null) {
1304            return;
1305        }
1306        if (!view.hasFocus() && mBaseListView.hasFocus()) {
1307            // transfer focus to the child if it does not have focus yet (e.g. triggered
1308            // by setSelection())
1309            view.requestFocus();
1310        }
1311        int viewCenterY = getScrollOffsetY() + getViewCenterY(view);
1312        int viewCenterX = getScrollOffsetX() + getViewCenterX(view);
1313        if (DEBUG) {
1314            Log.v(getTag(), "scrollToView smooth=" + smooth + " pos=" + mFocusPosition + " "
1315                    + viewCenterX+","+viewCenterY + " " + mWindowAlignment);
1316        }
1317
1318        if (mInLayout || viewCenterX != mWindowAlignment.horizontal.getScrollCenter()
1319                || viewCenterY != mWindowAlignment.vertical.getScrollCenter()) {
1320            mWindowAlignment.horizontal.updateScrollCenter(viewCenterX);
1321            mWindowAlignment.vertical.updateScrollCenter(viewCenterY);
1322            int scrollX = mWindowAlignment.horizontal.getSystemScrollPos();
1323            int scrollY = mWindowAlignment.vertical.getSystemScrollPos();
1324            if (DEBUG) {
1325                Log.v(getTag(), "adjustSystemScrollPos " + scrollX + " " + scrollY + " "
1326                    + mWindowAlignment);
1327            }
1328
1329            scrollX -= getScrollOffsetX();
1330            scrollY -= getScrollOffsetY();
1331
1332            if (DEBUG) Log.v(getTag(), "scrollX " + scrollX + " scrollY " + scrollY);
1333
1334            if (mInLayout) {
1335                if (mOrientation == HORIZONTAL) {
1336                    scrollDirectionPrimary(scrollX);
1337                    scrollDirectionSecondary(scrollY);
1338                } else {
1339                    scrollDirectionPrimary(scrollY);
1340                    scrollDirectionSecondary(scrollX);
1341                }
1342            } else if (smooth) {
1343                mBaseListView.smoothScrollBy(scrollX, scrollY);
1344            } else {
1345                mBaseListView.scrollBy(scrollX, scrollY);
1346            }
1347        }
1348    }
1349
1350    public void setAnimateChildLayout(boolean animateChildLayout) {
1351        mAnimateChildLayout = animateChildLayout;
1352        if (!mAnimateChildLayout) {
1353            for (int i = 0, c = getChildCount(); i < c; i++) {
1354                getViewTag(getChildAt(i)).endAnimate();
1355            }
1356        }
1357    }
1358
1359    private void attemptAnimateLayoutChild() {
1360        for (int i = 0, c = getChildCount(); i < c; i++) {
1361            // TODO: start delay can be staggered
1362            getViewTag(getChildAt(i)).startAnimate(this, 0);
1363        }
1364    }
1365
1366    public boolean isChildLayoutAnimated() {
1367        return mAnimateChildLayout;
1368    }
1369
1370    public void setChildLayoutAnimationInterpolator(Interpolator interpolator) {
1371        mAnimateLayoutChildInterpolator = interpolator;
1372    }
1373
1374    public Interpolator getChildLayoutAnimationInterpolator() {
1375        return mAnimateLayoutChildInterpolator;
1376    }
1377
1378    public void setChildLayoutAnimationDuration(long duration) {
1379        mAnimateLayoutChildDuration = duration;
1380    }
1381
1382    public long getChildLayoutAnimationDuration() {
1383        return mAnimateLayoutChildDuration;
1384    }
1385
1386    private int findImmediateChildIndex(View view) {
1387        while (view != null && view != mBaseListView) {
1388            int index = mBaseListView.indexOfChild(view);
1389            if (index >= 0) {
1390                return index;
1391            }
1392            view = (View) view.getParent();
1393        }
1394        return -1;
1395    }
1396
1397    @Override
1398    public boolean onAddFocusables(RecyclerView recyclerView,
1399            ArrayList<View> views, int direction, int focusableMode) {
1400        // If this viewgroup or one of its children currently has focus then we
1401        // consider our children for focus searching.
1402        // Otherwise, we only want the system to ignore our children and pass
1403        // focus to the viewgroup, which will pass focus on to its children
1404        // appropriately.
1405        if (recyclerView.hasFocus()) {
1406            final int movement = getMovement(direction);
1407            if (movement != PREV_ITEM && movement != NEXT_ITEM) {
1408                // Move on secondary direction uses default addFocusables().
1409                return false;
1410            }
1411            // Get current focus row.
1412            final View focused = recyclerView.findFocus();
1413            final int focusedImmediateChildIndex = findImmediateChildIndex(focused);
1414            final int focusedPos = getPositionByIndex(focusedImmediateChildIndex);
1415            final int focusedRow = mGrid != null ? mGrid.getLocation(focusedPos).row : -1;
1416            // Add focusables within the same row.
1417            final int focusableCount = views.size();
1418            final int descendantFocusability = recyclerView.getDescendantFocusability();
1419            if (focusedRow != -1 &&
1420                    mGrid != null && descendantFocusability != ViewGroup.FOCUS_BLOCK_DESCENDANTS) {
1421                for (int i = 0, count = getChildCount(); i < count; i++) {
1422                    final View child = getChildAt(i);
1423                    if (child.getVisibility() != View.VISIBLE) {
1424                        continue;
1425                    }
1426                    StaggeredGrid.Location loc = mGrid.getLocation(getPositionByIndex(i));
1427                    if (loc != null && loc.row == focusedRow) {
1428                        child.addFocusables(views,  direction, focusableMode);
1429                    }
1430                }
1431            }
1432            // From ViewGroup.addFocusables():
1433            // we add ourselves (if focusable) in all cases except for when we are
1434            // FOCUS_AFTER_DESCENDANTS and there are some descendants focusable.  this is
1435            // to avoid the focus search finding layouts when a more precise search
1436            // among the focusable children would be more interesting.
1437            if (descendantFocusability != ViewGroup.FOCUS_AFTER_DESCENDANTS
1438                    // No focusable descendants
1439                    || (focusableCount == views.size())) {
1440                if (recyclerView.isFocusable()) {
1441                    views.add(recyclerView);
1442                }
1443            }
1444        } else {
1445            if (recyclerView.isFocusable()) {
1446                views.add(recyclerView);
1447            }
1448        }
1449        return true;
1450    }
1451
1452    @Override
1453    public View onFocusSearchFailed(View focused, int direction, Adapter adapter,
1454            Recycler recycler) {
1455        if (DEBUG) Log.v(getTag(), "onFocusSearchFailed direction " + direction);
1456
1457        View view = null;
1458        int movement = getMovement(direction);
1459        final FocusFinder ff = FocusFinder.getInstance();
1460        if (movement == NEXT_ITEM) {
1461            while (view == null && !appendOneVisibleItem()) {
1462                view = ff.findNextFocus(mBaseListView, focused, direction);
1463            }
1464        } else if (movement == PREV_ITEM){
1465            while (view == null && !prependOneVisibleItem()) {
1466                view = ff.findNextFocus(mBaseListView, focused, direction);
1467            }
1468        }
1469        if (view == null) {
1470            // returning the same view to prevent focus lost when scrolling past the end of the list
1471            if (movement == PREV_ITEM) {
1472                view = mFocusOutFront ? null : focused;
1473            } else if (movement == NEXT_ITEM){
1474                view = mFocusOutEnd ? null : focused;
1475            }
1476        }
1477        if (DEBUG) Log.v(getTag(), "returning view " + view);
1478        return view;
1479    }
1480
1481    private final static int PREV_ITEM = 0;
1482    private final static int NEXT_ITEM = 1;
1483    private final static int PREV_ROW = 2;
1484    private final static int NEXT_ROW = 3;
1485
1486    boolean focusSelectedChild(int direction, Rect previouslyFocusedRect) {
1487        View view = getViewByPosition(mFocusPosition);
1488        if (view != null) {
1489            if (!view.requestFocus(direction, previouslyFocusedRect)) {
1490                if (DEBUG) {
1491                    Log.w(getTag(), "failed to request focus on " + view);
1492                }
1493            } else {
1494                return true;
1495            }
1496        }
1497        return false;
1498    }
1499
1500    private int getMovement(int direction) {
1501        int movement = View.FOCUS_LEFT;
1502
1503        if (mOrientation == HORIZONTAL) {
1504            switch(direction) {
1505                case View.FOCUS_LEFT:
1506                    movement = PREV_ITEM;
1507                    break;
1508                case View.FOCUS_RIGHT:
1509                    movement = NEXT_ITEM;
1510                    break;
1511                case View.FOCUS_UP:
1512                    movement = PREV_ROW;
1513                    break;
1514                case View.FOCUS_DOWN:
1515                    movement = NEXT_ROW;
1516                    break;
1517            }
1518         } else if (mOrientation == VERTICAL) {
1519             switch(direction) {
1520                 case View.FOCUS_LEFT:
1521                     movement = PREV_ROW;
1522                     break;
1523                 case View.FOCUS_RIGHT:
1524                     movement = NEXT_ROW;
1525                     break;
1526                 case View.FOCUS_UP:
1527                     movement = PREV_ITEM;
1528                     break;
1529                 case View.FOCUS_DOWN:
1530                     movement = NEXT_ITEM;
1531                     break;
1532             }
1533         }
1534
1535        return movement;
1536    }
1537
1538    @Override
1539    public void onAdapterChanged() {
1540        mGrid = null;
1541        mRows = null;
1542        super.onAdapterChanged();
1543    }
1544}
1545