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