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