1/*
2 * Copyright (C) 2007 The Android Open Source Project
3 *
4 * Licensed under the Apache License, Version 2.0 (the "License");
5 * you may not use this file except in compliance with the License.
6 * You may obtain a copy of the License at
7 *
8 *      http://www.apache.org/licenses/LICENSE-2.0
9 *
10 * Unless required by applicable law or agreed to in writing, software
11 * distributed under the License is distributed on an "AS IS" BASIS,
12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 * See the License for the specific language governing permissions and
14 * limitations under the License.
15 */
16
17package android.widget;
18
19import android.annotation.IntDef;
20import android.annotation.NonNull;
21import android.content.Context;
22import android.content.Intent;
23import android.content.res.TypedArray;
24import android.graphics.Rect;
25import android.os.Bundle;
26import android.os.Trace;
27import android.util.AttributeSet;
28import android.util.MathUtils;
29import android.view.Gravity;
30import android.view.KeyEvent;
31import android.view.SoundEffectConstants;
32import android.view.View;
33import android.view.ViewDebug;
34import android.view.ViewGroup;
35import android.view.ViewHierarchyEncoder;
36import android.view.ViewRootImpl;
37import android.view.accessibility.AccessibilityNodeInfo;
38import android.view.accessibility.AccessibilityNodeInfo.AccessibilityAction;
39import android.view.accessibility.AccessibilityNodeInfo.CollectionInfo;
40import android.view.accessibility.AccessibilityNodeInfo.CollectionItemInfo;
41import android.view.accessibility.AccessibilityNodeProvider;
42import android.view.animation.GridLayoutAnimationController;
43import android.widget.RemoteViews.RemoteView;
44
45import com.android.internal.R;
46
47import java.lang.annotation.Retention;
48import java.lang.annotation.RetentionPolicy;
49
50
51/**
52 * A view that shows items in two-dimensional scrolling grid. The items in the
53 * grid come from the {@link ListAdapter} associated with this view.
54 *
55 * <p>See the <a href="{@docRoot}guide/topics/ui/layout/gridview.html">Grid
56 * View</a> guide.</p>
57 *
58 * @attr ref android.R.styleable#GridView_horizontalSpacing
59 * @attr ref android.R.styleable#GridView_verticalSpacing
60 * @attr ref android.R.styleable#GridView_stretchMode
61 * @attr ref android.R.styleable#GridView_columnWidth
62 * @attr ref android.R.styleable#GridView_numColumns
63 * @attr ref android.R.styleable#GridView_gravity
64 */
65@RemoteView
66public class GridView extends AbsListView {
67    /** @hide */
68    @IntDef(prefix = { "NO_STRETCH", "STRETCH_" }, value = {
69            NO_STRETCH,
70            STRETCH_SPACING,
71            STRETCH_COLUMN_WIDTH,
72            STRETCH_SPACING_UNIFORM
73    })
74    @Retention(RetentionPolicy.SOURCE)
75    public @interface StretchMode {}
76
77    /**
78     * Disables stretching.
79     *
80     * @see #setStretchMode(int)
81     */
82    public static final int NO_STRETCH = 0;
83    /**
84     * Stretches the spacing between columns.
85     *
86     * @see #setStretchMode(int)
87     */
88    public static final int STRETCH_SPACING = 1;
89    /**
90     * Stretches columns.
91     *
92     * @see #setStretchMode(int)
93     */
94    public static final int STRETCH_COLUMN_WIDTH = 2;
95    /**
96     * Stretches the spacing between columns. The spacing is uniform.
97     *
98     * @see #setStretchMode(int)
99     */
100    public static final int STRETCH_SPACING_UNIFORM = 3;
101
102    /**
103     * Creates as many columns as can fit on screen.
104     *
105     * @see #setNumColumns(int)
106     */
107    public static final int AUTO_FIT = -1;
108
109    private int mNumColumns = AUTO_FIT;
110
111    private int mHorizontalSpacing = 0;
112    private int mRequestedHorizontalSpacing;
113    private int mVerticalSpacing = 0;
114    private int mStretchMode = STRETCH_COLUMN_WIDTH;
115    private int mColumnWidth;
116    private int mRequestedColumnWidth;
117    private int mRequestedNumColumns;
118
119    private View mReferenceView = null;
120    private View mReferenceViewInSelectedRow = null;
121
122    private int mGravity = Gravity.START;
123
124    private final Rect mTempRect = new Rect();
125
126    public GridView(Context context) {
127        this(context, null);
128    }
129
130    public GridView(Context context, AttributeSet attrs) {
131        this(context, attrs, R.attr.gridViewStyle);
132    }
133
134    public GridView(Context context, AttributeSet attrs, int defStyleAttr) {
135        this(context, attrs, defStyleAttr, 0);
136    }
137
138    public GridView(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) {
139        super(context, attrs, defStyleAttr, defStyleRes);
140
141        final TypedArray a = context.obtainStyledAttributes(
142                attrs, R.styleable.GridView, defStyleAttr, defStyleRes);
143
144        int hSpacing = a.getDimensionPixelOffset(
145                R.styleable.GridView_horizontalSpacing, 0);
146        setHorizontalSpacing(hSpacing);
147
148        int vSpacing = a.getDimensionPixelOffset(
149                R.styleable.GridView_verticalSpacing, 0);
150        setVerticalSpacing(vSpacing);
151
152        int index = a.getInt(R.styleable.GridView_stretchMode, STRETCH_COLUMN_WIDTH);
153        if (index >= 0) {
154            setStretchMode(index);
155        }
156
157        int columnWidth = a.getDimensionPixelOffset(R.styleable.GridView_columnWidth, -1);
158        if (columnWidth > 0) {
159            setColumnWidth(columnWidth);
160        }
161
162        int numColumns = a.getInt(R.styleable.GridView_numColumns, 1);
163        setNumColumns(numColumns);
164
165        index = a.getInt(R.styleable.GridView_gravity, -1);
166        if (index >= 0) {
167            setGravity(index);
168        }
169
170        a.recycle();
171    }
172
173    @Override
174    public ListAdapter getAdapter() {
175        return mAdapter;
176    }
177
178    /**
179     * Sets up this AbsListView to use a remote views adapter which connects to a RemoteViewsService
180     * through the specified intent.
181     * @param intent the intent used to identify the RemoteViewsService for the adapter to connect to.
182     */
183    @android.view.RemotableViewMethod(asyncImpl="setRemoteViewsAdapterAsync")
184    public void setRemoteViewsAdapter(Intent intent) {
185        super.setRemoteViewsAdapter(intent);
186    }
187
188    /**
189     * Sets the data behind this GridView.
190     *
191     * @param adapter the adapter providing the grid's data
192     */
193    @Override
194    public void setAdapter(ListAdapter adapter) {
195        if (mAdapter != null && mDataSetObserver != null) {
196            mAdapter.unregisterDataSetObserver(mDataSetObserver);
197        }
198
199        resetList();
200        mRecycler.clear();
201        mAdapter = adapter;
202
203        mOldSelectedPosition = INVALID_POSITION;
204        mOldSelectedRowId = INVALID_ROW_ID;
205
206        // AbsListView#setAdapter will update choice mode states.
207        super.setAdapter(adapter);
208
209        if (mAdapter != null) {
210            mOldItemCount = mItemCount;
211            mItemCount = mAdapter.getCount();
212            mDataChanged = true;
213            checkFocus();
214
215            mDataSetObserver = new AdapterDataSetObserver();
216            mAdapter.registerDataSetObserver(mDataSetObserver);
217
218            mRecycler.setViewTypeCount(mAdapter.getViewTypeCount());
219
220            int position;
221            if (mStackFromBottom) {
222                position = lookForSelectablePosition(mItemCount - 1, false);
223            } else {
224                position = lookForSelectablePosition(0, true);
225            }
226            setSelectedPositionInt(position);
227            setNextSelectedPositionInt(position);
228            checkSelectionChanged();
229        } else {
230            checkFocus();
231            // Nothing selected
232            checkSelectionChanged();
233        }
234
235        requestLayout();
236    }
237
238    @Override
239    int lookForSelectablePosition(int position, boolean lookDown) {
240        final ListAdapter adapter = mAdapter;
241        if (adapter == null || isInTouchMode()) {
242            return INVALID_POSITION;
243        }
244
245        if (position < 0 || position >= mItemCount) {
246            return INVALID_POSITION;
247        }
248        return position;
249    }
250
251    /**
252     * {@inheritDoc}
253     */
254    @Override
255    void fillGap(boolean down) {
256        final int numColumns = mNumColumns;
257        final int verticalSpacing = mVerticalSpacing;
258
259        final int count = getChildCount();
260
261        if (down) {
262            int paddingTop = 0;
263            if ((mGroupFlags & CLIP_TO_PADDING_MASK) == CLIP_TO_PADDING_MASK) {
264                paddingTop = getListPaddingTop();
265            }
266            final int startOffset = count > 0 ?
267                    getChildAt(count - 1).getBottom() + verticalSpacing : paddingTop;
268            int position = mFirstPosition + count;
269            if (mStackFromBottom) {
270                position += numColumns - 1;
271            }
272            fillDown(position, startOffset);
273            correctTooHigh(numColumns, verticalSpacing, getChildCount());
274        } else {
275            int paddingBottom = 0;
276            if ((mGroupFlags & CLIP_TO_PADDING_MASK) == CLIP_TO_PADDING_MASK) {
277                paddingBottom = getListPaddingBottom();
278            }
279            final int startOffset = count > 0 ?
280                    getChildAt(0).getTop() - verticalSpacing : getHeight() - paddingBottom;
281            int position = mFirstPosition;
282            if (!mStackFromBottom) {
283                position -= numColumns;
284            } else {
285                position--;
286            }
287            fillUp(position, startOffset);
288            correctTooLow(numColumns, verticalSpacing, getChildCount());
289        }
290    }
291
292    /**
293     * Fills the list from pos down to the end of the list view.
294     *
295     * @param pos The first position to put in the list
296     *
297     * @param nextTop The location where the top of the item associated with pos
298     *        should be drawn
299     *
300     * @return The view that is currently selected, if it happens to be in the
301     *         range that we draw.
302     */
303    private View fillDown(int pos, int nextTop) {
304        View selectedView = null;
305
306        int end = (mBottom - mTop);
307        if ((mGroupFlags & CLIP_TO_PADDING_MASK) == CLIP_TO_PADDING_MASK) {
308            end -= mListPadding.bottom;
309        }
310
311        while (nextTop < end && pos < mItemCount) {
312            View temp = makeRow(pos, nextTop, true);
313            if (temp != null) {
314                selectedView = temp;
315            }
316
317            // mReferenceView will change with each call to makeRow()
318            // do not cache in a local variable outside of this loop
319            nextTop = mReferenceView.getBottom() + mVerticalSpacing;
320
321            pos += mNumColumns;
322        }
323
324        setVisibleRangeHint(mFirstPosition, mFirstPosition + getChildCount() - 1);
325        return selectedView;
326    }
327
328    private View makeRow(int startPos, int y, boolean flow) {
329        final int columnWidth = mColumnWidth;
330        final int horizontalSpacing = mHorizontalSpacing;
331
332        final boolean isLayoutRtl = isLayoutRtl();
333
334        int last;
335        int nextLeft;
336
337        if (isLayoutRtl) {
338            nextLeft = getWidth() - mListPadding.right - columnWidth -
339                    ((mStretchMode == STRETCH_SPACING_UNIFORM) ? horizontalSpacing : 0);
340        } else {
341            nextLeft = mListPadding.left +
342                    ((mStretchMode == STRETCH_SPACING_UNIFORM) ? horizontalSpacing : 0);
343        }
344
345        if (!mStackFromBottom) {
346            last = Math.min(startPos + mNumColumns, mItemCount);
347        } else {
348            last = startPos + 1;
349            startPos = Math.max(0, startPos - mNumColumns + 1);
350
351            if (last - startPos < mNumColumns) {
352                final int deltaLeft = (mNumColumns - (last - startPos)) * (columnWidth + horizontalSpacing);
353                nextLeft += (isLayoutRtl ? -1 : +1) * deltaLeft;
354            }
355        }
356
357        View selectedView = null;
358
359        final boolean hasFocus = shouldShowSelector();
360        final boolean inClick = touchModeDrawsInPressedState();
361        final int selectedPosition = mSelectedPosition;
362
363        View child = null;
364        final int nextChildDir = isLayoutRtl ? -1 : +1;
365        for (int pos = startPos; pos < last; pos++) {
366            // is this the selected item?
367            boolean selected = pos == selectedPosition;
368            // does the list view have focus or contain focus
369
370            final int where = flow ? -1 : pos - startPos;
371            child = makeAndAddView(pos, y, flow, nextLeft, selected, where);
372
373            nextLeft += nextChildDir * columnWidth;
374            if (pos < last - 1) {
375                nextLeft += nextChildDir * horizontalSpacing;
376            }
377
378            if (selected && (hasFocus || inClick)) {
379                selectedView = child;
380            }
381        }
382
383        mReferenceView = child;
384
385        if (selectedView != null) {
386            mReferenceViewInSelectedRow = mReferenceView;
387        }
388
389        return selectedView;
390    }
391
392    /**
393     * Fills the list from pos up to the top of the list view.
394     *
395     * @param pos The first position to put in the list
396     *
397     * @param nextBottom The location where the bottom of the item associated
398     *        with pos should be drawn
399     *
400     * @return The view that is currently selected
401     */
402    private View fillUp(int pos, int nextBottom) {
403        View selectedView = null;
404
405        int end = 0;
406        if ((mGroupFlags & CLIP_TO_PADDING_MASK) == CLIP_TO_PADDING_MASK) {
407            end = mListPadding.top;
408        }
409
410        while (nextBottom > end && pos >= 0) {
411
412            View temp = makeRow(pos, nextBottom, false);
413            if (temp != null) {
414                selectedView = temp;
415            }
416
417            nextBottom = mReferenceView.getTop() - mVerticalSpacing;
418
419            mFirstPosition = pos;
420
421            pos -= mNumColumns;
422        }
423
424        if (mStackFromBottom) {
425            mFirstPosition = Math.max(0, pos + 1);
426        }
427
428        setVisibleRangeHint(mFirstPosition, mFirstPosition + getChildCount() - 1);
429        return selectedView;
430    }
431
432    /**
433     * Fills the list from top to bottom, starting with mFirstPosition
434     *
435     * @param nextTop The location where the top of the first item should be
436     *        drawn
437     *
438     * @return The view that is currently selected
439     */
440    private View fillFromTop(int nextTop) {
441        mFirstPosition = Math.min(mFirstPosition, mSelectedPosition);
442        mFirstPosition = Math.min(mFirstPosition, mItemCount - 1);
443        if (mFirstPosition < 0) {
444            mFirstPosition = 0;
445        }
446        mFirstPosition -= mFirstPosition % mNumColumns;
447        return fillDown(mFirstPosition, nextTop);
448    }
449
450    private View fillFromBottom(int lastPosition, int nextBottom) {
451        lastPosition = Math.max(lastPosition, mSelectedPosition);
452        lastPosition = Math.min(lastPosition, mItemCount - 1);
453
454        final int invertedPosition = mItemCount - 1 - lastPosition;
455        lastPosition = mItemCount - 1 - (invertedPosition - (invertedPosition % mNumColumns));
456
457        return fillUp(lastPosition, nextBottom);
458    }
459
460    private View fillSelection(int childrenTop, int childrenBottom) {
461        final int selectedPosition = reconcileSelectedPosition();
462        final int numColumns = mNumColumns;
463        final int verticalSpacing = mVerticalSpacing;
464
465        int rowStart;
466        int rowEnd = -1;
467
468        if (!mStackFromBottom) {
469            rowStart = selectedPosition - (selectedPosition % numColumns);
470        } else {
471            final int invertedSelection = mItemCount - 1 - selectedPosition;
472
473            rowEnd = mItemCount - 1 - (invertedSelection - (invertedSelection % numColumns));
474            rowStart = Math.max(0, rowEnd - numColumns + 1);
475        }
476
477        final int fadingEdgeLength = getVerticalFadingEdgeLength();
478        final int topSelectionPixel = getTopSelectionPixel(childrenTop, fadingEdgeLength, rowStart);
479
480        final View sel = makeRow(mStackFromBottom ? rowEnd : rowStart, topSelectionPixel, true);
481        mFirstPosition = rowStart;
482
483        final View referenceView = mReferenceView;
484
485        if (!mStackFromBottom) {
486            fillDown(rowStart + numColumns, referenceView.getBottom() + verticalSpacing);
487            pinToBottom(childrenBottom);
488            fillUp(rowStart - numColumns, referenceView.getTop() - verticalSpacing);
489            adjustViewsUpOrDown();
490        } else {
491            final int bottomSelectionPixel = getBottomSelectionPixel(childrenBottom,
492                    fadingEdgeLength, numColumns, rowStart);
493            final int offset = bottomSelectionPixel - referenceView.getBottom();
494            offsetChildrenTopAndBottom(offset);
495            fillUp(rowStart - 1, referenceView.getTop() - verticalSpacing);
496            pinToTop(childrenTop);
497            fillDown(rowEnd + numColumns, referenceView.getBottom() + verticalSpacing);
498            adjustViewsUpOrDown();
499        }
500
501        return sel;
502    }
503
504    private void pinToTop(int childrenTop) {
505        if (mFirstPosition == 0) {
506            final int top = getChildAt(0).getTop();
507            final int offset = childrenTop - top;
508            if (offset < 0) {
509                offsetChildrenTopAndBottom(offset);
510            }
511        }
512    }
513
514    private void pinToBottom(int childrenBottom) {
515        final int count = getChildCount();
516        if (mFirstPosition + count == mItemCount) {
517            final int bottom = getChildAt(count - 1).getBottom();
518            final int offset = childrenBottom - bottom;
519            if (offset > 0) {
520                offsetChildrenTopAndBottom(offset);
521            }
522        }
523    }
524
525    @Override
526    int findMotionRow(int y) {
527        final int childCount = getChildCount();
528        if (childCount > 0) {
529
530            final int numColumns = mNumColumns;
531            if (!mStackFromBottom) {
532                for (int i = 0; i < childCount; i += numColumns) {
533                    if (y <= getChildAt(i).getBottom()) {
534                        return mFirstPosition + i;
535                    }
536                }
537            } else {
538                for (int i = childCount - 1; i >= 0; i -= numColumns) {
539                    if (y >= getChildAt(i).getTop()) {
540                        return mFirstPosition + i;
541                    }
542                }
543            }
544        }
545        return INVALID_POSITION;
546    }
547
548    /**
549     * Layout during a scroll that results from tracking motion events. Places
550     * the mMotionPosition view at the offset specified by mMotionViewTop, and
551     * then build surrounding views from there.
552     *
553     * @param position the position at which to start filling
554     * @param top the top of the view at that position
555     * @return The selected view, or null if the selected view is outside the
556     *         visible area.
557     */
558    private View fillSpecific(int position, int top) {
559        final int numColumns = mNumColumns;
560
561        int motionRowStart;
562        int motionRowEnd = -1;
563
564        if (!mStackFromBottom) {
565            motionRowStart = position - (position % numColumns);
566        } else {
567            final int invertedSelection = mItemCount - 1 - position;
568
569            motionRowEnd = mItemCount - 1 - (invertedSelection - (invertedSelection % numColumns));
570            motionRowStart = Math.max(0, motionRowEnd - numColumns + 1);
571        }
572
573        final View temp = makeRow(mStackFromBottom ? motionRowEnd : motionRowStart, top, true);
574
575        // Possibly changed again in fillUp if we add rows above this one.
576        mFirstPosition = motionRowStart;
577
578        final View referenceView = mReferenceView;
579        // We didn't have anything to layout, bail out
580        if (referenceView == null) {
581            return null;
582        }
583
584        final int verticalSpacing = mVerticalSpacing;
585
586        View above;
587        View below;
588
589        if (!mStackFromBottom) {
590            above = fillUp(motionRowStart - numColumns, referenceView.getTop() - verticalSpacing);
591            adjustViewsUpOrDown();
592            below = fillDown(motionRowStart + numColumns, referenceView.getBottom() + verticalSpacing);
593            // Check if we have dragged the bottom of the grid too high
594            final int childCount = getChildCount();
595            if (childCount > 0) {
596                correctTooHigh(numColumns, verticalSpacing, childCount);
597            }
598        } else {
599            below = fillDown(motionRowEnd + numColumns, referenceView.getBottom() + verticalSpacing);
600            adjustViewsUpOrDown();
601            above = fillUp(motionRowStart - 1, referenceView.getTop() - verticalSpacing);
602            // Check if we have dragged the bottom of the grid too high
603            final int childCount = getChildCount();
604            if (childCount > 0) {
605                correctTooLow(numColumns, verticalSpacing, childCount);
606            }
607        }
608
609        if (temp != null) {
610            return temp;
611        } else if (above != null) {
612            return above;
613        } else {
614            return below;
615        }
616    }
617
618    private void correctTooHigh(int numColumns, int verticalSpacing, int childCount) {
619        // First see if the last item is visible
620        final int lastPosition = mFirstPosition + childCount - 1;
621        if (lastPosition == mItemCount - 1 && childCount > 0) {
622            // Get the last child ...
623            final View lastChild = getChildAt(childCount - 1);
624
625            // ... and its bottom edge
626            final int lastBottom = lastChild.getBottom();
627            // This is bottom of our drawable area
628            final int end = (mBottom - mTop) - mListPadding.bottom;
629
630            // This is how far the bottom edge of the last view is from the bottom of the
631            // drawable area
632            int bottomOffset = end - lastBottom;
633
634            final View firstChild = getChildAt(0);
635            final int firstTop = firstChild.getTop();
636
637            // Make sure we are 1) Too high, and 2) Either there are more rows above the
638            // first row or the first row is scrolled off the top of the drawable area
639            if (bottomOffset > 0 && (mFirstPosition > 0 || firstTop < mListPadding.top))  {
640                if (mFirstPosition == 0) {
641                    // Don't pull the top too far down
642                    bottomOffset = Math.min(bottomOffset, mListPadding.top - firstTop);
643                }
644
645                // Move everything down
646                offsetChildrenTopAndBottom(bottomOffset);
647                if (mFirstPosition > 0) {
648                    // Fill the gap that was opened above mFirstPosition with more rows, if
649                    // possible
650                    fillUp(mFirstPosition - (mStackFromBottom ? 1 : numColumns),
651                            firstChild.getTop() - verticalSpacing);
652                    // Close up the remaining gap
653                    adjustViewsUpOrDown();
654                }
655            }
656        }
657    }
658
659    private void correctTooLow(int numColumns, int verticalSpacing, int childCount) {
660        if (mFirstPosition == 0 && childCount > 0) {
661            // Get the first child ...
662            final View firstChild = getChildAt(0);
663
664            // ... and its top edge
665            final int firstTop = firstChild.getTop();
666
667            // This is top of our drawable area
668            final int start = mListPadding.top;
669
670            // This is bottom of our drawable area
671            final int end = (mBottom - mTop) - mListPadding.bottom;
672
673            // This is how far the top edge of the first view is from the top of the
674            // drawable area
675            int topOffset = firstTop - start;
676            final View lastChild = getChildAt(childCount - 1);
677            final int lastBottom = lastChild.getBottom();
678            final int lastPosition = mFirstPosition + childCount - 1;
679
680            // Make sure we are 1) Too low, and 2) Either there are more rows below the
681            // last row or the last row is scrolled off the bottom of the drawable area
682            if (topOffset > 0 && (lastPosition < mItemCount - 1 || lastBottom > end))  {
683                if (lastPosition == mItemCount - 1 ) {
684                    // Don't pull the bottom too far up
685                    topOffset = Math.min(topOffset, lastBottom - end);
686                }
687
688                // Move everything up
689                offsetChildrenTopAndBottom(-topOffset);
690                if (lastPosition < mItemCount - 1) {
691                    // Fill the gap that was opened below the last position with more rows, if
692                    // possible
693                    fillDown(lastPosition + (!mStackFromBottom ? 1 : numColumns),
694                            lastChild.getBottom() + verticalSpacing);
695                    // Close up the remaining gap
696                    adjustViewsUpOrDown();
697                }
698            }
699        }
700    }
701
702    /**
703     * Fills the grid based on positioning the new selection at a specific
704     * location. The selection may be moved so that it does not intersect the
705     * faded edges. The grid is then filled upwards and downwards from there.
706     *
707     * @param selectedTop Where the selected item should be
708     * @param childrenTop Where to start drawing children
709     * @param childrenBottom Last pixel where children can be drawn
710     * @return The view that currently has selection
711     */
712    private View fillFromSelection(int selectedTop, int childrenTop, int childrenBottom) {
713        final int fadingEdgeLength = getVerticalFadingEdgeLength();
714        final int selectedPosition = mSelectedPosition;
715        final int numColumns = mNumColumns;
716        final int verticalSpacing = mVerticalSpacing;
717
718        int rowStart;
719        int rowEnd = -1;
720
721        if (!mStackFromBottom) {
722            rowStart = selectedPosition - (selectedPosition % numColumns);
723        } else {
724            int invertedSelection = mItemCount - 1 - selectedPosition;
725
726            rowEnd = mItemCount - 1 - (invertedSelection - (invertedSelection % numColumns));
727            rowStart = Math.max(0, rowEnd - numColumns + 1);
728        }
729
730        View sel;
731        View referenceView;
732
733        int topSelectionPixel = getTopSelectionPixel(childrenTop, fadingEdgeLength, rowStart);
734        int bottomSelectionPixel = getBottomSelectionPixel(childrenBottom, fadingEdgeLength,
735                numColumns, rowStart);
736
737        sel = makeRow(mStackFromBottom ? rowEnd : rowStart, selectedTop, true);
738        // Possibly changed again in fillUp if we add rows above this one.
739        mFirstPosition = rowStart;
740
741        referenceView = mReferenceView;
742        adjustForTopFadingEdge(referenceView, topSelectionPixel, bottomSelectionPixel);
743        adjustForBottomFadingEdge(referenceView, topSelectionPixel, bottomSelectionPixel);
744
745        if (!mStackFromBottom) {
746            fillUp(rowStart - numColumns, referenceView.getTop() - verticalSpacing);
747            adjustViewsUpOrDown();
748            fillDown(rowStart + numColumns, referenceView.getBottom() + verticalSpacing);
749        } else {
750            fillDown(rowEnd + numColumns, referenceView.getBottom() + verticalSpacing);
751            adjustViewsUpOrDown();
752            fillUp(rowStart - 1, referenceView.getTop() - verticalSpacing);
753        }
754
755
756        return sel;
757    }
758
759    /**
760     * Calculate the bottom-most pixel we can draw the selection into
761     *
762     * @param childrenBottom Bottom pixel were children can be drawn
763     * @param fadingEdgeLength Length of the fading edge in pixels, if present
764     * @param numColumns Number of columns in the grid
765     * @param rowStart The start of the row that will contain the selection
766     * @return The bottom-most pixel we can draw the selection into
767     */
768    private int getBottomSelectionPixel(int childrenBottom, int fadingEdgeLength,
769            int numColumns, int rowStart) {
770        // Last pixel we can draw the selection into
771        int bottomSelectionPixel = childrenBottom;
772        if (rowStart + numColumns - 1 < mItemCount - 1) {
773            bottomSelectionPixel -= fadingEdgeLength;
774        }
775        return bottomSelectionPixel;
776    }
777
778    /**
779     * Calculate the top-most pixel we can draw the selection into
780     *
781     * @param childrenTop Top pixel were children can be drawn
782     * @param fadingEdgeLength Length of the fading edge in pixels, if present
783     * @param rowStart The start of the row that will contain the selection
784     * @return The top-most pixel we can draw the selection into
785     */
786    private int getTopSelectionPixel(int childrenTop, int fadingEdgeLength, int rowStart) {
787        // first pixel we can draw the selection into
788        int topSelectionPixel = childrenTop;
789        if (rowStart > 0) {
790            topSelectionPixel += fadingEdgeLength;
791        }
792        return topSelectionPixel;
793    }
794
795    /**
796     * Move all views upwards so the selected row does not interesect the bottom
797     * fading edge (if necessary).
798     *
799     * @param childInSelectedRow A child in the row that contains the selection
800     * @param topSelectionPixel The topmost pixel we can draw the selection into
801     * @param bottomSelectionPixel The bottommost pixel we can draw the
802     *        selection into
803     */
804    private void adjustForBottomFadingEdge(View childInSelectedRow,
805            int topSelectionPixel, int bottomSelectionPixel) {
806        // Some of the newly selected item extends below the bottom of the
807        // list
808        if (childInSelectedRow.getBottom() > bottomSelectionPixel) {
809
810            // Find space available above the selection into which we can
811            // scroll upwards
812            int spaceAbove = childInSelectedRow.getTop() - topSelectionPixel;
813
814            // Find space required to bring the bottom of the selected item
815            // fully into view
816            int spaceBelow = childInSelectedRow.getBottom() - bottomSelectionPixel;
817            int offset = Math.min(spaceAbove, spaceBelow);
818
819            // Now offset the selected item to get it into view
820            offsetChildrenTopAndBottom(-offset);
821        }
822    }
823
824    /**
825     * Move all views upwards so the selected row does not interesect the top
826     * fading edge (if necessary).
827     *
828     * @param childInSelectedRow A child in the row that contains the selection
829     * @param topSelectionPixel The topmost pixel we can draw the selection into
830     * @param bottomSelectionPixel The bottommost pixel we can draw the
831     *        selection into
832     */
833    private void adjustForTopFadingEdge(View childInSelectedRow,
834            int topSelectionPixel, int bottomSelectionPixel) {
835        // Some of the newly selected item extends above the top of the list
836        if (childInSelectedRow.getTop() < topSelectionPixel) {
837            // Find space required to bring the top of the selected item
838            // fully into view
839            int spaceAbove = topSelectionPixel - childInSelectedRow.getTop();
840
841            // Find space available below the selection into which we can
842            // scroll downwards
843            int spaceBelow = bottomSelectionPixel - childInSelectedRow.getBottom();
844            int offset = Math.min(spaceAbove, spaceBelow);
845
846            // Now offset the selected item to get it into view
847            offsetChildrenTopAndBottom(offset);
848        }
849    }
850
851    /**
852     * Smoothly scroll to the specified adapter position. The view will
853     * scroll such that the indicated position is displayed.
854     * @param position Scroll to this adapter position.
855     */
856    @android.view.RemotableViewMethod
857    public void smoothScrollToPosition(int position) {
858        super.smoothScrollToPosition(position);
859    }
860
861    /**
862     * Smoothly scroll to the specified adapter position offset. The view will
863     * scroll such that the indicated position is displayed.
864     * @param offset The amount to offset from the adapter position to scroll to.
865     */
866    @android.view.RemotableViewMethod
867    public void smoothScrollByOffset(int offset) {
868        super.smoothScrollByOffset(offset);
869    }
870
871    /**
872     * Fills the grid based on positioning the new selection relative to the old
873     * selection. The new selection will be placed at, above, or below the
874     * location of the new selection depending on how the selection is moving.
875     * The selection will then be pinned to the visible part of the screen,
876     * excluding the edges that are faded. The grid is then filled upwards and
877     * downwards from there.
878     *
879     * @param delta Which way we are moving
880     * @param childrenTop Where to start drawing children
881     * @param childrenBottom Last pixel where children can be drawn
882     * @return The view that currently has selection
883     */
884    private View moveSelection(int delta, int childrenTop, int childrenBottom) {
885        final int fadingEdgeLength = getVerticalFadingEdgeLength();
886        final int selectedPosition = mSelectedPosition;
887        final int numColumns = mNumColumns;
888        final int verticalSpacing = mVerticalSpacing;
889
890        int oldRowStart;
891        int rowStart;
892        int rowEnd = -1;
893
894        if (!mStackFromBottom) {
895            oldRowStart = (selectedPosition - delta) - ((selectedPosition - delta) % numColumns);
896
897            rowStart = selectedPosition - (selectedPosition % numColumns);
898        } else {
899            int invertedSelection = mItemCount - 1 - selectedPosition;
900
901            rowEnd = mItemCount - 1 - (invertedSelection - (invertedSelection % numColumns));
902            rowStart = Math.max(0, rowEnd - numColumns + 1);
903
904            invertedSelection = mItemCount - 1 - (selectedPosition - delta);
905            oldRowStart = mItemCount - 1 - (invertedSelection - (invertedSelection % numColumns));
906            oldRowStart = Math.max(0, oldRowStart - numColumns + 1);
907        }
908
909        final int rowDelta = rowStart - oldRowStart;
910
911        final int topSelectionPixel = getTopSelectionPixel(childrenTop, fadingEdgeLength, rowStart);
912        final int bottomSelectionPixel = getBottomSelectionPixel(childrenBottom, fadingEdgeLength,
913                numColumns, rowStart);
914
915        // Possibly changed again in fillUp if we add rows above this one.
916        mFirstPosition = rowStart;
917
918        View sel;
919        View referenceView;
920
921        if (rowDelta > 0) {
922            /*
923             * Case 1: Scrolling down.
924             */
925
926            final int oldBottom = mReferenceViewInSelectedRow == null ? 0 :
927                    mReferenceViewInSelectedRow.getBottom();
928
929            sel = makeRow(mStackFromBottom ? rowEnd : rowStart, oldBottom + verticalSpacing, true);
930            referenceView = mReferenceView;
931
932            adjustForBottomFadingEdge(referenceView, topSelectionPixel, bottomSelectionPixel);
933        } else if (rowDelta < 0) {
934            /*
935             * Case 2: Scrolling up.
936             */
937            final int oldTop = mReferenceViewInSelectedRow == null ?
938                    0 : mReferenceViewInSelectedRow .getTop();
939
940            sel = makeRow(mStackFromBottom ? rowEnd : rowStart, oldTop - verticalSpacing, false);
941            referenceView = mReferenceView;
942
943            adjustForTopFadingEdge(referenceView, topSelectionPixel, bottomSelectionPixel);
944        } else {
945            /*
946             * Keep selection where it was
947             */
948            final int oldTop = mReferenceViewInSelectedRow == null ?
949                    0 : mReferenceViewInSelectedRow .getTop();
950
951            sel = makeRow(mStackFromBottom ? rowEnd : rowStart, oldTop, true);
952            referenceView = mReferenceView;
953        }
954
955        if (!mStackFromBottom) {
956            fillUp(rowStart - numColumns, referenceView.getTop() - verticalSpacing);
957            adjustViewsUpOrDown();
958            fillDown(rowStart + numColumns, referenceView.getBottom() + verticalSpacing);
959        } else {
960            fillDown(rowEnd + numColumns, referenceView.getBottom() + verticalSpacing);
961            adjustViewsUpOrDown();
962            fillUp(rowStart - 1, referenceView.getTop() - verticalSpacing);
963        }
964
965        return sel;
966    }
967
968    private boolean determineColumns(int availableSpace) {
969        final int requestedHorizontalSpacing = mRequestedHorizontalSpacing;
970        final int stretchMode = mStretchMode;
971        final int requestedColumnWidth = mRequestedColumnWidth;
972        boolean didNotInitiallyFit = false;
973
974        if (mRequestedNumColumns == AUTO_FIT) {
975            if (requestedColumnWidth > 0) {
976                // Client told us to pick the number of columns
977                mNumColumns = (availableSpace + requestedHorizontalSpacing) /
978                        (requestedColumnWidth + requestedHorizontalSpacing);
979            } else {
980                // Just make up a number if we don't have enough info
981                mNumColumns = 2;
982            }
983        } else {
984            // We picked the columns
985            mNumColumns = mRequestedNumColumns;
986        }
987
988        if (mNumColumns <= 0) {
989            mNumColumns = 1;
990        }
991
992        switch (stretchMode) {
993            case NO_STRETCH:
994                // Nobody stretches
995                mColumnWidth = requestedColumnWidth;
996                mHorizontalSpacing = requestedHorizontalSpacing;
997                break;
998
999            default:
1000                int spaceLeftOver = availableSpace - (mNumColumns * requestedColumnWidth)
1001                        - ((mNumColumns - 1) * requestedHorizontalSpacing);
1002
1003                if (spaceLeftOver < 0) {
1004                    didNotInitiallyFit = true;
1005                }
1006
1007                switch (stretchMode) {
1008                    case STRETCH_COLUMN_WIDTH:
1009                        // Stretch the columns
1010                        mColumnWidth = requestedColumnWidth + spaceLeftOver / mNumColumns;
1011                        mHorizontalSpacing = requestedHorizontalSpacing;
1012                        break;
1013
1014                    case STRETCH_SPACING:
1015                        // Stretch the spacing between columns
1016                        mColumnWidth = requestedColumnWidth;
1017                        if (mNumColumns > 1) {
1018                            mHorizontalSpacing = requestedHorizontalSpacing
1019                                    + spaceLeftOver / (mNumColumns - 1);
1020                        } else {
1021                            mHorizontalSpacing = requestedHorizontalSpacing + spaceLeftOver;
1022                        }
1023                        break;
1024
1025                    case STRETCH_SPACING_UNIFORM:
1026                        // Stretch the spacing between columns
1027                        mColumnWidth = requestedColumnWidth;
1028                        if (mNumColumns > 1) {
1029                            mHorizontalSpacing = requestedHorizontalSpacing
1030                                    + spaceLeftOver / (mNumColumns + 1);
1031                        } else {
1032                            mHorizontalSpacing = requestedHorizontalSpacing + spaceLeftOver;
1033                        }
1034                        break;
1035                }
1036
1037                break;
1038        }
1039        return didNotInitiallyFit;
1040    }
1041
1042    @Override
1043    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
1044        // Sets up mListPadding
1045        super.onMeasure(widthMeasureSpec, heightMeasureSpec);
1046
1047        int widthMode = MeasureSpec.getMode(widthMeasureSpec);
1048        int heightMode = MeasureSpec.getMode(heightMeasureSpec);
1049        int widthSize = MeasureSpec.getSize(widthMeasureSpec);
1050        int heightSize = MeasureSpec.getSize(heightMeasureSpec);
1051
1052        if (widthMode == MeasureSpec.UNSPECIFIED) {
1053            if (mColumnWidth > 0) {
1054                widthSize = mColumnWidth + mListPadding.left + mListPadding.right;
1055            } else {
1056                widthSize = mListPadding.left + mListPadding.right;
1057            }
1058            widthSize += getVerticalScrollbarWidth();
1059        }
1060
1061        int childWidth = widthSize - mListPadding.left - mListPadding.right;
1062        boolean didNotInitiallyFit = determineColumns(childWidth);
1063
1064        int childHeight = 0;
1065        int childState = 0;
1066
1067        mItemCount = mAdapter == null ? 0 : mAdapter.getCount();
1068        final int count = mItemCount;
1069        if (count > 0) {
1070            final View child = obtainView(0, mIsScrap);
1071
1072            AbsListView.LayoutParams p = (AbsListView.LayoutParams) child.getLayoutParams();
1073            if (p == null) {
1074                p = (AbsListView.LayoutParams) generateDefaultLayoutParams();
1075                child.setLayoutParams(p);
1076            }
1077            p.viewType = mAdapter.getItemViewType(0);
1078            p.isEnabled = mAdapter.isEnabled(0);
1079            p.forceAdd = true;
1080
1081            int childHeightSpec = getChildMeasureSpec(
1082                    MeasureSpec.makeSafeMeasureSpec(MeasureSpec.getSize(heightMeasureSpec),
1083                            MeasureSpec.UNSPECIFIED), 0, p.height);
1084            int childWidthSpec = getChildMeasureSpec(
1085                    MeasureSpec.makeMeasureSpec(mColumnWidth, MeasureSpec.EXACTLY), 0, p.width);
1086            child.measure(childWidthSpec, childHeightSpec);
1087
1088            childHeight = child.getMeasuredHeight();
1089            childState = combineMeasuredStates(childState, child.getMeasuredState());
1090
1091            if (mRecycler.shouldRecycleViewType(p.viewType)) {
1092                mRecycler.addScrapView(child, -1);
1093            }
1094        }
1095
1096        if (heightMode == MeasureSpec.UNSPECIFIED) {
1097            heightSize = mListPadding.top + mListPadding.bottom + childHeight +
1098                    getVerticalFadingEdgeLength() * 2;
1099        }
1100
1101        if (heightMode == MeasureSpec.AT_MOST) {
1102            int ourSize =  mListPadding.top + mListPadding.bottom;
1103
1104            final int numColumns = mNumColumns;
1105            for (int i = 0; i < count; i += numColumns) {
1106                ourSize += childHeight;
1107                if (i + numColumns < count) {
1108                    ourSize += mVerticalSpacing;
1109                }
1110                if (ourSize >= heightSize) {
1111                    ourSize = heightSize;
1112                    break;
1113                }
1114            }
1115            heightSize = ourSize;
1116        }
1117
1118        if (widthMode == MeasureSpec.AT_MOST && mRequestedNumColumns != AUTO_FIT) {
1119            int ourSize = (mRequestedNumColumns*mColumnWidth)
1120                    + ((mRequestedNumColumns-1)*mHorizontalSpacing)
1121                    + mListPadding.left + mListPadding.right;
1122            if (ourSize > widthSize || didNotInitiallyFit) {
1123                widthSize |= MEASURED_STATE_TOO_SMALL;
1124            }
1125        }
1126
1127        setMeasuredDimension(widthSize, heightSize);
1128        mWidthMeasureSpec = widthMeasureSpec;
1129    }
1130
1131    @Override
1132    protected void attachLayoutAnimationParameters(View child,
1133            ViewGroup.LayoutParams params, int index, int count) {
1134
1135        GridLayoutAnimationController.AnimationParameters animationParams =
1136                (GridLayoutAnimationController.AnimationParameters) params.layoutAnimationParameters;
1137
1138        if (animationParams == null) {
1139            animationParams = new GridLayoutAnimationController.AnimationParameters();
1140            params.layoutAnimationParameters = animationParams;
1141        }
1142
1143        animationParams.count = count;
1144        animationParams.index = index;
1145        animationParams.columnsCount = mNumColumns;
1146        animationParams.rowsCount = count / mNumColumns;
1147
1148        if (!mStackFromBottom) {
1149            animationParams.column = index % mNumColumns;
1150            animationParams.row = index / mNumColumns;
1151        } else {
1152            final int invertedIndex = count - 1 - index;
1153
1154            animationParams.column = mNumColumns - 1 - (invertedIndex % mNumColumns);
1155            animationParams.row = animationParams.rowsCount - 1 - invertedIndex / mNumColumns;
1156        }
1157    }
1158
1159    @Override
1160    protected void layoutChildren() {
1161        final boolean blockLayoutRequests = mBlockLayoutRequests;
1162        if (!blockLayoutRequests) {
1163            mBlockLayoutRequests = true;
1164        }
1165
1166        try {
1167            super.layoutChildren();
1168
1169            invalidate();
1170
1171            if (mAdapter == null) {
1172                resetList();
1173                invokeOnItemScrollListener();
1174                return;
1175            }
1176
1177            final int childrenTop = mListPadding.top;
1178            final int childrenBottom = mBottom - mTop - mListPadding.bottom;
1179
1180            int childCount = getChildCount();
1181            int index;
1182            int delta = 0;
1183
1184            View sel;
1185            View oldSel = null;
1186            View oldFirst = null;
1187            View newSel = null;
1188
1189            // Remember stuff we will need down below
1190            switch (mLayoutMode) {
1191            case LAYOUT_SET_SELECTION:
1192                index = mNextSelectedPosition - mFirstPosition;
1193                if (index >= 0 && index < childCount) {
1194                    newSel = getChildAt(index);
1195                }
1196                break;
1197            case LAYOUT_FORCE_TOP:
1198            case LAYOUT_FORCE_BOTTOM:
1199            case LAYOUT_SPECIFIC:
1200            case LAYOUT_SYNC:
1201                break;
1202            case LAYOUT_MOVE_SELECTION:
1203                if (mNextSelectedPosition >= 0) {
1204                    delta = mNextSelectedPosition - mSelectedPosition;
1205                }
1206                break;
1207            default:
1208                // Remember the previously selected view
1209                index = mSelectedPosition - mFirstPosition;
1210                if (index >= 0 && index < childCount) {
1211                    oldSel = getChildAt(index);
1212                }
1213
1214                // Remember the previous first child
1215                oldFirst = getChildAt(0);
1216            }
1217
1218            boolean dataChanged = mDataChanged;
1219            if (dataChanged) {
1220                handleDataChanged();
1221            }
1222
1223            // Handle the empty set by removing all views that are visible
1224            // and calling it a day
1225            if (mItemCount == 0) {
1226                resetList();
1227                invokeOnItemScrollListener();
1228                return;
1229            }
1230
1231            setSelectedPositionInt(mNextSelectedPosition);
1232
1233            AccessibilityNodeInfo accessibilityFocusLayoutRestoreNode = null;
1234            View accessibilityFocusLayoutRestoreView = null;
1235            int accessibilityFocusPosition = INVALID_POSITION;
1236
1237            // Remember which child, if any, had accessibility focus. This must
1238            // occur before recycling any views, since that will clear
1239            // accessibility focus.
1240            final ViewRootImpl viewRootImpl = getViewRootImpl();
1241            if (viewRootImpl != null) {
1242                final View focusHost = viewRootImpl.getAccessibilityFocusedHost();
1243                if (focusHost != null) {
1244                    final View focusChild = getAccessibilityFocusedChild(focusHost);
1245                    if (focusChild != null) {
1246                        if (!dataChanged || focusChild.hasTransientState()
1247                                || mAdapterHasStableIds) {
1248                            // The views won't be changing, so try to maintain
1249                            // focus on the current host and virtual view.
1250                            accessibilityFocusLayoutRestoreView = focusHost;
1251                            accessibilityFocusLayoutRestoreNode = viewRootImpl
1252                                    .getAccessibilityFocusedVirtualView();
1253                        }
1254
1255                        // Try to maintain focus at the same position.
1256                        accessibilityFocusPosition = getPositionForView(focusChild);
1257                    }
1258                }
1259            }
1260
1261            // Pull all children into the RecycleBin.
1262            // These views will be reused if possible
1263            final int firstPosition = mFirstPosition;
1264            final RecycleBin recycleBin = mRecycler;
1265
1266            if (dataChanged) {
1267                for (int i = 0; i < childCount; i++) {
1268                    recycleBin.addScrapView(getChildAt(i), firstPosition+i);
1269                }
1270            } else {
1271                recycleBin.fillActiveViews(childCount, firstPosition);
1272            }
1273
1274            // Clear out old views
1275            detachAllViewsFromParent();
1276            recycleBin.removeSkippedScrap();
1277
1278            switch (mLayoutMode) {
1279            case LAYOUT_SET_SELECTION:
1280                if (newSel != null) {
1281                    sel = fillFromSelection(newSel.getTop(), childrenTop, childrenBottom);
1282                } else {
1283                    sel = fillSelection(childrenTop, childrenBottom);
1284                }
1285                break;
1286            case LAYOUT_FORCE_TOP:
1287                mFirstPosition = 0;
1288                sel = fillFromTop(childrenTop);
1289                adjustViewsUpOrDown();
1290                break;
1291            case LAYOUT_FORCE_BOTTOM:
1292                sel = fillUp(mItemCount - 1, childrenBottom);
1293                adjustViewsUpOrDown();
1294                break;
1295            case LAYOUT_SPECIFIC:
1296                sel = fillSpecific(mSelectedPosition, mSpecificTop);
1297                break;
1298            case LAYOUT_SYNC:
1299                sel = fillSpecific(mSyncPosition, mSpecificTop);
1300                break;
1301            case LAYOUT_MOVE_SELECTION:
1302                // Move the selection relative to its old position
1303                sel = moveSelection(delta, childrenTop, childrenBottom);
1304                break;
1305            default:
1306                if (childCount == 0) {
1307                    if (!mStackFromBottom) {
1308                        setSelectedPositionInt(mAdapter == null || isInTouchMode() ?
1309                                INVALID_POSITION : 0);
1310                        sel = fillFromTop(childrenTop);
1311                    } else {
1312                        final int last = mItemCount - 1;
1313                        setSelectedPositionInt(mAdapter == null || isInTouchMode() ?
1314                                INVALID_POSITION : last);
1315                        sel = fillFromBottom(last, childrenBottom);
1316                    }
1317                } else {
1318                    if (mSelectedPosition >= 0 && mSelectedPosition < mItemCount) {
1319                        sel = fillSpecific(mSelectedPosition, oldSel == null ?
1320                                childrenTop : oldSel.getTop());
1321                    } else if (mFirstPosition < mItemCount)  {
1322                        sel = fillSpecific(mFirstPosition, oldFirst == null ?
1323                                childrenTop : oldFirst.getTop());
1324                    } else {
1325                        sel = fillSpecific(0, childrenTop);
1326                    }
1327                }
1328                break;
1329            }
1330
1331            // Flush any cached views that did not get reused above
1332            recycleBin.scrapActiveViews();
1333
1334            if (sel != null) {
1335               positionSelector(INVALID_POSITION, sel);
1336               mSelectedTop = sel.getTop();
1337            } else {
1338                final boolean inTouchMode = mTouchMode > TOUCH_MODE_DOWN
1339                        && mTouchMode < TOUCH_MODE_SCROLL;
1340                if (inTouchMode) {
1341                    // If the user's finger is down, select the motion position.
1342                    final View child = getChildAt(mMotionPosition - mFirstPosition);
1343                    if (child != null) {
1344                        positionSelector(mMotionPosition, child);
1345                    }
1346                } else if (mSelectedPosition != INVALID_POSITION) {
1347                    // If we had previously positioned the selector somewhere,
1348                    // put it back there. It might not match up with the data,
1349                    // but it's transitioning out so it's not a big deal.
1350                    final View child = getChildAt(mSelectorPosition - mFirstPosition);
1351                    if (child != null) {
1352                        positionSelector(mSelectorPosition, child);
1353                    }
1354                } else {
1355                    // Otherwise, clear selection.
1356                    mSelectedTop = 0;
1357                    mSelectorRect.setEmpty();
1358                }
1359            }
1360
1361            // Attempt to restore accessibility focus, if necessary.
1362            if (viewRootImpl != null) {
1363                final View newAccessibilityFocusedView = viewRootImpl.getAccessibilityFocusedHost();
1364                if (newAccessibilityFocusedView == null) {
1365                    if (accessibilityFocusLayoutRestoreView != null
1366                            && accessibilityFocusLayoutRestoreView.isAttachedToWindow()) {
1367                        final AccessibilityNodeProvider provider =
1368                                accessibilityFocusLayoutRestoreView.getAccessibilityNodeProvider();
1369                        if (accessibilityFocusLayoutRestoreNode != null && provider != null) {
1370                            final int virtualViewId = AccessibilityNodeInfo.getVirtualDescendantId(
1371                                    accessibilityFocusLayoutRestoreNode.getSourceNodeId());
1372                            provider.performAction(virtualViewId,
1373                                    AccessibilityNodeInfo.ACTION_ACCESSIBILITY_FOCUS, null);
1374                        } else {
1375                            accessibilityFocusLayoutRestoreView.requestAccessibilityFocus();
1376                        }
1377                    } else if (accessibilityFocusPosition != INVALID_POSITION) {
1378                        // Bound the position within the visible children.
1379                        final int position = MathUtils.constrain(
1380                                accessibilityFocusPosition - mFirstPosition, 0,
1381                                getChildCount() - 1);
1382                        final View restoreView = getChildAt(position);
1383                        if (restoreView != null) {
1384                            restoreView.requestAccessibilityFocus();
1385                        }
1386                    }
1387                }
1388            }
1389
1390            mLayoutMode = LAYOUT_NORMAL;
1391            mDataChanged = false;
1392            if (mPositionScrollAfterLayout != null) {
1393                post(mPositionScrollAfterLayout);
1394                mPositionScrollAfterLayout = null;
1395            }
1396            mNeedSync = false;
1397            setNextSelectedPositionInt(mSelectedPosition);
1398
1399            updateScrollIndicators();
1400
1401            if (mItemCount > 0) {
1402                checkSelectionChanged();
1403            }
1404
1405            invokeOnItemScrollListener();
1406        } finally {
1407            if (!blockLayoutRequests) {
1408                mBlockLayoutRequests = false;
1409            }
1410        }
1411    }
1412
1413
1414    /**
1415     * Obtains the view and adds it to our list of children. The view can be
1416     * made fresh, converted from an unused view, or used as is if it was in
1417     * the recycle bin.
1418     *
1419     * @param position logical position in the list
1420     * @param y top or bottom edge of the view to add
1421     * @param flow {@code true} to align top edge to y, {@code false} to align
1422     *             bottom edge to y
1423     * @param childrenLeft left edge where children should be positioned
1424     * @param selected {@code true} if the position is selected, {@code false}
1425     *                 otherwise
1426     * @param where position at which to add new item in the list
1427     * @return View that was added
1428     */
1429    private View makeAndAddView(int position, int y, boolean flow, int childrenLeft,
1430            boolean selected, int where) {
1431        if (!mDataChanged) {
1432            // Try to use an existing view for this position
1433            final View activeView = mRecycler.getActiveView(position);
1434            if (activeView != null) {
1435                // Found it -- we're using an existing child
1436                // This just needs to be positioned
1437                setupChild(activeView, position, y, flow, childrenLeft, selected, true, where);
1438                return activeView;
1439            }
1440        }
1441
1442        // Make a new view for this position, or convert an unused view if
1443        // possible.
1444        final View child = obtainView(position, mIsScrap);
1445
1446        // This needs to be positioned and measured.
1447        setupChild(child, position, y, flow, childrenLeft, selected, mIsScrap[0], where);
1448
1449        return child;
1450    }
1451
1452    /**
1453     * Adds a view as a child and make sure it is measured (if necessary) and
1454     * positioned properly.
1455     *
1456     * @param child the view to add
1457     * @param position the position of this child
1458     * @param y the y position relative to which this view will be positioned
1459     * @param flowDown {@code true} to align top edge to y, {@code false} to
1460     *                 align bottom edge to y
1461     * @param childrenLeft left edge where children should be positioned
1462     * @param selected {@code true} if the position is selected, {@code false}
1463     *                 otherwise
1464     * @param isAttachedToWindow {@code true} if the view is already attached
1465     *                           to the window, e.g. whether it was reused, or
1466     *                           {@code false} otherwise
1467     * @param where position at which to add new item in the list
1468     *
1469     */
1470    private void setupChild(View child, int position, int y, boolean flowDown, int childrenLeft,
1471            boolean selected, boolean isAttachedToWindow, int where) {
1472        Trace.traceBegin(Trace.TRACE_TAG_VIEW, "setupGridItem");
1473
1474        boolean isSelected = selected && shouldShowSelector();
1475        final boolean updateChildSelected = isSelected != child.isSelected();
1476        final int mode = mTouchMode;
1477        final boolean isPressed = mode > TOUCH_MODE_DOWN && mode < TOUCH_MODE_SCROLL
1478                && mMotionPosition == position;
1479        final boolean updateChildPressed = isPressed != child.isPressed();
1480        final boolean needToMeasure = !isAttachedToWindow || updateChildSelected
1481                || child.isLayoutRequested();
1482
1483        // Respect layout params that are already in the view. Otherwise make
1484        // some up...
1485        AbsListView.LayoutParams p = (AbsListView.LayoutParams) child.getLayoutParams();
1486        if (p == null) {
1487            p = (AbsListView.LayoutParams) generateDefaultLayoutParams();
1488        }
1489        p.viewType = mAdapter.getItemViewType(position);
1490        p.isEnabled = mAdapter.isEnabled(position);
1491
1492        // Set up view state before attaching the view, since we may need to
1493        // rely on the jumpDrawablesToCurrentState() call that occurs as part
1494        // of view attachment.
1495        if (updateChildSelected) {
1496            child.setSelected(isSelected);
1497            if (isSelected) {
1498                requestFocus();
1499            }
1500        }
1501
1502        if (updateChildPressed) {
1503            child.setPressed(isPressed);
1504        }
1505
1506        if (mChoiceMode != CHOICE_MODE_NONE && mCheckStates != null) {
1507            if (child instanceof Checkable) {
1508                ((Checkable) child).setChecked(mCheckStates.get(position));
1509            } else if (getContext().getApplicationInfo().targetSdkVersion
1510                    >= android.os.Build.VERSION_CODES.HONEYCOMB) {
1511                child.setActivated(mCheckStates.get(position));
1512            }
1513        }
1514
1515        if (isAttachedToWindow && !p.forceAdd) {
1516            attachViewToParent(child, where, p);
1517
1518            // If the view isn't attached, or if it's attached but for a different
1519            // position, then jump the drawables.
1520            if (!isAttachedToWindow
1521                    || (((AbsListView.LayoutParams) child.getLayoutParams()).scrappedFromPosition)
1522                            != position) {
1523                child.jumpDrawablesToCurrentState();
1524            }
1525        } else {
1526            p.forceAdd = false;
1527            addViewInLayout(child, where, p, true);
1528        }
1529
1530        if (needToMeasure) {
1531            int childHeightSpec = ViewGroup.getChildMeasureSpec(
1532                    MeasureSpec.makeMeasureSpec(0, MeasureSpec.UNSPECIFIED), 0, p.height);
1533
1534            int childWidthSpec = ViewGroup.getChildMeasureSpec(
1535                    MeasureSpec.makeMeasureSpec(mColumnWidth, MeasureSpec.EXACTLY), 0, p.width);
1536            child.measure(childWidthSpec, childHeightSpec);
1537        } else {
1538            cleanupLayoutState(child);
1539        }
1540
1541        final int w = child.getMeasuredWidth();
1542        final int h = child.getMeasuredHeight();
1543
1544        int childLeft;
1545        final int childTop = flowDown ? y : y - h;
1546
1547        final int layoutDirection = getLayoutDirection();
1548        final int absoluteGravity = Gravity.getAbsoluteGravity(mGravity, layoutDirection);
1549        switch (absoluteGravity & Gravity.HORIZONTAL_GRAVITY_MASK) {
1550            case Gravity.LEFT:
1551                childLeft = childrenLeft;
1552                break;
1553            case Gravity.CENTER_HORIZONTAL:
1554                childLeft = childrenLeft + ((mColumnWidth - w) / 2);
1555                break;
1556            case Gravity.RIGHT:
1557                childLeft = childrenLeft + mColumnWidth - w;
1558                break;
1559            default:
1560                childLeft = childrenLeft;
1561                break;
1562        }
1563
1564        if (needToMeasure) {
1565            final int childRight = childLeft + w;
1566            final int childBottom = childTop + h;
1567            child.layout(childLeft, childTop, childRight, childBottom);
1568        } else {
1569            child.offsetLeftAndRight(childLeft - child.getLeft());
1570            child.offsetTopAndBottom(childTop - child.getTop());
1571        }
1572
1573        if (mCachingStarted && !child.isDrawingCacheEnabled()) {
1574            child.setDrawingCacheEnabled(true);
1575        }
1576
1577        Trace.traceEnd(Trace.TRACE_TAG_VIEW);
1578    }
1579
1580    /**
1581     * Sets the currently selected item
1582     *
1583     * @param position Index (starting at 0) of the data item to be selected.
1584     *
1585     * If in touch mode, the item will not be selected but it will still be positioned
1586     * appropriately.
1587     */
1588    @Override
1589    public void setSelection(int position) {
1590        if (!isInTouchMode()) {
1591            setNextSelectedPositionInt(position);
1592        } else {
1593            mResurrectToPosition = position;
1594        }
1595        mLayoutMode = LAYOUT_SET_SELECTION;
1596        if (mPositionScroller != null) {
1597            mPositionScroller.stop();
1598        }
1599        requestLayout();
1600    }
1601
1602    /**
1603     * Makes the item at the supplied position selected.
1604     *
1605     * @param position the position of the new selection
1606     */
1607    @Override
1608    void setSelectionInt(int position) {
1609        int previousSelectedPosition = mNextSelectedPosition;
1610
1611        if (mPositionScroller != null) {
1612            mPositionScroller.stop();
1613        }
1614
1615        setNextSelectedPositionInt(position);
1616        layoutChildren();
1617
1618        final int next = mStackFromBottom ? mItemCount - 1  - mNextSelectedPosition :
1619            mNextSelectedPosition;
1620        final int previous = mStackFromBottom ? mItemCount - 1
1621                - previousSelectedPosition : previousSelectedPosition;
1622
1623        final int nextRow = next / mNumColumns;
1624        final int previousRow = previous / mNumColumns;
1625
1626        if (nextRow != previousRow) {
1627            awakenScrollBars();
1628        }
1629
1630    }
1631
1632    @Override
1633    public boolean onKeyDown(int keyCode, KeyEvent event) {
1634        return commonKey(keyCode, 1, event);
1635    }
1636
1637    @Override
1638    public boolean onKeyMultiple(int keyCode, int repeatCount, KeyEvent event) {
1639        return commonKey(keyCode, repeatCount, event);
1640    }
1641
1642    @Override
1643    public boolean onKeyUp(int keyCode, KeyEvent event) {
1644        return commonKey(keyCode, 1, event);
1645    }
1646
1647    private boolean commonKey(int keyCode, int count, KeyEvent event) {
1648        if (mAdapter == null) {
1649            return false;
1650        }
1651
1652        if (mDataChanged) {
1653            layoutChildren();
1654        }
1655
1656        boolean handled = false;
1657        int action = event.getAction();
1658        if (KeyEvent.isConfirmKey(keyCode)
1659                && event.hasNoModifiers() && action != KeyEvent.ACTION_UP) {
1660            handled = resurrectSelectionIfNeeded();
1661            if (!handled && event.getRepeatCount() == 0 && getChildCount() > 0) {
1662                keyPressed();
1663                handled = true;
1664            }
1665        }
1666
1667        if (!handled && action != KeyEvent.ACTION_UP) {
1668            switch (keyCode) {
1669                case KeyEvent.KEYCODE_DPAD_LEFT:
1670                    if (event.hasNoModifiers()) {
1671                        handled = resurrectSelectionIfNeeded() || arrowScroll(FOCUS_LEFT);
1672                    }
1673                    break;
1674
1675                case KeyEvent.KEYCODE_DPAD_RIGHT:
1676                    if (event.hasNoModifiers()) {
1677                        handled = resurrectSelectionIfNeeded() || arrowScroll(FOCUS_RIGHT);
1678                    }
1679                    break;
1680
1681                case KeyEvent.KEYCODE_DPAD_UP:
1682                    if (event.hasNoModifiers()) {
1683                        handled = resurrectSelectionIfNeeded() || arrowScroll(FOCUS_UP);
1684                    } else if (event.hasModifiers(KeyEvent.META_ALT_ON)) {
1685                        handled = resurrectSelectionIfNeeded() || fullScroll(FOCUS_UP);
1686                    }
1687                    break;
1688
1689                case KeyEvent.KEYCODE_DPAD_DOWN:
1690                    if (event.hasNoModifiers()) {
1691                        handled = resurrectSelectionIfNeeded() || arrowScroll(FOCUS_DOWN);
1692                    } else if (event.hasModifiers(KeyEvent.META_ALT_ON)) {
1693                        handled = resurrectSelectionIfNeeded() || fullScroll(FOCUS_DOWN);
1694                    }
1695                    break;
1696
1697                case KeyEvent.KEYCODE_PAGE_UP:
1698                    if (event.hasNoModifiers()) {
1699                        handled = resurrectSelectionIfNeeded() || pageScroll(FOCUS_UP);
1700                    } else if (event.hasModifiers(KeyEvent.META_ALT_ON)) {
1701                        handled = resurrectSelectionIfNeeded() || fullScroll(FOCUS_UP);
1702                    }
1703                    break;
1704
1705                case KeyEvent.KEYCODE_PAGE_DOWN:
1706                    if (event.hasNoModifiers()) {
1707                        handled = resurrectSelectionIfNeeded() || pageScroll(FOCUS_DOWN);
1708                    } else if (event.hasModifiers(KeyEvent.META_ALT_ON)) {
1709                        handled = resurrectSelectionIfNeeded() || fullScroll(FOCUS_DOWN);
1710                    }
1711                    break;
1712
1713                case KeyEvent.KEYCODE_MOVE_HOME:
1714                    if (event.hasNoModifiers()) {
1715                        handled = resurrectSelectionIfNeeded() || fullScroll(FOCUS_UP);
1716                    }
1717                    break;
1718
1719                case KeyEvent.KEYCODE_MOVE_END:
1720                    if (event.hasNoModifiers()) {
1721                        handled = resurrectSelectionIfNeeded() || fullScroll(FOCUS_DOWN);
1722                    }
1723                    break;
1724
1725                case KeyEvent.KEYCODE_TAB:
1726                    // TODO: Sometimes it is useful to be able to TAB through the items in
1727                    //     a GridView sequentially.  Unfortunately this can create an
1728                    //     asymmetry in TAB navigation order unless the list selection
1729                    //     always reverts to the top or bottom when receiving TAB focus from
1730                    //     another widget.
1731                    if (event.hasNoModifiers()) {
1732                        handled = resurrectSelectionIfNeeded()
1733                                || sequenceScroll(FOCUS_FORWARD);
1734                    } else if (event.hasModifiers(KeyEvent.META_SHIFT_ON)) {
1735                        handled = resurrectSelectionIfNeeded()
1736                                || sequenceScroll(FOCUS_BACKWARD);
1737                    }
1738                    break;
1739            }
1740        }
1741
1742        if (handled) {
1743            return true;
1744        }
1745
1746        if (sendToTextFilter(keyCode, count, event)) {
1747            return true;
1748        }
1749
1750        switch (action) {
1751            case KeyEvent.ACTION_DOWN:
1752                return super.onKeyDown(keyCode, event);
1753            case KeyEvent.ACTION_UP:
1754                return super.onKeyUp(keyCode, event);
1755            case KeyEvent.ACTION_MULTIPLE:
1756                return super.onKeyMultiple(keyCode, count, event);
1757            default:
1758                return false;
1759        }
1760    }
1761
1762    /**
1763     * Scrolls up or down by the number of items currently present on screen.
1764     *
1765     * @param direction either {@link View#FOCUS_UP} or {@link View#FOCUS_DOWN}
1766     * @return whether selection was moved
1767     */
1768    boolean pageScroll(int direction) {
1769        int nextPage = -1;
1770
1771        if (direction == FOCUS_UP) {
1772            nextPage = Math.max(0, mSelectedPosition - getChildCount());
1773        } else if (direction == FOCUS_DOWN) {
1774            nextPage = Math.min(mItemCount - 1, mSelectedPosition + getChildCount());
1775        }
1776
1777        if (nextPage >= 0) {
1778            setSelectionInt(nextPage);
1779            invokeOnItemScrollListener();
1780            awakenScrollBars();
1781            return true;
1782        }
1783
1784        return false;
1785    }
1786
1787    /**
1788     * Go to the last or first item if possible.
1789     *
1790     * @param direction either {@link View#FOCUS_UP} or {@link View#FOCUS_DOWN}.
1791     *
1792     * @return Whether selection was moved.
1793     */
1794    boolean fullScroll(int direction) {
1795        boolean moved = false;
1796        if (direction == FOCUS_UP) {
1797            mLayoutMode = LAYOUT_SET_SELECTION;
1798            setSelectionInt(0);
1799            invokeOnItemScrollListener();
1800            moved = true;
1801        } else if (direction == FOCUS_DOWN) {
1802            mLayoutMode = LAYOUT_SET_SELECTION;
1803            setSelectionInt(mItemCount - 1);
1804            invokeOnItemScrollListener();
1805            moved = true;
1806        }
1807
1808        if (moved) {
1809            awakenScrollBars();
1810        }
1811
1812        return moved;
1813    }
1814
1815    /**
1816     * Scrolls to the next or previous item, horizontally or vertically.
1817     *
1818     * @param direction either {@link View#FOCUS_LEFT}, {@link View#FOCUS_RIGHT},
1819     *        {@link View#FOCUS_UP} or {@link View#FOCUS_DOWN}
1820     *
1821     * @return whether selection was moved
1822     */
1823    boolean arrowScroll(int direction) {
1824        final int selectedPosition = mSelectedPosition;
1825        final int numColumns = mNumColumns;
1826
1827        int startOfRowPos;
1828        int endOfRowPos;
1829
1830        boolean moved = false;
1831
1832        if (!mStackFromBottom) {
1833            startOfRowPos = (selectedPosition / numColumns) * numColumns;
1834            endOfRowPos = Math.min(startOfRowPos + numColumns - 1, mItemCount - 1);
1835        } else {
1836            final int invertedSelection = mItemCount - 1 - selectedPosition;
1837            endOfRowPos = mItemCount - 1 - (invertedSelection / numColumns) * numColumns;
1838            startOfRowPos = Math.max(0, endOfRowPos - numColumns + 1);
1839        }
1840
1841        switch (direction) {
1842            case FOCUS_UP:
1843                if (startOfRowPos > 0) {
1844                    mLayoutMode = LAYOUT_MOVE_SELECTION;
1845                    setSelectionInt(Math.max(0, selectedPosition - numColumns));
1846                    moved = true;
1847                }
1848                break;
1849            case FOCUS_DOWN:
1850                if (endOfRowPos < mItemCount - 1) {
1851                    mLayoutMode = LAYOUT_MOVE_SELECTION;
1852                    setSelectionInt(Math.min(selectedPosition + numColumns, mItemCount - 1));
1853                    moved = true;
1854                }
1855                break;
1856        }
1857
1858        final boolean isLayoutRtl = isLayoutRtl();
1859        if (selectedPosition > startOfRowPos && ((direction == FOCUS_LEFT && !isLayoutRtl) ||
1860                (direction == FOCUS_RIGHT && isLayoutRtl))) {
1861            mLayoutMode = LAYOUT_MOVE_SELECTION;
1862            setSelectionInt(Math.max(0, selectedPosition - 1));
1863            moved = true;
1864        } else if (selectedPosition < endOfRowPos && ((direction == FOCUS_LEFT && isLayoutRtl) ||
1865                (direction == FOCUS_RIGHT && !isLayoutRtl))) {
1866            mLayoutMode = LAYOUT_MOVE_SELECTION;
1867            setSelectionInt(Math.min(selectedPosition + 1, mItemCount - 1));
1868            moved = true;
1869        }
1870
1871        if (moved) {
1872            playSoundEffect(SoundEffectConstants.getContantForFocusDirection(direction));
1873            invokeOnItemScrollListener();
1874        }
1875
1876        if (moved) {
1877            awakenScrollBars();
1878        }
1879
1880        return moved;
1881    }
1882
1883    /**
1884     * Goes to the next or previous item according to the order set by the
1885     * adapter.
1886     */
1887    boolean sequenceScroll(int direction) {
1888        int selectedPosition = mSelectedPosition;
1889        int numColumns = mNumColumns;
1890        int count = mItemCount;
1891
1892        int startOfRow;
1893        int endOfRow;
1894        if (!mStackFromBottom) {
1895            startOfRow = (selectedPosition / numColumns) * numColumns;
1896            endOfRow = Math.min(startOfRow + numColumns - 1, count - 1);
1897        } else {
1898            int invertedSelection = count - 1 - selectedPosition;
1899            endOfRow = count - 1 - (invertedSelection / numColumns) * numColumns;
1900            startOfRow = Math.max(0, endOfRow - numColumns + 1);
1901        }
1902
1903        boolean moved = false;
1904        boolean showScroll = false;
1905        switch (direction) {
1906            case FOCUS_FORWARD:
1907                if (selectedPosition < count - 1) {
1908                    // Move to the next item.
1909                    mLayoutMode = LAYOUT_MOVE_SELECTION;
1910                    setSelectionInt(selectedPosition + 1);
1911                    moved = true;
1912                    // Show the scrollbar only if changing rows.
1913                    showScroll = selectedPosition == endOfRow;
1914                }
1915                break;
1916
1917            case FOCUS_BACKWARD:
1918                if (selectedPosition > 0) {
1919                    // Move to the previous item.
1920                    mLayoutMode = LAYOUT_MOVE_SELECTION;
1921                    setSelectionInt(selectedPosition - 1);
1922                    moved = true;
1923                    // Show the scrollbar only if changing rows.
1924                    showScroll = selectedPosition == startOfRow;
1925                }
1926                break;
1927        }
1928
1929        if (moved) {
1930            playSoundEffect(SoundEffectConstants.getContantForFocusDirection(direction));
1931            invokeOnItemScrollListener();
1932        }
1933
1934        if (showScroll) {
1935            awakenScrollBars();
1936        }
1937
1938        return moved;
1939    }
1940
1941    @Override
1942    protected void onFocusChanged(boolean gainFocus, int direction, Rect previouslyFocusedRect) {
1943        super.onFocusChanged(gainFocus, direction, previouslyFocusedRect);
1944
1945        int closestChildIndex = -1;
1946        if (gainFocus && previouslyFocusedRect != null) {
1947            previouslyFocusedRect.offset(mScrollX, mScrollY);
1948
1949            // figure out which item should be selected based on previously
1950            // focused rect
1951            Rect otherRect = mTempRect;
1952            int minDistance = Integer.MAX_VALUE;
1953            final int childCount = getChildCount();
1954            for (int i = 0; i < childCount; i++) {
1955                // only consider view's on appropriate edge of grid
1956                if (!isCandidateSelection(i, direction)) {
1957                    continue;
1958                }
1959
1960                final View other = getChildAt(i);
1961                other.getDrawingRect(otherRect);
1962                offsetDescendantRectToMyCoords(other, otherRect);
1963                int distance = getDistance(previouslyFocusedRect, otherRect, direction);
1964
1965                if (distance < minDistance) {
1966                    minDistance = distance;
1967                    closestChildIndex = i;
1968                }
1969            }
1970        }
1971
1972        if (closestChildIndex >= 0) {
1973            setSelection(closestChildIndex + mFirstPosition);
1974        } else {
1975            requestLayout();
1976        }
1977    }
1978
1979    /**
1980     * Is childIndex a candidate for next focus given the direction the focus
1981     * change is coming from?
1982     * @param childIndex The index to check.
1983     * @param direction The direction, one of
1984     *        {FOCUS_UP, FOCUS_DOWN, FOCUS_LEFT, FOCUS_RIGHT, FOCUS_FORWARD, FOCUS_BACKWARD}
1985     * @return Whether childIndex is a candidate.
1986     */
1987    private boolean isCandidateSelection(int childIndex, int direction) {
1988        final int count = getChildCount();
1989        final int invertedIndex = count - 1 - childIndex;
1990
1991        int rowStart;
1992        int rowEnd;
1993
1994        if (!mStackFromBottom) {
1995            rowStart = childIndex - (childIndex % mNumColumns);
1996            rowEnd = Math.min(rowStart + mNumColumns - 1, count);
1997        } else {
1998            rowEnd = count - 1 - (invertedIndex - (invertedIndex % mNumColumns));
1999            rowStart = Math.max(0, rowEnd - mNumColumns + 1);
2000        }
2001
2002        switch (direction) {
2003            case View.FOCUS_RIGHT:
2004                // coming from left, selection is only valid if it is on left
2005                // edge
2006                return childIndex == rowStart;
2007            case View.FOCUS_DOWN:
2008                // coming from top; only valid if in top row
2009                return rowStart == 0;
2010            case View.FOCUS_LEFT:
2011                // coming from right, must be on right edge
2012                return childIndex == rowEnd;
2013            case View.FOCUS_UP:
2014                // coming from bottom, need to be in last row
2015                return rowEnd == count - 1;
2016            case View.FOCUS_FORWARD:
2017                // coming from top-left, need to be first in top row
2018                return childIndex == rowStart && rowStart == 0;
2019            case View.FOCUS_BACKWARD:
2020                // coming from bottom-right, need to be last in bottom row
2021                return childIndex == rowEnd && rowEnd == count - 1;
2022            default:
2023                throw new IllegalArgumentException("direction must be one of "
2024                        + "{FOCUS_UP, FOCUS_DOWN, FOCUS_LEFT, FOCUS_RIGHT, "
2025                        + "FOCUS_FORWARD, FOCUS_BACKWARD}.");
2026        }
2027    }
2028
2029    /**
2030     * Set the gravity for this grid. Gravity describes how the child views
2031     * are horizontally aligned. Defaults to Gravity.LEFT
2032     *
2033     * @param gravity the gravity to apply to this grid's children
2034     *
2035     * @attr ref android.R.styleable#GridView_gravity
2036     */
2037    public void setGravity(int gravity) {
2038        if (mGravity != gravity) {
2039            mGravity = gravity;
2040            requestLayoutIfNecessary();
2041        }
2042    }
2043
2044    /**
2045     * Describes how the child views are horizontally aligned. Defaults to Gravity.LEFT
2046     *
2047     * @return the gravity that will be applied to this grid's children
2048     *
2049     * @attr ref android.R.styleable#GridView_gravity
2050     */
2051    public int getGravity() {
2052        return mGravity;
2053    }
2054
2055    /**
2056     * Set the amount of horizontal (x) spacing to place between each item
2057     * in the grid.
2058     *
2059     * @param horizontalSpacing The amount of horizontal space between items,
2060     * in pixels.
2061     *
2062     * @attr ref android.R.styleable#GridView_horizontalSpacing
2063     */
2064    public void setHorizontalSpacing(int horizontalSpacing) {
2065        if (horizontalSpacing != mRequestedHorizontalSpacing) {
2066            mRequestedHorizontalSpacing = horizontalSpacing;
2067            requestLayoutIfNecessary();
2068        }
2069    }
2070
2071    /**
2072     * Returns the amount of horizontal spacing currently used between each item in the grid.
2073     *
2074     * <p>This is only accurate for the current layout. If {@link #setHorizontalSpacing(int)}
2075     * has been called but layout is not yet complete, this method may return a stale value.
2076     * To get the horizontal spacing that was explicitly requested use
2077     * {@link #getRequestedHorizontalSpacing()}.</p>
2078     *
2079     * @return Current horizontal spacing between each item in pixels
2080     *
2081     * @see #setHorizontalSpacing(int)
2082     * @see #getRequestedHorizontalSpacing()
2083     *
2084     * @attr ref android.R.styleable#GridView_horizontalSpacing
2085     */
2086    public int getHorizontalSpacing() {
2087        return mHorizontalSpacing;
2088    }
2089
2090    /**
2091     * Returns the requested amount of horizontal spacing between each item in the grid.
2092     *
2093     * <p>The value returned may have been supplied during inflation as part of a style,
2094     * the default GridView style, or by a call to {@link #setHorizontalSpacing(int)}.
2095     * If layout is not yet complete or if GridView calculated a different horizontal spacing
2096     * from what was requested, this may return a different value from
2097     * {@link #getHorizontalSpacing()}.</p>
2098     *
2099     * @return The currently requested horizontal spacing between items, in pixels
2100     *
2101     * @see #setHorizontalSpacing(int)
2102     * @see #getHorizontalSpacing()
2103     *
2104     * @attr ref android.R.styleable#GridView_horizontalSpacing
2105     */
2106    public int getRequestedHorizontalSpacing() {
2107        return mRequestedHorizontalSpacing;
2108    }
2109
2110    /**
2111     * Set the amount of vertical (y) spacing to place between each item
2112     * in the grid.
2113     *
2114     * @param verticalSpacing The amount of vertical space between items,
2115     * in pixels.
2116     *
2117     * @see #getVerticalSpacing()
2118     *
2119     * @attr ref android.R.styleable#GridView_verticalSpacing
2120     */
2121    public void setVerticalSpacing(int verticalSpacing) {
2122        if (verticalSpacing != mVerticalSpacing) {
2123            mVerticalSpacing = verticalSpacing;
2124            requestLayoutIfNecessary();
2125        }
2126    }
2127
2128    /**
2129     * Returns the amount of vertical spacing between each item in the grid.
2130     *
2131     * @return The vertical spacing between items in pixels
2132     *
2133     * @see #setVerticalSpacing(int)
2134     *
2135     * @attr ref android.R.styleable#GridView_verticalSpacing
2136     */
2137    public int getVerticalSpacing() {
2138        return mVerticalSpacing;
2139    }
2140
2141    /**
2142     * Control how items are stretched to fill their space.
2143     *
2144     * @param stretchMode Either {@link #NO_STRETCH},
2145     * {@link #STRETCH_SPACING}, {@link #STRETCH_SPACING_UNIFORM}, or {@link #STRETCH_COLUMN_WIDTH}.
2146     *
2147     * @attr ref android.R.styleable#GridView_stretchMode
2148     */
2149    public void setStretchMode(@StretchMode int stretchMode) {
2150        if (stretchMode != mStretchMode) {
2151            mStretchMode = stretchMode;
2152            requestLayoutIfNecessary();
2153        }
2154    }
2155
2156    @StretchMode
2157    public int getStretchMode() {
2158        return mStretchMode;
2159    }
2160
2161    /**
2162     * Set the width of columns in the grid.
2163     *
2164     * @param columnWidth The column width, in pixels.
2165     *
2166     * @attr ref android.R.styleable#GridView_columnWidth
2167     */
2168    public void setColumnWidth(int columnWidth) {
2169        if (columnWidth != mRequestedColumnWidth) {
2170            mRequestedColumnWidth = columnWidth;
2171            requestLayoutIfNecessary();
2172        }
2173    }
2174
2175    /**
2176     * Return the width of a column in the grid.
2177     *
2178     * <p>This may not be valid yet if a layout is pending.</p>
2179     *
2180     * @return The column width in pixels
2181     *
2182     * @see #setColumnWidth(int)
2183     * @see #getRequestedColumnWidth()
2184     *
2185     * @attr ref android.R.styleable#GridView_columnWidth
2186     */
2187    public int getColumnWidth() {
2188        return mColumnWidth;
2189    }
2190
2191    /**
2192     * Return the requested width of a column in the grid.
2193     *
2194     * <p>This may not be the actual column width used. Use {@link #getColumnWidth()}
2195     * to retrieve the current real width of a column.</p>
2196     *
2197     * @return The requested column width in pixels
2198     *
2199     * @see #setColumnWidth(int)
2200     * @see #getColumnWidth()
2201     *
2202     * @attr ref android.R.styleable#GridView_columnWidth
2203     */
2204    public int getRequestedColumnWidth() {
2205        return mRequestedColumnWidth;
2206    }
2207
2208    /**
2209     * Set the number of columns in the grid
2210     *
2211     * @param numColumns The desired number of columns.
2212     *
2213     * @attr ref android.R.styleable#GridView_numColumns
2214     */
2215    public void setNumColumns(int numColumns) {
2216        if (numColumns != mRequestedNumColumns) {
2217            mRequestedNumColumns = numColumns;
2218            requestLayoutIfNecessary();
2219        }
2220    }
2221
2222    /**
2223     * Get the number of columns in the grid.
2224     * Returns {@link #AUTO_FIT} if the Grid has never been laid out.
2225     *
2226     * @attr ref android.R.styleable#GridView_numColumns
2227     *
2228     * @see #setNumColumns(int)
2229     */
2230    @ViewDebug.ExportedProperty
2231    public int getNumColumns() {
2232        return mNumColumns;
2233    }
2234
2235    /**
2236     * Make sure views are touching the top or bottom edge, as appropriate for
2237     * our gravity
2238     */
2239    private void adjustViewsUpOrDown() {
2240        final int childCount = getChildCount();
2241
2242        if (childCount > 0) {
2243            int delta;
2244            View child;
2245
2246            if (!mStackFromBottom) {
2247                // Uh-oh -- we came up short. Slide all views up to make them
2248                // align with the top
2249                child = getChildAt(0);
2250                delta = child.getTop() - mListPadding.top;
2251                if (mFirstPosition != 0) {
2252                    // It's OK to have some space above the first item if it is
2253                    // part of the vertical spacing
2254                    delta -= mVerticalSpacing;
2255                }
2256                if (delta < 0) {
2257                    // We only are looking to see if we are too low, not too high
2258                    delta = 0;
2259                }
2260            } else {
2261                // we are too high, slide all views down to align with bottom
2262                child = getChildAt(childCount - 1);
2263                delta = child.getBottom() - (getHeight() - mListPadding.bottom);
2264
2265                if (mFirstPosition + childCount < mItemCount) {
2266                    // It's OK to have some space below the last item if it is
2267                    // part of the vertical spacing
2268                    delta += mVerticalSpacing;
2269                }
2270
2271                if (delta > 0) {
2272                    // We only are looking to see if we are too high, not too low
2273                    delta = 0;
2274                }
2275            }
2276
2277            if (delta != 0) {
2278                offsetChildrenTopAndBottom(-delta);
2279            }
2280        }
2281    }
2282
2283    @Override
2284    protected int computeVerticalScrollExtent() {
2285        final int count = getChildCount();
2286        if (count > 0) {
2287            final int numColumns = mNumColumns;
2288            final int rowCount = (count + numColumns - 1) / numColumns;
2289
2290            int extent = rowCount * 100;
2291
2292            View view = getChildAt(0);
2293            final int top = view.getTop();
2294            int height = view.getHeight();
2295            if (height > 0) {
2296                extent += (top * 100) / height;
2297            }
2298
2299            view = getChildAt(count - 1);
2300            final int bottom = view.getBottom();
2301            height = view.getHeight();
2302            if (height > 0) {
2303                extent -= ((bottom - getHeight()) * 100) / height;
2304            }
2305
2306            return extent;
2307        }
2308        return 0;
2309    }
2310
2311    @Override
2312    protected int computeVerticalScrollOffset() {
2313        if (mFirstPosition >= 0 && getChildCount() > 0) {
2314            final View view = getChildAt(0);
2315            final int top = view.getTop();
2316            int height = view.getHeight();
2317            if (height > 0) {
2318                final int numColumns = mNumColumns;
2319                final int rowCount = (mItemCount + numColumns - 1) / numColumns;
2320                // In case of stackFromBottom the calculation of whichRow needs
2321                // to take into account that counting from the top the first row
2322                // might not be entirely filled.
2323                final int oddItemsOnFirstRow = isStackFromBottom() ? ((rowCount * numColumns) -
2324                        mItemCount) : 0;
2325                final int whichRow = (mFirstPosition + oddItemsOnFirstRow) / numColumns;
2326                return Math.max(whichRow * 100 - (top * 100) / height +
2327                        (int) ((float) mScrollY / getHeight() * rowCount * 100), 0);
2328            }
2329        }
2330        return 0;
2331    }
2332
2333    @Override
2334    protected int computeVerticalScrollRange() {
2335        // TODO: Account for vertical spacing too
2336        final int numColumns = mNumColumns;
2337        final int rowCount = (mItemCount + numColumns - 1) / numColumns;
2338        int result = Math.max(rowCount * 100, 0);
2339        if (mScrollY != 0) {
2340            // Compensate for overscroll
2341            result += Math.abs((int) ((float) mScrollY / getHeight() * rowCount * 100));
2342        }
2343        return result;
2344    }
2345
2346    @Override
2347    public CharSequence getAccessibilityClassName() {
2348        return GridView.class.getName();
2349    }
2350
2351    /** @hide */
2352    @Override
2353    public void onInitializeAccessibilityNodeInfoInternal(AccessibilityNodeInfo info) {
2354        super.onInitializeAccessibilityNodeInfoInternal(info);
2355
2356        final int columnsCount = getNumColumns();
2357        final int rowsCount = getCount() / columnsCount;
2358        final int selectionMode = getSelectionModeForAccessibility();
2359        final CollectionInfo collectionInfo = CollectionInfo.obtain(
2360                rowsCount, columnsCount, false, selectionMode);
2361        info.setCollectionInfo(collectionInfo);
2362
2363        if (columnsCount > 0 || rowsCount > 0) {
2364            info.addAction(AccessibilityAction.ACTION_SCROLL_TO_POSITION);
2365        }
2366    }
2367
2368    /** @hide */
2369    @Override
2370    public boolean performAccessibilityActionInternal(int action, Bundle arguments) {
2371        if (super.performAccessibilityActionInternal(action, arguments)) {
2372            return true;
2373        }
2374
2375        switch (action) {
2376            case R.id.accessibilityActionScrollToPosition: {
2377                // GridView only supports scrolling in one direction, so we can
2378                // ignore the column argument.
2379                final int numColumns = getNumColumns();
2380                final int row = arguments.getInt(AccessibilityNodeInfo.ACTION_ARGUMENT_ROW_INT, -1);
2381                final int position = Math.min(row * numColumns, getCount() - 1);
2382                if (row >= 0) {
2383                    // The accessibility service gets data asynchronously, so
2384                    // we'll be a little lenient by clamping the last position.
2385                    smoothScrollToPosition(position);
2386                    return true;
2387                }
2388            } break;
2389        }
2390
2391        return false;
2392    }
2393
2394    @Override
2395    public void onInitializeAccessibilityNodeInfoForItem(
2396            View view, int position, AccessibilityNodeInfo info) {
2397        super.onInitializeAccessibilityNodeInfoForItem(view, position, info);
2398
2399        final int count = getCount();
2400        final int columnsCount = getNumColumns();
2401        final int rowsCount = count / columnsCount;
2402
2403        final int row;
2404        final int column;
2405        if (!mStackFromBottom) {
2406            column = position % columnsCount;
2407            row = position / columnsCount;
2408        } else {
2409            final int invertedIndex = count - 1 - position;
2410
2411            column = columnsCount - 1 - (invertedIndex % columnsCount);
2412            row = rowsCount - 1 - invertedIndex / columnsCount;
2413        }
2414
2415        final LayoutParams lp = (LayoutParams) view.getLayoutParams();
2416        final boolean isHeading = lp != null && lp.viewType == ITEM_VIEW_TYPE_HEADER_OR_FOOTER;
2417        final boolean isSelected = isItemChecked(position);
2418        final CollectionItemInfo itemInfo = CollectionItemInfo.obtain(
2419                row, 1, column, 1, isHeading, isSelected);
2420        info.setCollectionItemInfo(itemInfo);
2421    }
2422
2423    /** @hide */
2424    @Override
2425    protected void encodeProperties(@NonNull ViewHierarchyEncoder encoder) {
2426        super.encodeProperties(encoder);
2427        encoder.addProperty("numColumns", getNumColumns());
2428    }
2429}
2430