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