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