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