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