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