GridView.java revision 3dec7d563a2f3e1eb967ce2054a00b6620e3558c
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.content.Context;
20import android.content.res.TypedArray;
21import android.graphics.Rect;
22import android.util.AttributeSet;
23import android.view.Gravity;
24import android.view.KeyEvent;
25import android.view.View;
26import android.view.ViewGroup;
27import android.view.SoundEffectConstants;
28import android.view.animation.GridLayoutAnimationController;
29
30
31/**
32 * A view that shows items in two-dimensional scrolling grid. The items in the
33 * grid come from the {@link ListAdapter} associated with this view.
34 */
35public class GridView extends AbsListView {
36    public static final int NO_STRETCH = 0;
37    public static final int STRETCH_SPACING = 1;
38    public static final int STRETCH_COLUMN_WIDTH = 2;
39    public static final int STRETCH_SPACING_UNIFORM = 3;
40
41    public static final int AUTO_FIT = -1;
42
43    private int mNumColumns = AUTO_FIT;
44
45    private int mHorizontalSpacing = 0;
46    private int mRequestedHorizontalSpacing;
47    private int mVerticalSpacing = 0;
48    private int mStretchMode = STRETCH_COLUMN_WIDTH;
49    private int mColumnWidth;
50    private int mRequestedColumnWidth;
51    private int mRequestedNumColumns;
52
53    private View mReferenceView = null;
54    private View mReferenceViewInSelectedRow = null;
55
56    private int mGravity = Gravity.LEFT;
57
58    private final Rect mTempRect = new Rect();
59
60    public GridView(Context context) {
61        super(context);
62    }
63
64    public GridView(Context context, AttributeSet attrs) {
65        this(context, attrs, com.android.internal.R.attr.gridViewStyle);
66    }
67
68    public GridView(Context context, AttributeSet attrs, int defStyle) {
69        super(context, attrs, defStyle);
70
71        TypedArray a = context.obtainStyledAttributes(attrs,
72                com.android.internal.R.styleable.GridView, defStyle, 0);
73
74        int hSpacing = a.getDimensionPixelOffset(
75                com.android.internal.R.styleable.GridView_horizontalSpacing, 0);
76        setHorizontalSpacing(hSpacing);
77
78        int vSpacing = a.getDimensionPixelOffset(
79                com.android.internal.R.styleable.GridView_verticalSpacing, 0);
80        setVerticalSpacing(vSpacing);
81
82        int index = a.getInt(com.android.internal.R.styleable.GridView_stretchMode, STRETCH_COLUMN_WIDTH);
83        if (index >= 0) {
84            setStretchMode(index);
85        }
86
87        int columnWidth = a.getDimensionPixelOffset(com.android.internal.R.styleable.GridView_columnWidth, -1);
88        if (columnWidth > 0) {
89            setColumnWidth(columnWidth);
90        }
91
92        int numColumns = a.getInt(com.android.internal.R.styleable.GridView_numColumns, 1);
93        setNumColumns(numColumns);
94
95        index = a.getInt(com.android.internal.R.styleable.GridView_gravity, -1);
96        if (index >= 0) {
97            setGravity(index);
98        }
99
100        a.recycle();
101    }
102
103    @Override
104    public ListAdapter getAdapter() {
105        return mAdapter;
106    }
107
108    /**
109     * Sets the data behind this GridView.
110     *
111     * @param adapter the adapter providing the grid's data
112     */
113    @Override
114    public void setAdapter(ListAdapter adapter) {
115        if (null != mAdapter) {
116            mAdapter.unregisterDataSetObserver(mDataSetObserver);
117        }
118
119        resetList();
120        mRecycler.clear();
121        mAdapter = adapter;
122
123        mOldSelectedPosition = INVALID_POSITION;
124        mOldSelectedRowId = INVALID_ROW_ID;
125
126        if (mAdapter != null) {
127            mOldItemCount = mItemCount;
128            mItemCount = mAdapter.getCount();
129            mDataChanged = true;
130            checkFocus();
131
132            mDataSetObserver = new AdapterDataSetObserver();
133            mAdapter.registerDataSetObserver(mDataSetObserver);
134
135            mRecycler.setViewTypeCount(mAdapter.getViewTypeCount());
136
137            int position;
138            if (mStackFromBottom) {
139                position = lookForSelectablePosition(mItemCount - 1, false);
140            } else {
141                position = lookForSelectablePosition(0, true);
142            }
143            setSelectedPositionInt(position);
144            setNextSelectedPositionInt(position);
145            checkSelectionChanged();
146        } else {
147            checkFocus();
148            // Nothing selected
149            checkSelectionChanged();
150        }
151
152        requestLayout();
153    }
154
155    @Override
156    int lookForSelectablePosition(int position, boolean lookDown) {
157        final ListAdapter adapter = mAdapter;
158        if (adapter == null || isInTouchMode()) {
159            return INVALID_POSITION;
160        }
161
162        if (position < 0 || position >= mItemCount) {
163            return INVALID_POSITION;
164        }
165        return position;
166    }
167
168    /**
169     * {@inheritDoc}
170     */
171    @Override
172    void fillGap(boolean down) {
173        final int numColumns = mNumColumns;
174        final int verticalSpacing = mVerticalSpacing;
175
176        final int count = getChildCount();
177
178        if (down) {
179            final int startOffset = count > 0 ?
180                    getChildAt(count - 1).getBottom() + verticalSpacing : getListPaddingTop();
181            int position = mFirstPosition + count;
182            if (mStackFromBottom) {
183                position += numColumns - 1;
184            }
185            fillDown(position, startOffset);
186            correctTooHigh(numColumns, verticalSpacing, getChildCount());
187        } else {
188            final int startOffset = count > 0 ?
189                    getChildAt(0).getTop() - verticalSpacing : getHeight() - getListPaddingBottom();
190            int position = mFirstPosition;
191            if (!mStackFromBottom) {
192                position -= numColumns;
193            } else {
194                position--;
195            }
196            fillUp(position, startOffset);
197            correctTooLow(numColumns, verticalSpacing, getChildCount());
198        }
199    }
200
201    /**
202     * Fills the list from pos down to the end of the list view.
203     *
204     * @param pos The first position to put in the list
205     *
206     * @param nextTop The location where the top of the item associated with pos
207     *        should be drawn
208     *
209     * @return The view that is currently selected, if it happens to be in the
210     *         range that we draw.
211     */
212    private View fillDown(int pos, int nextTop) {
213        View selectedView = null;
214
215        final int end = (mBottom - mTop) - mListPadding.bottom;
216
217        while (nextTop < end && pos < mItemCount) {
218            View temp = makeRow(pos, nextTop, true);
219            if (temp != null) {
220                selectedView = temp;
221            }
222
223            nextTop = mReferenceView.getBottom() + mVerticalSpacing;
224
225            pos += mNumColumns;
226        }
227
228        return selectedView;
229    }
230
231    private View makeRow(int startPos, int y, boolean flow) {
232        final int columnWidth = mColumnWidth;
233        final int horizontalSpacing = mHorizontalSpacing;
234
235        int last;
236        int nextLeft = mListPadding.left + ((mStretchMode == STRETCH_SPACING_UNIFORM) ? horizontalSpacing : 0);
237
238        if (!mStackFromBottom) {
239            last = Math.min(startPos + mNumColumns, mItemCount);
240        } else {
241            last = startPos + 1;
242            startPos = Math.max(0, startPos - mNumColumns + 1);
243
244            if (last - startPos < mNumColumns) {
245                nextLeft += (mNumColumns - (last - startPos)) * (columnWidth + horizontalSpacing);
246            }
247        }
248
249        View selectedView = null;
250
251        final boolean hasFocus = shouldShowSelector();
252        final boolean inClick = touchModeDrawsInPressedState();
253        final int selectedPosition = mSelectedPosition;
254
255        mReferenceView = null;
256
257        for (int pos = startPos; pos < last; pos++) {
258            // is this the selected item?
259            boolean selected = pos == selectedPosition;
260            // does the list view have focus or contain focus
261
262            final int where = flow ? -1 : pos - startPos;
263            final View child = makeAndAddView(pos, y, flow, nextLeft, selected, where);
264            mReferenceView = child;
265
266            nextLeft += columnWidth;
267            if (pos < last - 1) {
268                nextLeft += horizontalSpacing;
269            }
270
271            if (selected && (hasFocus || inClick)) {
272                selectedView = child;
273            }
274        }
275
276        if (selectedView != null) {
277            mReferenceViewInSelectedRow = mReferenceView;
278        }
279
280        return selectedView;
281    }
282
283    /**
284     * Fills the list from pos up to the top of the list view.
285     *
286     * @param pos The first position to put in the list
287     *
288     * @param nextBottom The location where the bottom of the item associated
289     *        with pos should be drawn
290     *
291     * @return The view that is currently selected
292     */
293    private View fillUp(int pos, int nextBottom) {
294        View selectedView = null;
295
296        final int end = mListPadding.top;
297
298        while (nextBottom > end && pos >= 0) {
299
300            View temp = makeRow(pos, nextBottom, false);
301            if (temp != null) {
302                selectedView = temp;
303            }
304
305            nextBottom = mReferenceView.getTop() - mVerticalSpacing;
306
307            mFirstPosition = pos;
308
309            pos -= mNumColumns;
310        }
311
312        if (mStackFromBottom) {
313            mFirstPosition = Math.max(0, pos + 1);
314        }
315
316        return selectedView;
317    }
318
319    /**
320     * Fills the list from top to bottom, starting with mFirstPosition
321     *
322     * @param nextTop The location where the top of the first item should be
323     *        drawn
324     *
325     * @return The view that is currently selected
326     */
327    private View fillFromTop(int nextTop) {
328        mFirstPosition = Math.min(mFirstPosition, mSelectedPosition);
329        mFirstPosition = Math.min(mFirstPosition, mItemCount - 1);
330        if (mFirstPosition < 0) {
331            mFirstPosition = 0;
332        }
333        mFirstPosition -= mFirstPosition % mNumColumns;
334        return fillDown(mFirstPosition, nextTop);
335    }
336
337    private View fillFromBottom(int lastPosition, int nextBottom) {
338        lastPosition = Math.max(lastPosition, mSelectedPosition);
339        lastPosition = Math.min(lastPosition, mItemCount - 1);
340
341        final int invertedPosition = mItemCount - 1 - lastPosition;
342        lastPosition = mItemCount - 1 - (invertedPosition - (invertedPosition % mNumColumns));
343
344        return fillUp(lastPosition, nextBottom);
345    }
346
347    private View fillSelection(int childrenTop, int childrenBottom) {
348        final int selectedPosition = reconcileSelectedPosition();
349        final int numColumns = mNumColumns;
350        final int verticalSpacing = mVerticalSpacing;
351
352        int rowStart;
353        int rowEnd = -1;
354
355        if (!mStackFromBottom) {
356            rowStart = selectedPosition - (selectedPosition % numColumns);
357        } else {
358            final int invertedSelection = mItemCount - 1 - selectedPosition;
359
360            rowEnd = mItemCount - 1 - (invertedSelection - (invertedSelection % numColumns));
361            rowStart = Math.max(0, rowEnd - numColumns + 1);
362        }
363
364        final int fadingEdgeLength = getVerticalFadingEdgeLength();
365        final int topSelectionPixel = getTopSelectionPixel(childrenTop, fadingEdgeLength, rowStart);
366
367        final View sel = makeRow(mStackFromBottom ? rowEnd : rowStart, topSelectionPixel, true);
368        mFirstPosition = rowStart;
369
370        final View referenceView = mReferenceView;
371
372        if (!mStackFromBottom) {
373            fillDown(rowStart + numColumns, referenceView.getBottom() + verticalSpacing);
374            pinToBottom(childrenBottom);
375            fillUp(rowStart - numColumns, referenceView.getTop() - verticalSpacing);
376            adjustViewsUpOrDown();
377        } else {
378            final int bottomSelectionPixel = getBottomSelectionPixel(childrenBottom,
379                    fadingEdgeLength, numColumns, rowStart);
380            final int offset = bottomSelectionPixel - referenceView.getBottom();
381            offsetChildrenTopAndBottom(offset);
382            fillUp(rowStart - 1, referenceView.getTop() - verticalSpacing);
383            pinToTop(childrenTop);
384            fillDown(rowEnd + numColumns, referenceView.getBottom() + verticalSpacing);
385            adjustViewsUpOrDown();
386        }
387
388        return sel;
389    }
390
391    private void pinToTop(int childrenTop) {
392        if (mFirstPosition == 0) {
393            final int top = getChildAt(0).getTop();
394            final int offset = childrenTop - top;
395            if (offset < 0) {
396                offsetChildrenTopAndBottom(offset);
397            }
398        }
399    }
400
401    private void pinToBottom(int childrenBottom) {
402        final int count = getChildCount();
403        if (mFirstPosition + count == mItemCount) {
404            final int bottom = getChildAt(count - 1).getBottom();
405            final int offset = childrenBottom - bottom;
406            if (offset > 0) {
407                offsetChildrenTopAndBottom(offset);
408            }
409        }
410    }
411
412    @Override
413    int findMotionRow(int y) {
414        final int childCount = getChildCount();
415        if (childCount > 0) {
416
417            final int numColumns = mNumColumns;
418            if (!mStackFromBottom) {
419                for (int i = 0; i < childCount; i += numColumns) {
420                    if (y <= getChildAt(i).getBottom()) {
421                        return mFirstPosition + i;
422                    }
423                }
424            } else {
425                for (int i = childCount - 1; i >= 0; i -= numColumns) {
426                    if (y >= getChildAt(i).getTop()) {
427                        return mFirstPosition + i;
428                    }
429                }
430            }
431
432            return mFirstPosition + childCount - 1;
433        }
434        return INVALID_POSITION;
435    }
436
437    /**
438     * Layout during a scroll that results from tracking motion events. Places
439     * the mMotionPosition view at the offset specified by mMotionViewTop, and
440     * then build surrounding views from there.
441     *
442     * @param position the position at which to start filling
443     * @param top the top of the view at that position
444     * @return The selected view, or null if the selected view is outside the
445     *         visible area.
446     */
447    private View fillSpecific(int position, int top) {
448        final int numColumns = mNumColumns;
449
450        int motionRowStart;
451        int motionRowEnd = -1;
452
453        if (!mStackFromBottom) {
454            motionRowStart = position - (position % numColumns);
455        } else {
456            final int invertedSelection = mItemCount - 1 - position;
457
458            motionRowEnd = mItemCount - 1 - (invertedSelection - (invertedSelection % numColumns));
459            motionRowStart = Math.max(0, motionRowEnd - numColumns + 1);
460        }
461
462        final View temp = makeRow(mStackFromBottom ? motionRowEnd : motionRowStart, top, true);
463
464        // Possibly changed again in fillUp if we add rows above this one.
465        mFirstPosition = motionRowStart;
466
467        final View referenceView = mReferenceView;
468        final int verticalSpacing = mVerticalSpacing;
469
470        View above;
471        View below;
472
473        if (!mStackFromBottom) {
474            above = fillUp(motionRowStart - numColumns, referenceView.getTop() - verticalSpacing);
475            adjustViewsUpOrDown();
476            below = fillDown(motionRowStart + numColumns, referenceView.getBottom() + verticalSpacing);
477            // Check if we have dragged the bottom of the grid too high
478            final int childCount = getChildCount();
479            if (childCount > 0) {
480                correctTooHigh(numColumns, verticalSpacing, childCount);
481            }
482        } else {
483            below = fillDown(motionRowEnd + numColumns, referenceView.getBottom() + verticalSpacing);
484            adjustViewsUpOrDown();
485            above = fillUp(motionRowStart - 1, referenceView.getTop() - verticalSpacing);
486            // Check if we have dragged the bottom of the grid too high
487            final int childCount = getChildCount();
488            if (childCount > 0) {
489                correctTooLow(numColumns, verticalSpacing, childCount);
490            }
491        }
492
493        if (temp != null) {
494            return temp;
495        } else if (above != null) {
496            return above;
497        } else {
498            return below;
499        }
500    }
501
502    private void correctTooHigh(int numColumns, int verticalSpacing, int childCount) {
503        // First see if the last item is visible
504        final int lastPosition = mFirstPosition + childCount - 1;
505        if (lastPosition == mItemCount - 1 && childCount > 0) {
506            // Get the last child ...
507            final View lastChild = getChildAt(childCount - 1);
508
509            // ... and its bottom edge
510            final int lastBottom = lastChild.getBottom();
511            // This is bottom of our drawable area
512            final int end = (mBottom - mTop) - mListPadding.bottom;
513
514            // This is how far the bottom edge of the last view is from the bottom of the
515            // drawable area
516            int bottomOffset = end - lastBottom;
517
518            final View firstChild = getChildAt(0);
519            final int firstTop = firstChild.getTop();
520
521            // Make sure we are 1) Too high, and 2) Either there are more rows above the
522            // first row or the first row is scrolled off the top of the drawable area
523            if (bottomOffset > 0 && (mFirstPosition > 0 || firstTop < mListPadding.top))  {
524                if (mFirstPosition == 0) {
525                    // Don't pull the top too far down
526                    bottomOffset = Math.min(bottomOffset, mListPadding.top - firstTop);
527                }
528
529                // Move everything down
530                offsetChildrenTopAndBottom(bottomOffset);
531                if (mFirstPosition > 0) {
532                    // Fill the gap that was opened above mFirstPosition with more rows, if
533                    // possible
534                    fillUp(mFirstPosition - (mStackFromBottom ? 1 : numColumns),
535                            firstChild.getTop() - verticalSpacing);
536                    // Close up the remaining gap
537                    adjustViewsUpOrDown();
538                }
539            }
540        }
541    }
542
543    private void correctTooLow(int numColumns, int verticalSpacing, int childCount) {
544        if (mFirstPosition == 0 && childCount > 0) {
545            // Get the first child ...
546            final View firstChild = getChildAt(0);
547
548            // ... and its top edge
549            final int firstTop = firstChild.getTop();
550
551            // This is top of our drawable area
552            final int start = mListPadding.top;
553
554            // This is bottom of our drawable area
555            final int end = (mBottom - mTop) - mListPadding.bottom;
556
557            // This is how far the top edge of the first view is from the top of the
558            // drawable area
559            int topOffset = firstTop - start;
560            final View lastChild = getChildAt(childCount - 1);
561            final int lastBottom = lastChild.getBottom();
562            final int lastPosition = mFirstPosition + childCount - 1;
563
564            // Make sure we are 1) Too low, and 2) Either there are more rows below the
565            // last row or the last row is scrolled off the bottom of the drawable area
566            if (topOffset > 0 && (lastPosition < mItemCount - 1 || lastBottom > end))  {
567                if (lastPosition == mItemCount - 1 ) {
568                    // Don't pull the bottom too far up
569                    topOffset = Math.min(topOffset, lastBottom - end);
570                }
571
572                // Move everything up
573                offsetChildrenTopAndBottom(-topOffset);
574                if (lastPosition < mItemCount - 1) {
575                    // Fill the gap that was opened below the last position with more rows, if
576                    // possible
577                    fillDown(lastPosition + (!mStackFromBottom ? 1 : numColumns),
578                            lastChild.getBottom() + verticalSpacing);
579                    // Close up the remaining gap
580                    adjustViewsUpOrDown();
581                }
582            }
583        }
584    }
585
586    /**
587     * Fills the grid based on positioning the new selection at a specific
588     * location. The selection may be moved so that it does not intersect the
589     * faded edges. The grid is then filled upwards and downwards from there.
590     *
591     * @param selectedTop Where the selected item should be
592     * @param childrenTop Where to start drawing children
593     * @param childrenBottom Last pixel where children can be drawn
594     * @return The view that currently has selection
595     */
596    private View fillFromSelection(int selectedTop, int childrenTop, int childrenBottom) {
597        final int fadingEdgeLength = getVerticalFadingEdgeLength();
598        final int selectedPosition = mSelectedPosition;
599        final int numColumns = mNumColumns;
600        final int verticalSpacing = mVerticalSpacing;
601
602        int rowStart;
603        int rowEnd = -1;
604
605        if (!mStackFromBottom) {
606            rowStart = selectedPosition - (selectedPosition % numColumns);
607        } else {
608            int invertedSelection = mItemCount - 1 - selectedPosition;
609
610            rowEnd = mItemCount - 1 - (invertedSelection - (invertedSelection % numColumns));
611            rowStart = Math.max(0, rowEnd - numColumns + 1);
612        }
613
614        View sel;
615        View referenceView;
616
617        int topSelectionPixel = getTopSelectionPixel(childrenTop, fadingEdgeLength, rowStart);
618        int bottomSelectionPixel = getBottomSelectionPixel(childrenBottom, fadingEdgeLength,
619                numColumns, rowStart);
620
621        sel = makeRow(mStackFromBottom ? rowEnd : rowStart, selectedTop, true);
622        // Possibly changed again in fillUp if we add rows above this one.
623        mFirstPosition = rowStart;
624
625        referenceView = mReferenceView;
626        adjustForTopFadingEdge(referenceView, topSelectionPixel, bottomSelectionPixel);
627        adjustForBottomFadingEdge(referenceView, topSelectionPixel, bottomSelectionPixel);
628
629        if (!mStackFromBottom) {
630            fillUp(rowStart - numColumns, referenceView.getTop() - verticalSpacing);
631            adjustViewsUpOrDown();
632            fillDown(rowStart + numColumns, referenceView.getBottom() + verticalSpacing);
633        } else {
634            fillDown(rowEnd + numColumns, referenceView.getBottom() + verticalSpacing);
635            adjustViewsUpOrDown();
636            fillUp(rowStart - 1, referenceView.getTop() - verticalSpacing);
637        }
638
639
640        return sel;
641    }
642
643    /**
644     * Calculate the bottom-most pixel we can draw the selection into
645     *
646     * @param childrenBottom Bottom pixel were children can be drawn
647     * @param fadingEdgeLength Length of the fading edge in pixels, if present
648     * @param numColumns Number of columns in the grid
649     * @param rowStart The start of the row that will contain the selection
650     * @return The bottom-most pixel we can draw the selection into
651     */
652    private int getBottomSelectionPixel(int childrenBottom, int fadingEdgeLength,
653            int numColumns, int rowStart) {
654        // Last pixel we can draw the selection into
655        int bottomSelectionPixel = childrenBottom;
656        if (rowStart + numColumns - 1 < mItemCount - 1) {
657            bottomSelectionPixel -= fadingEdgeLength;
658        }
659        return bottomSelectionPixel;
660    }
661
662    /**
663     * Calculate the top-most pixel we can draw the selection into
664     *
665     * @param childrenTop Top pixel were children can be drawn
666     * @param fadingEdgeLength Length of the fading edge in pixels, if present
667     * @param rowStart The start of the row that will contain the selection
668     * @return The top-most pixel we can draw the selection into
669     */
670    private int getTopSelectionPixel(int childrenTop, int fadingEdgeLength, int rowStart) {
671        // first pixel we can draw the selection into
672        int topSelectionPixel = childrenTop;
673        if (rowStart > 0) {
674            topSelectionPixel += fadingEdgeLength;
675        }
676        return topSelectionPixel;
677    }
678
679    /**
680     * Move all views upwards so the selected row does not interesect the bottom
681     * fading edge (if necessary).
682     *
683     * @param childInSelectedRow A child in the row that contains the selection
684     * @param topSelectionPixel The topmost pixel we can draw the selection into
685     * @param bottomSelectionPixel The bottommost pixel we can draw the
686     *        selection into
687     */
688    private void adjustForBottomFadingEdge(View childInSelectedRow,
689            int topSelectionPixel, int bottomSelectionPixel) {
690        // Some of the newly selected item extends below the bottom of the
691        // list
692        if (childInSelectedRow.getBottom() > bottomSelectionPixel) {
693
694            // Find space available above the selection into which we can
695            // scroll upwards
696            int spaceAbove = childInSelectedRow.getTop() - topSelectionPixel;
697
698            // Find space required to bring the bottom of the selected item
699            // fully into view
700            int spaceBelow = childInSelectedRow.getBottom() - bottomSelectionPixel;
701            int offset = Math.min(spaceAbove, spaceBelow);
702
703            // Now offset the selected item to get it into view
704            offsetChildrenTopAndBottom(-offset);
705        }
706    }
707
708    /**
709     * Move all views upwards so the selected row does not interesect the top
710     * fading edge (if necessary).
711     *
712     * @param childInSelectedRow A child in the row that contains the selection
713     * @param topSelectionPixel The topmost pixel we can draw the selection into
714     * @param bottomSelectionPixel The bottommost pixel we can draw the
715     *        selection into
716     */
717    private void adjustForTopFadingEdge(View childInSelectedRow,
718            int topSelectionPixel, int bottomSelectionPixel) {
719        // Some of the newly selected item extends above the top of the list
720        if (childInSelectedRow.getTop() < topSelectionPixel) {
721            // Find space required to bring the top of the selected item
722            // fully into view
723            int spaceAbove = topSelectionPixel - childInSelectedRow.getTop();
724
725            // Find space available below the selection into which we can
726            // scroll downwards
727            int spaceBelow = bottomSelectionPixel - childInSelectedRow.getBottom();
728            int offset = Math.min(spaceAbove, spaceBelow);
729
730            // Now offset the selected item to get it into view
731            offsetChildrenTopAndBottom(offset);
732        }
733    }
734
735    /**
736     * Fills the grid based on positioning the new selection relative to the old
737     * selection. The new selection will be placed at, above, or below the
738     * location of the new selection depending on how the selection is moving.
739     * The selection will then be pinned to the visible part of the screen,
740     * excluding the edges that are faded. The grid is then filled upwards and
741     * downwards from there.
742     *
743     * @param delta Which way we are moving
744     * @param childrenTop Where to start drawing children
745     * @param childrenBottom Last pixel where children can be drawn
746     * @return The view that currently has selection
747     */
748    private View moveSelection(int delta, int childrenTop, int childrenBottom) {
749        final int fadingEdgeLength = getVerticalFadingEdgeLength();
750        final int selectedPosition = mSelectedPosition;
751        final int numColumns = mNumColumns;
752        final int verticalSpacing = mVerticalSpacing;
753
754        int oldRowStart;
755        int rowStart;
756        int rowEnd = -1;
757
758        if (!mStackFromBottom) {
759            oldRowStart = (selectedPosition - delta) - ((selectedPosition - delta) % numColumns);
760
761            rowStart = selectedPosition - (selectedPosition % numColumns);
762        } else {
763            int invertedSelection = mItemCount - 1 - selectedPosition;
764
765            rowEnd = mItemCount - 1 - (invertedSelection - (invertedSelection % numColumns));
766            rowStart = Math.max(0, rowEnd - numColumns + 1);
767
768            invertedSelection = mItemCount - 1 - (selectedPosition - delta);
769            oldRowStart = mItemCount - 1 - (invertedSelection - (invertedSelection % numColumns));
770            oldRowStart = Math.max(0, oldRowStart - numColumns + 1);
771        }
772
773        final int rowDelta = rowStart - oldRowStart;
774
775        final int topSelectionPixel = getTopSelectionPixel(childrenTop, fadingEdgeLength, rowStart);
776        final int bottomSelectionPixel = getBottomSelectionPixel(childrenBottom, fadingEdgeLength,
777                numColumns, rowStart);
778
779        // Possibly changed again in fillUp if we add rows above this one.
780        mFirstPosition = rowStart;
781
782        View sel;
783        View referenceView;
784
785        if (rowDelta > 0) {
786            /*
787             * Case 1: Scrolling down.
788             */
789
790            final int oldBottom = mReferenceViewInSelectedRow == null ? 0 :
791                    mReferenceViewInSelectedRow.getBottom();
792
793            sel = makeRow(mStackFromBottom ? rowEnd : rowStart, oldBottom + verticalSpacing, true);
794            referenceView = mReferenceView;
795
796            adjustForBottomFadingEdge(referenceView, topSelectionPixel, bottomSelectionPixel);
797        } else if (rowDelta < 0) {
798            /*
799             * Case 2: Scrolling up.
800             */
801            final int oldTop = mReferenceViewInSelectedRow == null ?
802                    0 : mReferenceViewInSelectedRow .getTop();
803
804            sel = makeRow(mStackFromBottom ? rowEnd : rowStart, oldTop - verticalSpacing, false);
805            referenceView = mReferenceView;
806
807            adjustForTopFadingEdge(referenceView, topSelectionPixel, bottomSelectionPixel);
808        } else {
809            /*
810             * Keep selection where it was
811             */
812            final int oldTop = mReferenceViewInSelectedRow == null ?
813                    0 : mReferenceViewInSelectedRow .getTop();
814
815            sel = makeRow(mStackFromBottom ? rowEnd : rowStart, oldTop, true);
816            referenceView = mReferenceView;
817        }
818
819        if (!mStackFromBottom) {
820            fillUp(rowStart - numColumns, referenceView.getTop() - verticalSpacing);
821            adjustViewsUpOrDown();
822            fillDown(rowStart + numColumns, referenceView.getBottom() + verticalSpacing);
823        } else {
824            fillDown(rowEnd + numColumns, referenceView.getBottom() + verticalSpacing);
825            adjustViewsUpOrDown();
826            fillUp(rowStart - 1, referenceView.getTop() - verticalSpacing);
827        }
828
829        return sel;
830    }
831
832    private void determineColumns(int availableSpace) {
833        final int requestedHorizontalSpacing = mRequestedHorizontalSpacing;
834        final int stretchMode = mStretchMode;
835        final int requestedColumnWidth = mRequestedColumnWidth;
836
837        if (mRequestedNumColumns == AUTO_FIT) {
838            if (requestedColumnWidth > 0) {
839                // Client told us to pick the number of columns
840                mNumColumns = (availableSpace + requestedHorizontalSpacing) /
841                        (requestedColumnWidth + requestedHorizontalSpacing);
842            } else {
843                // Just make up a number if we don't have enough info
844                mNumColumns = 2;
845            }
846        } else {
847            // We picked the columns
848            mNumColumns = mRequestedNumColumns;
849        }
850
851        if (mNumColumns <= 0) {
852            mNumColumns = 1;
853        }
854
855        switch (stretchMode) {
856        case NO_STRETCH:
857            // Nobody stretches
858            mColumnWidth = requestedColumnWidth;
859            mHorizontalSpacing = requestedHorizontalSpacing;
860            break;
861
862        default:
863            int spaceLeftOver = availableSpace - (mNumColumns * requestedColumnWidth) -
864                    ((mNumColumns - 1) * requestedHorizontalSpacing);
865            switch (stretchMode) {
866            case STRETCH_COLUMN_WIDTH:
867                // Stretch the columns
868                mColumnWidth = requestedColumnWidth + spaceLeftOver / mNumColumns;
869                mHorizontalSpacing = requestedHorizontalSpacing;
870                break;
871
872            case STRETCH_SPACING:
873                // Stretch the spacing between columns
874                mColumnWidth = requestedColumnWidth;
875                if (mNumColumns > 1) {
876                    mHorizontalSpacing = requestedHorizontalSpacing +
877                        spaceLeftOver / (mNumColumns - 1);
878                } else {
879                    mHorizontalSpacing = requestedHorizontalSpacing + spaceLeftOver;
880                }
881                break;
882
883            case STRETCH_SPACING_UNIFORM:
884                // Stretch the spacing between columns
885                mColumnWidth = requestedColumnWidth;
886                if (mNumColumns > 1) {
887                    mHorizontalSpacing = requestedHorizontalSpacing +
888                        spaceLeftOver / (mNumColumns + 1);
889                } else {
890                    mHorizontalSpacing = requestedHorizontalSpacing + spaceLeftOver;
891                }
892                break;
893            }
894
895            break;
896        }
897    }
898
899    @Override
900    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
901        // Sets up mListPadding
902        super.onMeasure(widthMeasureSpec, heightMeasureSpec);
903
904        int widthMode = MeasureSpec.getMode(widthMeasureSpec);
905        int heightMode = MeasureSpec.getMode(heightMeasureSpec);
906        int widthSize = MeasureSpec.getSize(widthMeasureSpec);
907        int heightSize = MeasureSpec.getSize(heightMeasureSpec);
908
909        if (widthMode == MeasureSpec.UNSPECIFIED) {
910            if (mColumnWidth > 0) {
911                widthSize = mColumnWidth + mListPadding.left + mListPadding.right;
912            } else {
913                widthSize = mListPadding.left + mListPadding.right;
914            }
915            widthSize += getVerticalScrollbarWidth();
916        }
917
918        int childWidth = widthSize - mListPadding.left - mListPadding.right;
919        determineColumns(childWidth);
920
921        int childHeight = 0;
922
923        mItemCount = mAdapter == null ? 0 : mAdapter.getCount();
924        final int count = mItemCount;
925        if (count > 0) {
926            final View child = obtainView(0);
927
928            AbsListView.LayoutParams p = (AbsListView.LayoutParams)child.getLayoutParams();
929            if (p == null) {
930                p = new AbsListView.LayoutParams(ViewGroup.LayoutParams.FILL_PARENT,
931                        ViewGroup.LayoutParams.WRAP_CONTENT, 0);
932            }
933            p.viewType = mAdapter.getItemViewType(0);
934
935            int childHeightSpec = getChildMeasureSpec(
936                    MeasureSpec.makeMeasureSpec(0, MeasureSpec.UNSPECIFIED), 0, p.height);
937            int childWidthSpec = getChildMeasureSpec(
938                    MeasureSpec.makeMeasureSpec(mColumnWidth, MeasureSpec.EXACTLY), 0, p.width);
939            child.measure(childWidthSpec, childHeightSpec);
940
941            childHeight = child.getMeasuredHeight();
942
943            if (mRecycler.shouldRecycleViewType(p.viewType)) {
944                mRecycler.addScrapView(child);
945            }
946        }
947
948        if (heightMode == MeasureSpec.UNSPECIFIED) {
949            heightSize = mListPadding.top + mListPadding.bottom + childHeight +
950                    getVerticalFadingEdgeLength() * 2;
951        }
952
953        if (heightMode == MeasureSpec.AT_MOST) {
954            int ourSize =  mListPadding.top + mListPadding.bottom;
955
956            final int numColumns = mNumColumns;
957            for (int i = 0; i < count; i += numColumns) {
958                ourSize += childHeight;
959                if (i + numColumns < count) {
960                    ourSize += mVerticalSpacing;
961                }
962                if (ourSize >= heightSize) {
963                    ourSize = heightSize;
964                    break;
965                }
966            }
967            heightSize = ourSize;
968        }
969
970        setMeasuredDimension(widthSize, heightSize);
971        mWidthMeasureSpec = widthMeasureSpec;
972    }
973
974    @Override
975    protected void attachLayoutAnimationParameters(View child,
976            ViewGroup.LayoutParams params, int index, int count) {
977
978        GridLayoutAnimationController.AnimationParameters animationParams =
979                (GridLayoutAnimationController.AnimationParameters) params.layoutAnimationParameters;
980
981        if (animationParams == null) {
982            animationParams = new GridLayoutAnimationController.AnimationParameters();
983            params.layoutAnimationParameters = animationParams;
984        }
985
986        animationParams.count = count;
987        animationParams.index = index;
988        animationParams.columnsCount = mNumColumns;
989        animationParams.rowsCount = count / mNumColumns;
990
991        if (!mStackFromBottom) {
992            animationParams.column = index % mNumColumns;
993            animationParams.row = index / mNumColumns;
994        } else {
995            final int invertedIndex = count - 1 - index;
996
997            animationParams.column = mNumColumns - 1 - (invertedIndex % mNumColumns);
998            animationParams.row = animationParams.rowsCount - 1 - invertedIndex / mNumColumns;
999        }
1000    }
1001
1002    @Override
1003    protected void layoutChildren() {
1004        final boolean blockLayoutRequests = mBlockLayoutRequests;
1005        if (!blockLayoutRequests) {
1006            mBlockLayoutRequests = true;
1007        }
1008
1009        try {
1010            super.layoutChildren();
1011
1012            invalidate();
1013
1014            if (mAdapter == null) {
1015                resetList();
1016                invokeOnItemScrollListener();
1017                return;
1018            }
1019
1020            final int childrenTop = mListPadding.top;
1021            final int childrenBottom = mBottom - mTop - mListPadding.bottom;
1022
1023            int childCount = getChildCount();
1024            int index;
1025            int delta = 0;
1026
1027            View sel;
1028            View oldSel = null;
1029            View oldFirst = null;
1030            View newSel = null;
1031
1032            // Remember stuff we will need down below
1033            switch (mLayoutMode) {
1034            case LAYOUT_SET_SELECTION:
1035                index = mNextSelectedPosition - mFirstPosition;
1036                if (index >= 0 && index < childCount) {
1037                    newSel = getChildAt(index);
1038                }
1039                break;
1040            case LAYOUT_FORCE_TOP:
1041            case LAYOUT_FORCE_BOTTOM:
1042            case LAYOUT_SPECIFIC:
1043            case LAYOUT_SYNC:
1044                break;
1045            case LAYOUT_MOVE_SELECTION:
1046                if (mNextSelectedPosition >= 0) {
1047                    delta = mNextSelectedPosition - mSelectedPosition;
1048                }
1049                break;
1050            default:
1051                // Remember the previously selected view
1052                index = mSelectedPosition - mFirstPosition;
1053                if (index >= 0 && index < childCount) {
1054                    oldSel = getChildAt(index);
1055                }
1056
1057                // Remember the previous first child
1058                oldFirst = getChildAt(0);
1059            }
1060
1061            boolean dataChanged = mDataChanged;
1062            if (dataChanged) {
1063                handleDataChanged();
1064            }
1065
1066            // Handle the empty set by removing all views that are visible
1067            // and calling it a day
1068            if (mItemCount == 0) {
1069                resetList();
1070                invokeOnItemScrollListener();
1071                return;
1072            }
1073
1074            setSelectedPositionInt(mNextSelectedPosition);
1075
1076            // Pull all children into the RecycleBin.
1077            // These views will be reused if possible
1078            final int firstPosition = mFirstPosition;
1079            final RecycleBin recycleBin = mRecycler;
1080
1081            if (dataChanged) {
1082                for (int i = 0; i < childCount; i++) {
1083                    recycleBin.addScrapView(getChildAt(i));
1084                }
1085            } else {
1086                recycleBin.fillActiveViews(childCount, firstPosition);
1087            }
1088
1089            // Clear out old views
1090            //removeAllViewsInLayout();
1091            detachAllViewsFromParent();
1092
1093            switch (mLayoutMode) {
1094            case LAYOUT_SET_SELECTION:
1095                if (newSel != null) {
1096                    sel = fillFromSelection(newSel.getTop(), childrenTop, childrenBottom);
1097                } else {
1098                    sel = fillSelection(childrenTop, childrenBottom);
1099                }
1100                break;
1101            case LAYOUT_FORCE_TOP:
1102                mFirstPosition = 0;
1103                sel = fillFromTop(childrenTop);
1104                adjustViewsUpOrDown();
1105                break;
1106            case LAYOUT_FORCE_BOTTOM:
1107                sel = fillUp(mItemCount - 1, childrenBottom);
1108                adjustViewsUpOrDown();
1109                break;
1110            case LAYOUT_SPECIFIC:
1111                sel = fillSpecific(mSelectedPosition, mSpecificTop);
1112                break;
1113            case LAYOUT_SYNC:
1114                sel = fillSpecific(mSyncPosition, mSpecificTop);
1115                break;
1116            case LAYOUT_MOVE_SELECTION:
1117                // Move the selection relative to its old position
1118                sel = moveSelection(delta, childrenTop, childrenBottom);
1119                break;
1120            default:
1121                if (childCount == 0) {
1122                    if (!mStackFromBottom) {
1123                        setSelectedPositionInt(0);
1124                        sel = fillFromTop(childrenTop);
1125                    } else {
1126                        final int last = mItemCount - 1;
1127                        setSelectedPositionInt(last);
1128                        sel = fillFromBottom(last, childrenBottom);
1129                    }
1130                } else {
1131                    if (mSelectedPosition >= 0 && mSelectedPosition < mItemCount) {
1132                        sel = fillSpecific(mSelectedPosition, oldSel == null ?
1133                                childrenTop : oldSel.getTop());
1134                    } else if (mFirstPosition < mItemCount)  {
1135                        sel = fillSpecific(mFirstPosition, oldFirst == null ?
1136                                childrenTop : oldFirst.getTop());
1137                    } else {
1138                        sel = fillSpecific(0, childrenTop);
1139                    }
1140                }
1141                break;
1142            }
1143
1144            // Flush any cached views that did not get reused above
1145            recycleBin.scrapActiveViews();
1146
1147            if (sel != null) {
1148               positionSelector(sel);
1149               mSelectedTop = sel.getTop();
1150            } else {
1151               mSelectedTop = 0;
1152               mSelectorRect.setEmpty();
1153            }
1154
1155            mLayoutMode = LAYOUT_NORMAL;
1156            mDataChanged = false;
1157            mNeedSync = false;
1158            setNextSelectedPositionInt(mSelectedPosition);
1159
1160            updateScrollIndicators();
1161
1162            if (mItemCount > 0) {
1163                checkSelectionChanged();
1164            }
1165
1166            invokeOnItemScrollListener();
1167        } finally {
1168            if (!blockLayoutRequests) {
1169                mBlockLayoutRequests = false;
1170            }
1171        }
1172    }
1173
1174
1175    /**
1176     * Obtain the view and add it to our list of children. The view can be made
1177     * fresh, converted from an unused view, or used as is if it was in the
1178     * recycle bin.
1179     *
1180     * @param position Logical position in the list
1181     * @param y Top or bottom edge of the view to add
1182     * @param flow if true, align top edge to y. If false, align bottom edge to
1183     *        y.
1184     * @param childrenLeft Left edge where children should be positioned
1185     * @param selected Is this position selected?
1186     * @param where to add new item in the list
1187     * @return View that was added
1188     */
1189    private View makeAndAddView(int position, int y, boolean flow, int childrenLeft,
1190            boolean selected, int where) {
1191        View child;
1192
1193        if (!mDataChanged) {
1194            // Try to use an exsiting view for this position
1195            child = mRecycler.getActiveView(position);
1196            if (child != null) {
1197                // Found it -- we're using an existing child
1198                // This just needs to be positioned
1199                setupChild(child, position, y, flow, childrenLeft, selected, true, where);
1200                return child;
1201            }
1202        }
1203
1204        // Make a new view for this position, or convert an unused view if
1205        // possible
1206        child = obtainView(position);
1207
1208        // This needs to be positioned and measured
1209        setupChild(child, position, y, flow, childrenLeft, selected, false, where);
1210
1211        return child;
1212    }
1213
1214    /**
1215     * Add a view as a child and make sure it is measured (if necessary) and
1216     * positioned properly.
1217     *
1218     * @param child The view to add
1219     * @param position The position of the view
1220     * @param y The y position relative to which this view will be positioned
1221     * @param flow if true, align top edge to y. If false, align bottom edge
1222     *        to y.
1223     * @param childrenLeft Left edge where children should be positioned
1224     * @param selected Is this position selected?
1225     * @param recycled Has this view been pulled from the recycle bin? If so it
1226     *        does not need to be remeasured.
1227     * @param where Where to add the item in the list
1228     *
1229     */
1230    private void setupChild(View child, int position, int y, boolean flow, int childrenLeft,
1231            boolean selected, boolean recycled, int where) {
1232        boolean isSelected = selected && shouldShowSelector();
1233
1234        final boolean updateChildSelected = isSelected != child.isSelected();
1235        boolean needToMeasure = !recycled || updateChildSelected || child.isLayoutRequested();
1236
1237        // Respect layout params that are already in the view. Otherwise make
1238        // some up...
1239        AbsListView.LayoutParams p = (AbsListView.LayoutParams)child.getLayoutParams();
1240        if (p == null) {
1241            p = new AbsListView.LayoutParams(ViewGroup.LayoutParams.FILL_PARENT,
1242                    ViewGroup.LayoutParams.WRAP_CONTENT, 0);
1243        }
1244        p.viewType = mAdapter.getItemViewType(position);
1245
1246        if (recycled) {
1247            attachViewToParent(child, where, p);
1248        } else {
1249            addViewInLayout(child, where, p, true);
1250        }
1251
1252        if (updateChildSelected) {
1253            child.setSelected(isSelected);
1254            if (isSelected) {
1255                requestFocus();
1256            }
1257        }
1258
1259        if (needToMeasure) {
1260            int childHeightSpec = ViewGroup.getChildMeasureSpec(
1261                    MeasureSpec.makeMeasureSpec(0, MeasureSpec.UNSPECIFIED), 0, p.height);
1262
1263            int childWidthSpec = ViewGroup.getChildMeasureSpec(
1264                    MeasureSpec.makeMeasureSpec(mColumnWidth, MeasureSpec.EXACTLY), 0, p.width);
1265            child.measure(childWidthSpec, childHeightSpec);
1266        } else {
1267            cleanupLayoutState(child);
1268        }
1269
1270        final int w = child.getMeasuredWidth();
1271        final int h = child.getMeasuredHeight();
1272
1273        int childLeft;
1274        final int childTop = flow ? y : y - h;
1275
1276        switch (mGravity & Gravity.HORIZONTAL_GRAVITY_MASK) {
1277        case Gravity.LEFT:
1278            childLeft = childrenLeft;
1279            break;
1280        case Gravity.CENTER_HORIZONTAL:
1281            childLeft = childrenLeft + ((mColumnWidth - w) / 2);
1282            break;
1283        case Gravity.RIGHT:
1284            childLeft = childrenLeft + mColumnWidth - w;
1285            break;
1286        default:
1287            childLeft = childrenLeft;
1288            break;
1289        }
1290
1291        if (needToMeasure) {
1292            final int childRight = childLeft + w;
1293            final int childBottom = childTop + h;
1294            child.layout(childLeft, childTop, childRight, childBottom);
1295        } else {
1296            child.offsetLeftAndRight(childLeft - child.getLeft());
1297            child.offsetTopAndBottom(childTop - child.getTop());
1298        }
1299
1300        if (mCachingStarted) {
1301            child.setDrawingCacheEnabled(true);
1302        }
1303    }
1304
1305    /**
1306     * Sets the currently selected item
1307     *
1308     * @param position Index (starting at 0) of the data item to be selected.
1309     *
1310     * If in touch mode, the item will not be selected but it will still be positioned
1311     * appropriately.
1312     */
1313    @Override
1314    public void setSelection(int position) {
1315        if (!isInTouchMode()) {
1316            setNextSelectedPositionInt(position);
1317        } else {
1318            mResurrectToPosition = position;
1319        }
1320        mLayoutMode = LAYOUT_SET_SELECTION;
1321        requestLayout();
1322    }
1323
1324    /**
1325     * Makes the item at the supplied position selected.
1326     *
1327     * @param position the position of the new selection
1328     */
1329    @Override
1330    void setSelectionInt(int position) {
1331        mBlockLayoutRequests = true;
1332        setNextSelectedPositionInt(position);
1333        layoutChildren();
1334
1335        mBlockLayoutRequests = false;
1336    }
1337
1338    @Override
1339    public boolean onKeyDown(int keyCode, KeyEvent event) {
1340        return commonKey(keyCode, 1, event);
1341    }
1342
1343    @Override
1344    public boolean onKeyMultiple(int keyCode, int repeatCount, KeyEvent event) {
1345        return commonKey(keyCode, repeatCount, event);
1346    }
1347
1348    @Override
1349    public boolean onKeyUp(int keyCode, KeyEvent event) {
1350        return commonKey(keyCode, 1, event);
1351    }
1352
1353    private boolean commonKey(int keyCode, int count, KeyEvent event) {
1354        if (mAdapter == null) {
1355            return false;
1356        }
1357
1358        if (mDataChanged) {
1359            layoutChildren();
1360        }
1361
1362        boolean handled = false;
1363        int action = event.getAction();
1364
1365        if (action != KeyEvent.ACTION_UP) {
1366            if (mSelectedPosition < 0) {
1367                switch (keyCode) {
1368                    case KeyEvent.KEYCODE_DPAD_UP:
1369                    case KeyEvent.KEYCODE_DPAD_DOWN:
1370                    case KeyEvent.KEYCODE_DPAD_LEFT:
1371                    case KeyEvent.KEYCODE_DPAD_RIGHT:
1372                    case KeyEvent.KEYCODE_DPAD_CENTER:
1373                    case KeyEvent.KEYCODE_SPACE:
1374                    case KeyEvent.KEYCODE_ENTER:
1375                        resurrectSelection();
1376                        return true;
1377                }
1378            }
1379
1380            switch (keyCode) {
1381                case KeyEvent.KEYCODE_DPAD_LEFT:
1382                    handled = arrowScroll(FOCUS_LEFT);
1383                    break;
1384
1385                case KeyEvent.KEYCODE_DPAD_RIGHT:
1386                    handled = arrowScroll(FOCUS_RIGHT);
1387                    break;
1388
1389                case KeyEvent.KEYCODE_DPAD_UP:
1390                    if (!event.isAltPressed()) {
1391                        handled = arrowScroll(FOCUS_UP);
1392
1393                    } else {
1394                        handled = fullScroll(FOCUS_UP);
1395                    }
1396                    break;
1397
1398                case KeyEvent.KEYCODE_DPAD_DOWN:
1399                    if (!event.isAltPressed()) {
1400                        handled = arrowScroll(FOCUS_DOWN);
1401                    } else {
1402                        handled = fullScroll(FOCUS_DOWN);
1403                    }
1404                    break;
1405
1406                case KeyEvent.KEYCODE_DPAD_CENTER:
1407                case KeyEvent.KEYCODE_ENTER: {
1408                    if (getChildCount() > 0 && event.getRepeatCount() == 0) {
1409                        keyPressed();
1410                    }
1411
1412                    return true;
1413                }
1414
1415                case KeyEvent.KEYCODE_SPACE:
1416                    if (mPopup == null || !mPopup.isShowing()) {
1417                        if (!event.isShiftPressed()) {
1418                            handled = pageScroll(FOCUS_DOWN);
1419                        } else {
1420                            handled = pageScroll(FOCUS_UP);
1421                        }
1422                    }
1423                    break;
1424            }
1425        }
1426
1427        if (!handled) {
1428            handled = sendToTextFilter(keyCode, count, event);
1429        }
1430
1431        if (handled) {
1432            return true;
1433        } else {
1434            switch (action) {
1435                case KeyEvent.ACTION_DOWN:
1436                    return super.onKeyDown(keyCode, event);
1437                case KeyEvent.ACTION_UP:
1438                    return super.onKeyUp(keyCode, event);
1439                case KeyEvent.ACTION_MULTIPLE:
1440                    return super.onKeyMultiple(keyCode, count, event);
1441                default:
1442                    return false;
1443            }
1444        }
1445    }
1446
1447    /**
1448     * Scrolls up or down by the number of items currently present on screen.
1449     *
1450     * @param direction either {@link View#FOCUS_UP} or {@link View#FOCUS_DOWN}
1451     * @return whether selection was moved
1452     */
1453    boolean pageScroll(int direction) {
1454        int nextPage = -1;
1455
1456        if (direction == FOCUS_UP) {
1457            nextPage = Math.max(0, mSelectedPosition - getChildCount() - 1);
1458        } else if (direction == FOCUS_DOWN) {
1459            nextPage = Math.min(mItemCount - 1, mSelectedPosition + getChildCount() - 1);
1460        }
1461
1462        if (nextPage >= 0) {
1463            setSelectionInt(nextPage);
1464            invokeOnItemScrollListener();
1465            return true;
1466        }
1467
1468        return false;
1469    }
1470
1471    /**
1472     * Go to the last or first item if possible.
1473     *
1474     * @param direction either {@link View#FOCUS_UP} or {@link View#FOCUS_DOWN}.
1475     *
1476     * @return Whether selection was moved.
1477     */
1478    boolean fullScroll(int direction) {
1479        boolean moved = false;
1480        if (direction == FOCUS_UP) {
1481            mLayoutMode = LAYOUT_SET_SELECTION;
1482            setSelectionInt(0);
1483            invokeOnItemScrollListener();
1484            moved = true;
1485        } else if (direction == FOCUS_DOWN) {
1486            mLayoutMode = LAYOUT_SET_SELECTION;
1487            setSelectionInt(mItemCount - 1);
1488            invokeOnItemScrollListener();
1489            moved = true;
1490        }
1491
1492        return moved;
1493    }
1494
1495    /**
1496     * Scrolls to the next or previous item, horizontally or vertically.
1497     *
1498     * @param direction either {@link View#FOCUS_LEFT}, {@link View#FOCUS_RIGHT},
1499     *        {@link View#FOCUS_UP} or {@link View#FOCUS_DOWN}
1500     *
1501     * @return whether selection was moved
1502     */
1503    boolean arrowScroll(int direction) {
1504        final int selectedPosition = mSelectedPosition;
1505        final int numColumns = mNumColumns;
1506
1507        int startOfRowPos;
1508        int endOfRowPos;
1509
1510        boolean moved = false;
1511
1512        if (!mStackFromBottom) {
1513            startOfRowPos = (selectedPosition / numColumns) * numColumns;
1514            endOfRowPos = Math.min(startOfRowPos + numColumns - 1, mItemCount - 1);
1515        } else {
1516            final int invertedSelection = mItemCount - 1 - selectedPosition;
1517            endOfRowPos = mItemCount - 1 - (invertedSelection / numColumns) * numColumns;
1518            startOfRowPos = Math.max(0, endOfRowPos - numColumns + 1);
1519        }
1520
1521        switch (direction) {
1522            case FOCUS_UP:
1523                if (startOfRowPos > 0) {
1524                    mLayoutMode = LAYOUT_MOVE_SELECTION;
1525                    setSelectionInt(Math.max(0, selectedPosition - numColumns));
1526                    moved = true;
1527                }
1528                break;
1529            case FOCUS_DOWN:
1530                if (endOfRowPos < mItemCount - 1) {
1531                    mLayoutMode = LAYOUT_MOVE_SELECTION;
1532                    setSelectionInt(Math.min(selectedPosition + numColumns, mItemCount - 1));
1533                    moved = true;
1534                }
1535                break;
1536            case FOCUS_LEFT:
1537                if (selectedPosition > startOfRowPos) {
1538                    mLayoutMode = LAYOUT_MOVE_SELECTION;
1539                    setSelectionInt(selectedPosition - 1);
1540                    moved = true;
1541                }
1542                break;
1543            case FOCUS_RIGHT:
1544                if (selectedPosition < endOfRowPos) {
1545                    mLayoutMode = LAYOUT_MOVE_SELECTION;
1546                    setSelectionInt(selectedPosition + 1);
1547                    moved = true;
1548                }
1549                break;
1550        }
1551
1552        if (moved) {
1553            playSoundEffect(SoundEffectConstants.getContantForFocusDirection(direction));
1554            invokeOnItemScrollListener();
1555        }
1556
1557        return moved;
1558    }
1559
1560    @Override
1561    protected void onFocusChanged(boolean gainFocus, int direction, Rect previouslyFocusedRect) {
1562        super.onFocusChanged(gainFocus, direction, previouslyFocusedRect);
1563
1564        int closestChildIndex = -1;
1565        if (gainFocus && previouslyFocusedRect != null) {
1566            previouslyFocusedRect.offset(mScrollX, mScrollY);
1567
1568            // figure out which item should be selected based on previously
1569            // focused rect
1570            Rect otherRect = mTempRect;
1571            int minDistance = Integer.MAX_VALUE;
1572            final int childCount = getChildCount();
1573            for (int i = 0; i < childCount; i++) {
1574                // only consider view's on appropriate edge of grid
1575                if (!isCandidateSelection(i, direction)) {
1576                    continue;
1577                }
1578
1579                final View other = getChildAt(i);
1580                other.getDrawingRect(otherRect);
1581                offsetDescendantRectToMyCoords(other, otherRect);
1582                int distance = getDistance(previouslyFocusedRect, otherRect, direction);
1583
1584                if (distance < minDistance) {
1585                    minDistance = distance;
1586                    closestChildIndex = i;
1587                }
1588            }
1589        }
1590
1591        if (closestChildIndex >= 0) {
1592            setSelection(closestChildIndex + mFirstPosition);
1593        } else {
1594            requestLayout();
1595        }
1596    }
1597
1598    /**
1599     * Is childIndex a candidate for next focus given the direction the focus
1600     * change is coming from?
1601     * @param childIndex The index to check.
1602     * @param direction The direction, one of
1603     *        {FOCUS_UP, FOCUS_DOWN, FOCUS_LEFT, FOCUS_RIGHT}
1604     * @return Whether childIndex is a candidate.
1605     */
1606    private boolean isCandidateSelection(int childIndex, int direction) {
1607        final int count = getChildCount();
1608        final int invertedIndex = count - 1 - childIndex;
1609
1610        int rowStart;
1611        int rowEnd;
1612
1613        if (!mStackFromBottom) {
1614            rowStart = childIndex - (childIndex % mNumColumns);
1615            rowEnd = Math.max(rowStart + mNumColumns - 1, count);
1616        } else {
1617            rowEnd = count - 1 - (invertedIndex - (invertedIndex % mNumColumns));
1618            rowStart = Math.max(0, rowEnd - mNumColumns + 1);
1619        }
1620
1621        switch (direction) {
1622            case View.FOCUS_RIGHT:
1623                // coming from left, selection is only valid if it is on left
1624                // edge
1625                return childIndex == rowStart;
1626            case View.FOCUS_DOWN:
1627                // coming from top; only valid if in top row
1628                return rowStart == 0;
1629            case View.FOCUS_LEFT:
1630                // coming from right, must be on right edge
1631                return childIndex == rowEnd;
1632            case View.FOCUS_UP:
1633                // coming from bottom, need to be in last row
1634                return rowEnd == count - 1;
1635            default:
1636                throw new IllegalArgumentException("direction must be one of "
1637                        + "{FOCUS_UP, FOCUS_DOWN, FOCUS_LEFT, FOCUS_RIGHT}.");
1638        }
1639    }
1640
1641    /**
1642     * Describes how the child views are horizontally aligned. Defaults to Gravity.LEFT
1643     *
1644     * @param gravity the gravity to apply to this grid's children
1645     *
1646     * @attr ref android.R.styleable#GridView_gravity
1647     */
1648    public void setGravity(int gravity) {
1649        if (mGravity != gravity) {
1650            mGravity = gravity;
1651            requestLayoutIfNecessary();
1652        }
1653    }
1654
1655    /**
1656     * Set the amount of horizontal (x) spacing to place between each item
1657     * in the grid.
1658     *
1659     * @param horizontalSpacing The amount of horizontal space between items,
1660     * in pixels.
1661     *
1662     * @attr ref android.R.styleable#GridView_horizontalSpacing
1663     */
1664    public void setHorizontalSpacing(int horizontalSpacing) {
1665        if (horizontalSpacing != mRequestedHorizontalSpacing) {
1666            mRequestedHorizontalSpacing = horizontalSpacing;
1667            requestLayoutIfNecessary();
1668        }
1669    }
1670
1671
1672    /**
1673     * Set the amount of vertical (y) spacing to place between each item
1674     * in the grid.
1675     *
1676     * @param verticalSpacing The amount of vertical space between items,
1677     * in pixels.
1678     *
1679     * @attr ref android.R.styleable#GridView_verticalSpacing
1680     */
1681    public void setVerticalSpacing(int verticalSpacing) {
1682        if (verticalSpacing != mVerticalSpacing) {
1683            mVerticalSpacing = verticalSpacing;
1684            requestLayoutIfNecessary();
1685        }
1686    }
1687
1688    /**
1689     * Control how items are stretched to fill their space.
1690     *
1691     * @param stretchMode Either {@link #NO_STRETCH},
1692     * {@link #STRETCH_SPACING}, {@link #STRETCH_SPACING_UNIFORM}, or {@link #STRETCH_COLUMN_WIDTH}.
1693     *
1694     * @attr ref android.R.styleable#GridView_stretchMode
1695     */
1696    public void setStretchMode(int stretchMode) {
1697        if (stretchMode != mStretchMode) {
1698            mStretchMode = stretchMode;
1699            requestLayoutIfNecessary();
1700        }
1701    }
1702
1703    public int getStretchMode() {
1704        return mStretchMode;
1705    }
1706
1707    /**
1708     * Set the width of columns in the grid.
1709     *
1710     * @param columnWidth The column width, in pixels.
1711     *
1712     * @attr ref android.R.styleable#GridView_columnWidth
1713     */
1714    public void setColumnWidth(int columnWidth) {
1715        if (columnWidth != mRequestedColumnWidth) {
1716            mRequestedColumnWidth = columnWidth;
1717            requestLayoutIfNecessary();
1718        }
1719    }
1720
1721    /**
1722     * Set the number of columns in the grid
1723     *
1724     * @param numColumns The desired number of columns.
1725     *
1726     * @attr ref android.R.styleable#GridView_numColumns
1727     */
1728    public void setNumColumns(int numColumns) {
1729        if (numColumns != mRequestedNumColumns) {
1730            mRequestedNumColumns = numColumns;
1731            requestLayoutIfNecessary();
1732        }
1733    }
1734
1735    /**
1736     * Make sure views are touching the top or bottom edge, as appropriate for
1737     * our gravity
1738     */
1739    private void adjustViewsUpOrDown() {
1740        final int childCount = getChildCount();
1741
1742        if (childCount > 0) {
1743            int delta;
1744            View child;
1745
1746            if (!mStackFromBottom) {
1747                // Uh-oh -- we came up short. Slide all views up to make them
1748                // align with the top
1749                child = getChildAt(0);
1750                delta = child.getTop() - mListPadding.top;
1751                if (mFirstPosition != 0) {
1752                    // It's OK to have some space above the first item if it is
1753                    // part of the vertical spacing
1754                    delta -= mVerticalSpacing;
1755                }
1756                if (delta < 0) {
1757                    // We only are looking to see if we are too low, not too high
1758                    delta = 0;
1759                }
1760            } else {
1761                // we are too high, slide all views down to align with bottom
1762                child = getChildAt(childCount - 1);
1763                delta = child.getBottom() - (getHeight() - mListPadding.bottom);
1764
1765                if (mFirstPosition + childCount < mItemCount) {
1766                    // It's OK to have some space below the last item if it is
1767                    // part of the vertical spacing
1768                    delta += mVerticalSpacing;
1769                }
1770
1771                if (delta > 0) {
1772                    // We only are looking to see if we are too high, not too low
1773                    delta = 0;
1774                }
1775            }
1776
1777            if (delta != 0) {
1778                offsetChildrenTopAndBottom(-delta);
1779            }
1780        }
1781    }
1782
1783    @Override
1784    protected int computeVerticalScrollExtent() {
1785        final int count = getChildCount();
1786        if (count > 0) {
1787            final int numColumns = mNumColumns;
1788            final int rowCount = (count + numColumns - 1) / numColumns;
1789
1790            int extent = rowCount * 100;
1791
1792            View view = getChildAt(0);
1793            final int top = view.getTop();
1794            int height = view.getHeight();
1795            if (height > 0) {
1796                extent += (top * 100) / height;
1797            }
1798
1799            view = getChildAt(count - 1);
1800            final int bottom = view.getBottom();
1801            height = view.getHeight();
1802            if (height > 0) {
1803                extent -= ((bottom - getHeight()) * 100) / height;
1804            }
1805
1806            return extent;
1807        }
1808        return 0;
1809    }
1810
1811    @Override
1812    protected int computeVerticalScrollOffset() {
1813        if (mFirstPosition >= 0 && getChildCount() > 0) {
1814            final View view = getChildAt(0);
1815            final int top = view.getTop();
1816            int height = view.getHeight();
1817            if (height > 0) {
1818                final int whichRow = mFirstPosition / mNumColumns;
1819                return Math.max(whichRow * 100 - (top * 100) / height, 0);
1820            }
1821        }
1822        return 0;
1823    }
1824
1825    @Override
1826    protected int computeVerticalScrollRange() {
1827        // TODO: Account for vertical spacing too
1828        final int numColumns = mNumColumns;
1829        final int rowCount = (mItemCount + numColumns - 1) / numColumns;
1830        return Math.max(rowCount * 100, 0);
1831    }
1832}
1833
1834