GridLayoutManager.java revision fa9a61fa061befbbbd49b01ec926a0fe8d61a9a5
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 NO_POSITION;
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 = NO_POSITION;
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                } else {
676                    final int lastRow = mRows.length - 1;
677                    if (lastRow != rowIndex && mRows[lastRow].high != mRows[lastRow].low) {
678                        // if there are existing item in the last row, insert
679                        // the new item after the last item of last row.
680                        start = mRows[lastRow].high + mMarginPrimary;
681                    }
682                }
683                end = start + length;
684                mRows[rowIndex].high = end;
685            } else {
686                end = mRows[rowIndex].low;
687                if (end != mRows[rowIndex].high) {
688                    end -= mMarginPrimary;
689                } else if (0 != rowIndex && mRows[0].high != mRows[0].low) {
690                    // if there are existing item in the first row, insert
691                    // the new item before the first item of first row.
692                    end = mRows[0].low - mMarginPrimary;
693                }
694                start = end - length;
695                mRows[rowIndex].low = start;
696            }
697            if (mFirstVisiblePos < 0) {
698                mFirstVisiblePos = mLastVisiblePos = index;
699            } else {
700                if (append) {
701                    mLastVisiblePos++;
702                } else {
703                    mFirstVisiblePos--;
704                }
705            }
706            int startSecondary = rowIndex * (mItemLengthSecondary + mMarginSecondary);
707            layoutChild(v, start - mScrollOffsetPrimary, end - mScrollOffsetPrimary,
708                    startSecondary - mScrollOffsetSecondary);
709            if (DEBUG) {
710                Log.d(getTag(), "addView " + index + " " + v);
711            }
712            updateScrollMin();
713            updateScrollMax();
714        }
715    };
716
717    private void layoutChild(View v, int start, int end, int startSecondary) {
718        if (mOrientation == HORIZONTAL) {
719            v.layout(start, startSecondary, end, startSecondary + mItemLengthSecondary);
720        } else {
721            v.layout(startSecondary, start, startSecondary + mItemLengthSecondary, end);
722        }
723        updateChildAlignments(v);
724    }
725
726    private void updateChildAlignments(View v) {
727        GridLayoutManagerChildTag tag = getViewTag(v);
728        tag.setAlignX(mItemAlignment.horizontal.getAlignmentPosition(v, tag));
729        tag.setAlignY(mItemAlignment.vertical.getAlignmentPosition(v, tag));
730    }
731
732    private void updateChildAlignments() {
733        for (int i = 0, c = getChildCount(); i < c; i++) {
734            updateChildAlignments(getChildAt(i));
735        }
736    }
737
738    /**
739     * append invisible view of saved location
740     */
741    private void appendViewWithSavedLocation() {
742        int index = mLastVisiblePos + 1;
743        mGridProvider.createItem(index, mGrid.getLocation(index).row, true);
744    }
745
746    /**
747     * prepend invisible view of saved location
748     */
749    private void prependViewWithSavedLocation() {
750        int index = mFirstVisiblePos - 1;
751        mGridProvider.createItem(index, mGrid.getLocation(index).row, false);
752    }
753
754    private boolean needsAppendVisibleItem() {
755        if (mLastVisiblePos < mFocusPosition) {
756            return true;
757        }
758        int right = mScrollOffsetPrimary + mSizePrimary;
759        for (int i = 0; i < mNumRows; i++) {
760            if (mRows[i].low == mRows[i].high) {
761                if (mRows[i].high < right) {
762                    return true;
763                }
764            } else if (mRows[i].high < right - mMarginPrimary) {
765                return true;
766            }
767        }
768        return false;
769    }
770
771    private boolean needsPrependVisibleItem() {
772        if (mFirstVisiblePos > mFocusPosition) {
773            return true;
774        }
775        for (int i = 0; i < mNumRows; i++) {
776            if (mRows[i].low == mRows[i].high) {
777                if (mRows[i].low > mScrollOffsetPrimary) {
778                    return true;
779                }
780            } else if (mRows[i].low - mMarginPrimary > mScrollOffsetPrimary) {
781                return true;
782            }
783        }
784        return false;
785    }
786
787    // Append one column if possible and return true if reach end.
788    private boolean appendOneVisibleItem() {
789        if (mLastVisiblePos >= 0 && mLastVisiblePos < mGrid.getLastIndex()) {
790            appendViewWithSavedLocation();
791        } else if (mLastVisiblePos < mAdapter.getItemCount() - 1) {
792            mGrid.appendItems(mScrollOffsetPrimary + mSizePrimary);
793        } else {
794            return true;
795        }
796        return false;
797    }
798
799    private void appendVisibleItems() {
800        while (needsAppendVisibleItem()) {
801            if (appendOneVisibleItem()) {
802                break;
803            }
804        }
805    }
806
807    // Prepend one column if possible and return true if reach end.
808    private boolean prependOneVisibleItem() {
809        if (mFirstVisiblePos > 0) {
810            if (mFirstVisiblePos > mGrid.getFirstIndex()) {
811                prependViewWithSavedLocation();
812            } else {
813                mGrid.prependItems(mScrollOffsetPrimary);
814            }
815        } else {
816            return true;
817        }
818        return false;
819    }
820
821    private void prependVisibleItems() {
822        while (needsPrependVisibleItem()) {
823            if (prependOneVisibleItem()) {
824                break;
825            }
826        }
827    }
828
829    // TODO: use removeAndRecycleViewAt() once we stop using tags.
830    private void removeChildAt(int position) {
831        View v = getViewByPosition(position);
832        if (v != null) {
833            if (DEBUG) {
834                Log.d(getTag(), "detachAndScrape " + position);
835            }
836            getViewTag(v).detach();
837            removeAndRecycleViewAt(getIndexByPosition(position), mRecycler);
838        }
839    }
840
841    private void removeInvisibleViewsAtEnd() {
842        boolean update = false;
843        while(mLastVisiblePos > mFirstVisiblePos && mLastVisiblePos > mFocusPosition) {
844            View view = getViewByPosition(mLastVisiblePos);
845            if (getViewMin(view) > mSizePrimary) {
846                removeChildAt(mLastVisiblePos);
847                mLastVisiblePos--;
848                update = true;
849            } else {
850                break;
851            }
852        }
853        if (update) {
854            updateRowsMinMax();
855        }
856    }
857
858    private void removeInvisibleViewsAtFront() {
859        boolean update = false;
860        while(mLastVisiblePos > mFirstVisiblePos && mFirstVisiblePos < mFocusPosition) {
861            View view = getViewByPosition(mFirstVisiblePos);
862            if (getViewMax(view) < 0) {
863                removeChildAt(mFirstVisiblePos);
864                mFirstVisiblePos++;
865                update = true;
866            } else {
867                break;
868            }
869        }
870        if (update) {
871            updateRowsMinMax();
872        }
873    }
874
875    private void updateRowsMinMax() {
876        if (mFirstVisiblePos < 0) {
877            return;
878        }
879        for (int i = 0; i < mNumRows; i++) {
880            mRows[i].low = Integer.MAX_VALUE;
881            mRows[i].high = Integer.MIN_VALUE;
882        }
883        for (int i = mFirstVisiblePos; i <= mLastVisiblePos; i++) {
884            View view = getViewByPosition(i);
885            int row = mGrid.getLocation(i).row;
886            int low = getViewMin(view) + mScrollOffsetPrimary;
887            if (low < mRows[row].low) {
888                mRows[row].low = low;
889            }
890            int high = getViewMax(view) + mScrollOffsetPrimary;
891            if (high > mRows[row].high) {
892                mRows[row].high = high;
893            }
894        }
895    }
896
897    /**
898     * Relayout and re-positioning child for a possible new size and/or a new
899     * start.
900     *
901     * @param view View to measure and layout.
902     * @param start New start of the view or Integer.MIN_VALUE for not change.
903     * @return New start of next view.
904     */
905    private int updateChildView(View view, int start, int startSecondary) {
906        if (start == Integer.MIN_VALUE) {
907            start = getViewMin(view);
908        }
909        int end;
910        if (mOrientation == HORIZONTAL) {
911            if (view.isLayoutRequested() || view.getMeasuredHeight() != mItemLengthSecondary) {
912                measureChild(view);
913            }
914            end = start + view.getMeasuredWidth();
915        } else {
916            if (view.isLayoutRequested() || view.getMeasuredWidth() != mItemLengthSecondary) {
917                measureChild(view);
918            }
919            end = start + view.getMeasuredHeight();
920        }
921
922        layoutChild(view, start, end, startSecondary);
923        return end + mMarginPrimary;
924    }
925
926    // create a temporary structure that remembers visible items from left to
927    // right on each row
928    private ArrayList<Integer>[] buildRows() {
929        ArrayList<Integer>[] rows = new ArrayList[mNumRows];
930        for (int i = 0; i < mNumRows; i++) {
931            rows[i] = new ArrayList();
932        }
933        if (mFirstVisiblePos >= 0) {
934            for (int i = mFirstVisiblePos; i <= mLastVisiblePos; i++) {
935                rows[mGrid.getLocation(i).row].add(i);
936            }
937        }
938        return rows;
939    }
940
941    // Fast layout when there is no structure change, adapter change, etc.
942    protected void fastRelayout() {
943        initScrollController();
944
945        ArrayList<Integer>[] rows = buildRows();
946
947        // relayout and repositioning views on each row
948        for (int i = 0; i < mNumRows; i++) {
949            ArrayList<Integer> row = rows[i];
950            int start = Integer.MIN_VALUE;
951            int startSecondary =
952                i * (mItemLengthSecondary + mMarginSecondary) - mScrollOffsetSecondary;
953            for (int j = 0, size = row.size(); j < size; j++) {
954                int position = row.get(j);
955                start = updateChildView(getViewByPosition(position), start, startSecondary);
956            }
957        }
958
959        appendVisibleItems();
960        prependVisibleItems();
961
962        updateRowsMinMax();
963        updateScrollMin();
964        updateScrollMax();
965
966        View focusView = getViewByPosition(mFocusPosition == NO_POSITION ? 0 : mFocusPosition);
967        if (focusView != null) {
968            scrollToView(focusView, false);
969        }
970    }
971
972    // Lays out items based on the current scroll position
973    @Override
974    public void layoutChildren(RecyclerView.Adapter adapter, RecyclerView.Recycler recycler,
975            boolean structureChanged) {
976        if (DEBUG) {
977            Log.v(getTag(), "layoutChildren start numRows " + mNumRows + " mScrollOffsetSecondary "
978                    + mScrollOffsetSecondary + " mScrollOffsetPrimary " + mScrollOffsetPrimary
979                    + " structureChanged " + structureChanged
980                    + " mForceFullLayout " + mForceFullLayout);
981            Log.v(getTag(), "width " + getWidth() + " height " + getHeight());
982        }
983
984        if (mNumRows == 0) {
985            // haven't done measure yet
986            return;
987        }
988        final int itemCount = adapter.getItemCount();
989        if (itemCount < 0) {
990            return;
991        }
992
993        mInLayout = true;
994
995        // Track the old focus view so we can adjust our system scroll position
996        // so that any scroll animations happening now will remain valid.
997        int delta = 0, deltaSecondary = 0;
998        if (mFocusPosition != NO_POSITION) {
999            View focusView = getViewByPosition(mFocusPosition);
1000            if (focusView != null) {
1001                delta = mWindowAlignment.mainAxis().getSystemScrollPos(
1002                        getViewCenter(focusView) + mScrollOffsetPrimary) - mScrollOffsetPrimary;
1003                deltaSecondary =
1004                    mWindowAlignment.secondAxis().getSystemScrollPos(
1005                            getViewCenterSecondary(focusView) + mScrollOffsetSecondary)
1006                    - mScrollOffsetSecondary;
1007            }
1008        }
1009
1010        boolean hasDoneFirstLayout = hasDoneFirstLayout();
1011        if (!structureChanged && !mForceFullLayout && hasDoneFirstLayout) {
1012            fastRelayout();
1013        } else {
1014            boolean hadFocus = mBaseListView.hasFocus();
1015
1016            int newFocusPosition = init(adapter, recycler, mFocusPosition);
1017            if (DEBUG) {
1018                Log.v(getTag(), "mFocusPosition " + mFocusPosition + " newFocusPosition "
1019                    + newFocusPosition);
1020            }
1021
1022            // depending on result of init(), either recreating everything
1023            // or try to reuse the row start positions near mFocusPosition
1024            if (mGrid.getSize() == 0) {
1025                // this is a fresh creating all items, starting from
1026                // mFocusPosition with a estimated row index.
1027                mGrid.setStart(newFocusPosition, StaggeredGrid.START_DEFAULT);
1028
1029                // Can't track the old focus view
1030                delta = deltaSecondary = 0;
1031
1032            } else {
1033                // mGrid remembers Locations for the column that
1034                // contains mFocusePosition and also mRows remembers start
1035                // positions of each row.
1036                // Manually re-create child views for that column
1037                int firstIndex = mGrid.getFirstIndex();
1038                int lastIndex = mGrid.getLastIndex();
1039                for (int i = firstIndex; i <= lastIndex; i++) {
1040                    mGridProvider.createItem(i, mGrid.getLocation(i).row, true);
1041                }
1042            }
1043            // add visible views at end until reach the end of window
1044            appendVisibleItems();
1045            // add visible views at front until reach the start of window
1046            prependVisibleItems();
1047            // multiple rounds: scrollToView of first round may drag first/last child into
1048            // "visible window" and we update scrollMin/scrollMax then run second scrollToView
1049            int oldFirstVisible;
1050            int oldLastVisible;
1051            do {
1052                oldFirstVisible = mFirstVisiblePos;
1053                oldLastVisible = mLastVisiblePos;
1054                View focusView = getViewByPosition(newFocusPosition);
1055                // we need force to initialize the child view's position
1056                scrollToView(focusView, false);
1057                if (focusView != null && hadFocus) {
1058                    focusView.requestFocus();
1059                }
1060                appendVisibleItems();
1061                prependVisibleItems();
1062                removeInvisibleViewsAtFront();
1063                removeInvisibleViewsAtEnd();
1064            } while (mFirstVisiblePos != oldFirstVisible || mLastVisiblePos != oldLastVisible);
1065        }
1066        mForceFullLayout = false;
1067
1068        scrollDirectionPrimary(-delta);
1069        scrollDirectionSecondary(-deltaSecondary);
1070        appendVisibleItems();
1071        prependVisibleItems();
1072        removeInvisibleViewsAtFront();
1073        removeInvisibleViewsAtEnd();
1074
1075        if (DEBUG) {
1076            StringWriter sw = new StringWriter();
1077            PrintWriter pw = new PrintWriter(sw);
1078            mGrid.debugPrint(pw);
1079            Log.d(getTag(), sw.toString());
1080        }
1081
1082        removeAndRecycleScrap(recycler);
1083        attemptAnimateLayoutChild();
1084
1085        if (!hasDoneFirstLayout) {
1086            dispatchChildSelected();
1087        }
1088        mInLayout = false;
1089        if (DEBUG) Log.v(getTag(), "layoutChildren end");
1090    }
1091
1092    private void offsetChildrenSecondary(int increment) {
1093        final int childCount = getChildCount();
1094        if (mOrientation == HORIZONTAL) {
1095            for (int i = 0; i < childCount; i++) {
1096                getChildAt(i).offsetTopAndBottom(increment);
1097            }
1098        } else {
1099            for (int i = 0; i < childCount; i++) {
1100                getChildAt(i).offsetLeftAndRight(increment);
1101            }
1102        }
1103        mScrollOffsetSecondary -= increment;
1104    }
1105
1106    private void offsetChildrenPrimary(int increment) {
1107        final int childCount = getChildCount();
1108        if (mOrientation == VERTICAL) {
1109            for (int i = 0; i < childCount; i++) {
1110                getChildAt(i).offsetTopAndBottom(increment);
1111            }
1112        } else {
1113            for (int i = 0; i < childCount; i++) {
1114                getChildAt(i).offsetLeftAndRight(increment);
1115            }
1116        }
1117        mScrollOffsetPrimary -= increment;
1118    }
1119
1120    @Override
1121    public int scrollHorizontallyBy(int dx, Adapter adapter, Recycler recycler) {
1122        if (DEBUG) Log.v(TAG, "scrollHorizontallyBy " + dx);
1123
1124        if (mOrientation == HORIZONTAL) {
1125            return scrollDirectionPrimary(dx);
1126        } else {
1127            return scrollDirectionSecondary(dx);
1128        }
1129    }
1130
1131    @Override
1132    public int scrollVerticallyBy(int dy, Adapter adapter, Recycler recycler) {
1133        if (DEBUG) Log.v(TAG, "scrollVerticallyBy " + dy);
1134        if (mOrientation == VERTICAL) {
1135            return scrollDirectionPrimary(dy);
1136        } else {
1137            return scrollDirectionSecondary(dy);
1138        }
1139    }
1140
1141    // scroll in main direction may add/prune views
1142    private int scrollDirectionPrimary(int da) {
1143        offsetChildrenPrimary(-da);
1144        if (mInLayout) {
1145            return da;
1146        }
1147        if (da > 0) {
1148            appendVisibleItems();
1149            removeInvisibleViewsAtFront();
1150        } else if (da < 0) {
1151            prependVisibleItems();
1152            removeInvisibleViewsAtEnd();
1153        }
1154        attemptAnimateLayoutChild();
1155        mBaseListView.invalidate();
1156        return da;
1157    }
1158
1159    // scroll in second direction will not add/prune views
1160    private int scrollDirectionSecondary(int dy) {
1161        offsetChildrenSecondary(-dy);
1162        mBaseListView.invalidate();
1163        return dy;
1164    }
1165
1166    private void updateScrollMax() {
1167        if (mLastVisiblePos >= 0 && mLastVisiblePos == mAdapter.getItemCount() - 1) {
1168            int maxEdge = Integer.MIN_VALUE;
1169            for (int i = 0; i < mRows.length; i++) {
1170                if (mRows[i].high > maxEdge) {
1171                    maxEdge = mRows[i].high;
1172                }
1173            }
1174            mWindowAlignment.mainAxis().setMaxEdge(maxEdge);
1175            if (DEBUG) Log.v(getTag(), "updating scroll maxEdge to " + maxEdge);
1176        }
1177    }
1178
1179    private void updateScrollMin() {
1180        if (mFirstVisiblePos == 0) {
1181            int minEdge = Integer.MAX_VALUE;
1182            for (int i = 0; i < mRows.length; i++) {
1183                if (mRows[i].low < minEdge) {
1184                    minEdge = mRows[i].low;
1185                }
1186            }
1187            mWindowAlignment.mainAxis().setMinEdge(minEdge);
1188            if (DEBUG) Log.v(getTag(), "updating scroll minEdge to " + minEdge);
1189        }
1190    }
1191
1192    private void initScrollController() {
1193        mWindowAlignment.horizontal.setSize(getWidth());
1194        mWindowAlignment.horizontal.setPadding(getPaddingLeft(), getPaddingRight());
1195        mWindowAlignment.vertical.setSize(getHeight());
1196        mWindowAlignment.vertical.setPadding(getPaddingTop(), getPaddingBottom());
1197        mSizePrimary = mWindowAlignment.mainAxis().getSize();
1198        mWindowAlignment.mainAxis().invalidateScrollMin();
1199        mWindowAlignment.mainAxis().invalidateScrollMax();
1200
1201        // second axis min/max is determined at initialization, the mainAxis
1202        // min/max is determined later when we scroll to first or last item
1203        mWindowAlignment.secondAxis().setMinEdge(0);
1204        mWindowAlignment.secondAxis().setMaxEdge(mItemLengthSecondary * mNumRows + mMarginSecondary
1205                * (mNumRows - 1));
1206
1207        if (DEBUG) {
1208            Log.v(getTag(), "initScrollController mSizePrimary " + mSizePrimary + " "
1209                + " mItemLengthSecondary " + mItemLengthSecondary + " " + mWindowAlignment);
1210        }
1211    }
1212
1213    public void setSelection(RecyclerView parent, int position) {
1214        setSelection(parent, position, false);
1215    }
1216
1217    public void setSelectionSmooth(RecyclerView parent, int position) {
1218        setSelection(parent, position, true);
1219    }
1220
1221    public int getSelection() {
1222        return mFocusPosition;
1223    }
1224
1225    public void setSelection(RecyclerView parent, int position, boolean smooth) {
1226        if (mFocusPosition == position) {
1227            return;
1228        }
1229        View view = getViewByPosition(position);
1230        if (view != null) {
1231            scrollToView(view, smooth);
1232        } else {
1233            boolean right = position > mFocusPosition;
1234            mFocusPosition = position;
1235            if (smooth) {
1236                if (!hasDoneFirstLayout()) {
1237                    Log.w(getTag(), "setSelectionSmooth should " +
1238                            "not be called before first layout pass");
1239                    return;
1240                }
1241                if (right) {
1242                    appendVisibleItems();
1243                } else {
1244                    prependVisibleItems();
1245                }
1246                view = getViewByPosition(position);
1247                if (view != null) {
1248                    scrollToView(view, smooth);
1249                }
1250            } else {
1251                mForceFullLayout = true;
1252                parent.requestLayout();
1253            }
1254        }
1255    }
1256
1257    @Override
1258    public void onItemsAdded(RecyclerView recyclerView, int positionStart, int itemCount) {
1259        boolean needsLayout = false;
1260        if (itemCount != 0) {
1261            if (mFirstVisiblePos < 0) {
1262                needsLayout = true;
1263            } else if (!(positionStart > mLastVisiblePos + 1 ||
1264                    positionStart + itemCount < mFirstVisiblePos - 1)) {
1265                needsLayout = true;
1266            }
1267        }
1268        if (needsLayout) {
1269            recyclerView.requestLayout();
1270        }
1271    }
1272
1273    @Override
1274    public boolean onRequestChildFocus(RecyclerView parent, View child, View focused) {
1275        if (!mInLayout) {
1276            scrollToView(child, true);
1277        }
1278        return true;
1279    }
1280
1281    @Override
1282    public boolean requestChildRectangleOnScreen(RecyclerView parent, View view, Rect rect,
1283            boolean immediate) {
1284        if (DEBUG) Log.v(getTag(), "requestChildRectangleOnScreen " + view + " " + rect);
1285        return false;
1286    }
1287
1288    int getScrollOffsetX() {
1289        return mOrientation == HORIZONTAL ? mScrollOffsetPrimary : mScrollOffsetSecondary;
1290    }
1291
1292    int getScrollOffsetY() {
1293        return mOrientation == HORIZONTAL ? mScrollOffsetSecondary : mScrollOffsetPrimary;
1294    }
1295
1296    public void getViewSelectedOffsets(View view, int[] offsets) {
1297        int scrollOffsetX = getScrollOffsetX();
1298        int scrollOffsetY = getScrollOffsetY();
1299        int viewCenterX = scrollOffsetX + getViewCenterX(view);
1300        int viewCenterY = scrollOffsetY + getViewCenterY(view);
1301        offsets[0] = mWindowAlignment.horizontal.getSystemScrollPos(viewCenterX) - scrollOffsetX;
1302        offsets[1] = mWindowAlignment.vertical.getSystemScrollPos(viewCenterY) - scrollOffsetY;
1303    }
1304
1305    /**
1306     * Scroll to a given child view and change mFocusPosition.
1307     */
1308    private void scrollToView(View view, boolean smooth) {
1309        int newFocusPosition = getPositionByView(view);
1310        if (mInLayout || newFocusPosition != mFocusPosition) {
1311            mFocusPosition = newFocusPosition;
1312            dispatchChildSelected();
1313        }
1314        if (view == null) {
1315            return;
1316        }
1317        if (!view.hasFocus() && mBaseListView.hasFocus()) {
1318            // transfer focus to the child if it does not have focus yet (e.g. triggered
1319            // by setSelection())
1320            view.requestFocus();
1321        }
1322        int viewCenterY = getScrollOffsetY() + getViewCenterY(view);
1323        int viewCenterX = getScrollOffsetX() + getViewCenterX(view);
1324        if (DEBUG) {
1325            Log.v(getTag(), "scrollToView smooth=" + smooth + " pos=" + mFocusPosition + " "
1326                    + viewCenterX+","+viewCenterY + " " + mWindowAlignment);
1327        }
1328
1329        if (mInLayout || viewCenterX != mWindowAlignment.horizontal.getScrollCenter()
1330                || viewCenterY != mWindowAlignment.vertical.getScrollCenter()) {
1331            mWindowAlignment.horizontal.updateScrollCenter(viewCenterX);
1332            mWindowAlignment.vertical.updateScrollCenter(viewCenterY);
1333            int scrollX = mWindowAlignment.horizontal.getSystemScrollPos();
1334            int scrollY = mWindowAlignment.vertical.getSystemScrollPos();
1335            if (DEBUG) {
1336                Log.v(getTag(), "adjustSystemScrollPos " + scrollX + " " + scrollY + " "
1337                    + mWindowAlignment);
1338            }
1339
1340            scrollX -= getScrollOffsetX();
1341            scrollY -= getScrollOffsetY();
1342
1343            if (DEBUG) Log.v(getTag(), "scrollX " + scrollX + " scrollY " + scrollY);
1344
1345            if (mInLayout) {
1346                if (mOrientation == HORIZONTAL) {
1347                    scrollDirectionPrimary(scrollX);
1348                    scrollDirectionSecondary(scrollY);
1349                } else {
1350                    scrollDirectionPrimary(scrollY);
1351                    scrollDirectionSecondary(scrollX);
1352                }
1353            } else if (smooth) {
1354                mBaseListView.smoothScrollBy(scrollX, scrollY);
1355            } else {
1356                mBaseListView.scrollBy(scrollX, scrollY);
1357            }
1358        }
1359    }
1360
1361    public void setAnimateChildLayout(boolean animateChildLayout) {
1362        mAnimateChildLayout = animateChildLayout;
1363        if (!mAnimateChildLayout) {
1364            for (int i = 0, c = getChildCount(); i < c; i++) {
1365                getViewTag(getChildAt(i)).endAnimate();
1366            }
1367        }
1368    }
1369
1370    private void attemptAnimateLayoutChild() {
1371        for (int i = 0, c = getChildCount(); i < c; i++) {
1372            // TODO: start delay can be staggered
1373            getViewTag(getChildAt(i)).startAnimate(this, 0);
1374        }
1375    }
1376
1377    public boolean isChildLayoutAnimated() {
1378        return mAnimateChildLayout;
1379    }
1380
1381    public void setChildLayoutAnimationInterpolator(Interpolator interpolator) {
1382        mAnimateLayoutChildInterpolator = interpolator;
1383    }
1384
1385    public Interpolator getChildLayoutAnimationInterpolator() {
1386        return mAnimateLayoutChildInterpolator;
1387    }
1388
1389    public void setChildLayoutAnimationDuration(long duration) {
1390        mAnimateLayoutChildDuration = duration;
1391    }
1392
1393    public long getChildLayoutAnimationDuration() {
1394        return mAnimateLayoutChildDuration;
1395    }
1396
1397    private int findImmediateChildIndex(View view) {
1398        while (view != null && view != mBaseListView) {
1399            int index = mBaseListView.indexOfChild(view);
1400            if (index >= 0) {
1401                return index;
1402            }
1403            view = (View) view.getParent();
1404        }
1405        return NO_POSITION;
1406    }
1407
1408    @Override
1409    public boolean onAddFocusables(RecyclerView recyclerView,
1410            ArrayList<View> views, int direction, int focusableMode) {
1411        // If this viewgroup or one of its children currently has focus then we
1412        // consider our children for focus searching.
1413        // Otherwise, we only want the system to ignore our children and pass
1414        // focus to the viewgroup, which will pass focus on to its children
1415        // appropriately.
1416        if (recyclerView.hasFocus()) {
1417            final int movement = getMovement(direction);
1418            if (movement != PREV_ITEM && movement != NEXT_ITEM) {
1419                // Move on secondary direction uses default addFocusables().
1420                return false;
1421            }
1422            // Get current focus row.
1423            final View focused = recyclerView.findFocus();
1424            final int focusedPos = getPositionByIndex(findImmediateChildIndex(focused));
1425            final int focusedRow = mGrid != null && focusedPos != NO_POSITION ?
1426                    mGrid.getLocation(focusedPos).row : NO_POSITION;
1427            // Add focusables within the same row.
1428            final int focusableCount = views.size();
1429            final int descendantFocusability = recyclerView.getDescendantFocusability();
1430            if (mGrid != null && descendantFocusability != ViewGroup.FOCUS_BLOCK_DESCENDANTS) {
1431                for (int i = 0, count = getChildCount(); i < count; i++) {
1432                    final View child = getChildAt(i);
1433                    if (child.getVisibility() != View.VISIBLE) {
1434                        continue;
1435                    }
1436                    StaggeredGrid.Location loc = mGrid.getLocation(getPositionByIndex(i));
1437                    if (focusedRow == NO_POSITION || (loc != null && loc.row == focusedRow)) {
1438                        child.addFocusables(views,  direction, focusableMode);
1439                    }
1440                }
1441            }
1442            // From ViewGroup.addFocusables():
1443            // we add ourselves (if focusable) in all cases except for when we are
1444            // FOCUS_AFTER_DESCENDANTS and there are some descendants focusable.  this is
1445            // to avoid the focus search finding layouts when a more precise search
1446            // among the focusable children would be more interesting.
1447            if (descendantFocusability != ViewGroup.FOCUS_AFTER_DESCENDANTS
1448                    // No focusable descendants
1449                    || (focusableCount == views.size())) {
1450                if (recyclerView.isFocusable()) {
1451                    views.add(recyclerView);
1452                }
1453            }
1454        } else {
1455            if (recyclerView.isFocusable()) {
1456                views.add(recyclerView);
1457            }
1458        }
1459        return true;
1460    }
1461
1462    @Override
1463    public View onFocusSearchFailed(View focused, int direction, Adapter adapter,
1464            Recycler recycler) {
1465        if (DEBUG) Log.v(getTag(), "onFocusSearchFailed direction " + direction);
1466
1467        View view = null;
1468        int movement = getMovement(direction);
1469        final FocusFinder ff = FocusFinder.getInstance();
1470        if (movement == NEXT_ITEM) {
1471            while (view == null && !appendOneVisibleItem()) {
1472                view = ff.findNextFocus(mBaseListView, focused, direction);
1473            }
1474        } else if (movement == PREV_ITEM){
1475            while (view == null && !prependOneVisibleItem()) {
1476                view = ff.findNextFocus(mBaseListView, focused, direction);
1477            }
1478        }
1479        if (view == null) {
1480            // returning the same view to prevent focus lost when scrolling past the end of the list
1481            if (movement == PREV_ITEM) {
1482                view = mFocusOutFront ? null : focused;
1483            } else if (movement == NEXT_ITEM){
1484                view = mFocusOutEnd ? null : focused;
1485            }
1486        }
1487        if (DEBUG) Log.v(getTag(), "returning view " + view);
1488        return view;
1489    }
1490
1491    private final static int PREV_ITEM = 0;
1492    private final static int NEXT_ITEM = 1;
1493    private final static int PREV_ROW = 2;
1494    private final static int NEXT_ROW = 3;
1495
1496    boolean focusSelectedChild(int direction, Rect previouslyFocusedRect) {
1497        View view = getViewByPosition(mFocusPosition);
1498        if (view != null) {
1499            if (!view.requestFocus(direction, previouslyFocusedRect)) {
1500                if (DEBUG) {
1501                    Log.w(getTag(), "failed to request focus on " + view);
1502                }
1503            } else {
1504                return true;
1505            }
1506        }
1507        return false;
1508    }
1509
1510    private int getMovement(int direction) {
1511        int movement = View.FOCUS_LEFT;
1512
1513        if (mOrientation == HORIZONTAL) {
1514            switch(direction) {
1515                case View.FOCUS_LEFT:
1516                    movement = PREV_ITEM;
1517                    break;
1518                case View.FOCUS_RIGHT:
1519                    movement = NEXT_ITEM;
1520                    break;
1521                case View.FOCUS_UP:
1522                    movement = PREV_ROW;
1523                    break;
1524                case View.FOCUS_DOWN:
1525                    movement = NEXT_ROW;
1526                    break;
1527            }
1528         } else if (mOrientation == VERTICAL) {
1529             switch(direction) {
1530                 case View.FOCUS_LEFT:
1531                     movement = PREV_ROW;
1532                     break;
1533                 case View.FOCUS_RIGHT:
1534                     movement = NEXT_ROW;
1535                     break;
1536                 case View.FOCUS_UP:
1537                     movement = PREV_ITEM;
1538                     break;
1539                 case View.FOCUS_DOWN:
1540                     movement = NEXT_ITEM;
1541                     break;
1542             }
1543         }
1544
1545        return movement;
1546    }
1547
1548    @Override
1549    public void onAdapterChanged() {
1550        mGrid = null;
1551        mRows = null;
1552        super.onAdapterChanged();
1553    }
1554}
1555