1 /*
2  * Copyright (C) 2006 The Android Open Source Project
3  *
4  * Licensed under the Apache License, Version 2.0 (the "License");
5  * you may not use this file except in compliance with the License.
6  * You may obtain a copy of the License at
7  *
8  *      http://www.apache.org/licenses/LICENSE-2.0
9  *
10  * Unless required by applicable law or agreed to in writing, software
11  * distributed under the License is distributed on an "AS IS" BASIS,
12  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13  * See the License for the specific language governing permissions and
14  * limitations under the License.
15  */
16 
17 package android.widget;
18 
19 import android.content.Context;
20 import android.content.res.TypedArray;
21 import android.database.DataSetObserver;
22 import android.graphics.Rect;
23 import android.os.Parcel;
24 import android.os.Parcelable;
25 import android.util.AttributeSet;
26 import android.util.Log;
27 import android.util.SparseArray;
28 import android.view.View;
29 import android.view.ViewGroup;
30 import android.view.autofill.AutofillValue;
31 
32 import com.android.internal.R;
33 
34 /**
35  * An abstract base class for spinner widgets. SDK users will probably not
36  * need to use this class.
37  *
38  * @attr ref android.R.styleable#AbsSpinner_entries
39  */
40 public abstract class AbsSpinner extends AdapterView<SpinnerAdapter> {
41     private static final String LOG_TAG = AbsSpinner.class.getSimpleName();
42 
43     SpinnerAdapter mAdapter;
44 
45     int mHeightMeasureSpec;
46     int mWidthMeasureSpec;
47 
48     int mSelectionLeftPadding = 0;
49     int mSelectionTopPadding = 0;
50     int mSelectionRightPadding = 0;
51     int mSelectionBottomPadding = 0;
52     final Rect mSpinnerPadding = new Rect();
53 
54     final RecycleBin mRecycler = new RecycleBin();
55     private DataSetObserver mDataSetObserver;
56 
57     /** Temporary frame to hold a child View's frame rectangle */
58     private Rect mTouchFrame;
59 
AbsSpinner(Context context)60     public AbsSpinner(Context context) {
61         super(context);
62         initAbsSpinner();
63     }
64 
AbsSpinner(Context context, AttributeSet attrs)65     public AbsSpinner(Context context, AttributeSet attrs) {
66         this(context, attrs, 0);
67     }
68 
AbsSpinner(Context context, AttributeSet attrs, int defStyleAttr)69     public AbsSpinner(Context context, AttributeSet attrs, int defStyleAttr) {
70         this(context, attrs, defStyleAttr, 0);
71     }
72 
AbsSpinner(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes)73     public AbsSpinner(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) {
74         super(context, attrs, defStyleAttr, defStyleRes);
75 
76         // Spinner is important by default, unless app developer overrode attribute.
77         if (getImportantForAutofill() == IMPORTANT_FOR_AUTOFILL_AUTO) {
78             setImportantForAutofill(IMPORTANT_FOR_AUTOFILL_YES);
79         }
80 
81         initAbsSpinner();
82 
83         final TypedArray a = context.obtainStyledAttributes(
84                 attrs, R.styleable.AbsSpinner, defStyleAttr, defStyleRes);
85         saveAttributeDataForStyleable(context, R.styleable.AbsSpinner, attrs, a, defStyleAttr,
86                 defStyleRes);
87 
88         final CharSequence[] entries = a.getTextArray(R.styleable.AbsSpinner_entries);
89         if (entries != null) {
90             final ArrayAdapter<CharSequence> adapter = new ArrayAdapter<CharSequence>(
91                     context, R.layout.simple_spinner_item, entries);
92             adapter.setDropDownViewResource(R.layout.simple_spinner_dropdown_item);
93             setAdapter(adapter);
94         }
95 
96         a.recycle();
97     }
98 
99     /**
100      * Common code for different constructor flavors
101      */
initAbsSpinner()102     private void initAbsSpinner() {
103         setFocusable(true);
104         setWillNotDraw(false);
105     }
106 
107     /**
108      * The Adapter is used to provide the data which backs this Spinner.
109      * It also provides methods to transform spinner items based on their position
110      * relative to the selected item.
111      * @param adapter The SpinnerAdapter to use for this Spinner
112      */
113     @Override
setAdapter(SpinnerAdapter adapter)114     public void setAdapter(SpinnerAdapter adapter) {
115         if (null != mAdapter) {
116             mAdapter.unregisterDataSetObserver(mDataSetObserver);
117             resetList();
118         }
119 
120         mAdapter = adapter;
121 
122         mOldSelectedPosition = INVALID_POSITION;
123         mOldSelectedRowId = INVALID_ROW_ID;
124 
125         if (mAdapter != null) {
126             mOldItemCount = mItemCount;
127             mItemCount = mAdapter.getCount();
128             checkFocus();
129 
130             mDataSetObserver = new AdapterDataSetObserver();
131             mAdapter.registerDataSetObserver(mDataSetObserver);
132 
133             int position = mItemCount > 0 ? 0 : INVALID_POSITION;
134 
135             setSelectedPositionInt(position);
136             setNextSelectedPositionInt(position);
137 
138             if (mItemCount == 0) {
139                 // Nothing selected
140                 checkSelectionChanged();
141             }
142 
143         } else {
144             checkFocus();
145             resetList();
146             // Nothing selected
147             checkSelectionChanged();
148         }
149 
150         requestLayout();
151     }
152 
153     /**
154      * Clear out all children from the list
155      */
resetList()156     void resetList() {
157         mDataChanged = false;
158         mNeedSync = false;
159 
160         removeAllViewsInLayout();
161         mOldSelectedPosition = INVALID_POSITION;
162         mOldSelectedRowId = INVALID_ROW_ID;
163 
164         setSelectedPositionInt(INVALID_POSITION);
165         setNextSelectedPositionInt(INVALID_POSITION);
166         invalidate();
167     }
168 
169     /**
170      * @see android.view.View#measure(int, int)
171      *
172      * Figure out the dimensions of this Spinner. The width comes from
173      * the widthMeasureSpec as Spinners can't have their width set to
174      * UNSPECIFIED. The height is based on the height of the selected item
175      * plus padding.
176      */
177     @Override
onMeasure(int widthMeasureSpec, int heightMeasureSpec)178     protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
179         int widthMode = MeasureSpec.getMode(widthMeasureSpec);
180         int widthSize;
181         int heightSize;
182 
183         mSpinnerPadding.left = mPaddingLeft > mSelectionLeftPadding ? mPaddingLeft
184                 : mSelectionLeftPadding;
185         mSpinnerPadding.top = mPaddingTop > mSelectionTopPadding ? mPaddingTop
186                 : mSelectionTopPadding;
187         mSpinnerPadding.right = mPaddingRight > mSelectionRightPadding ? mPaddingRight
188                 : mSelectionRightPadding;
189         mSpinnerPadding.bottom = mPaddingBottom > mSelectionBottomPadding ? mPaddingBottom
190                 : mSelectionBottomPadding;
191 
192         if (mDataChanged) {
193             handleDataChanged();
194         }
195 
196         int preferredHeight = 0;
197         int preferredWidth = 0;
198         boolean needsMeasuring = true;
199 
200         int selectedPosition = getSelectedItemPosition();
201         if (selectedPosition >= 0 && mAdapter != null && selectedPosition < mAdapter.getCount()) {
202             // Try looking in the recycler. (Maybe we were measured once already)
203             View view = mRecycler.get(selectedPosition);
204             if (view == null) {
205                 // Make a new one
206                 view = mAdapter.getView(selectedPosition, null, this);
207 
208                 if (view.getImportantForAccessibility() == IMPORTANT_FOR_ACCESSIBILITY_AUTO) {
209                     view.setImportantForAccessibility(IMPORTANT_FOR_ACCESSIBILITY_YES);
210                 }
211             }
212 
213             if (view != null) {
214                 // Put in recycler for re-measuring and/or layout
215                 mRecycler.put(selectedPosition, view);
216 
217                 if (view.getLayoutParams() == null) {
218                     mBlockLayoutRequests = true;
219                     view.setLayoutParams(generateDefaultLayoutParams());
220                     mBlockLayoutRequests = false;
221                 }
222                 measureChild(view, widthMeasureSpec, heightMeasureSpec);
223 
224                 preferredHeight = getChildHeight(view) + mSpinnerPadding.top + mSpinnerPadding.bottom;
225                 preferredWidth = getChildWidth(view) + mSpinnerPadding.left + mSpinnerPadding.right;
226 
227                 needsMeasuring = false;
228             }
229         }
230 
231         if (needsMeasuring) {
232             // No views -- just use padding
233             preferredHeight = mSpinnerPadding.top + mSpinnerPadding.bottom;
234             if (widthMode == MeasureSpec.UNSPECIFIED) {
235                 preferredWidth = mSpinnerPadding.left + mSpinnerPadding.right;
236             }
237         }
238 
239         preferredHeight = Math.max(preferredHeight, getSuggestedMinimumHeight());
240         preferredWidth = Math.max(preferredWidth, getSuggestedMinimumWidth());
241 
242         heightSize = resolveSizeAndState(preferredHeight, heightMeasureSpec, 0);
243         widthSize = resolveSizeAndState(preferredWidth, widthMeasureSpec, 0);
244 
245         setMeasuredDimension(widthSize, heightSize);
246         mHeightMeasureSpec = heightMeasureSpec;
247         mWidthMeasureSpec = widthMeasureSpec;
248     }
249 
getChildHeight(View child)250     int getChildHeight(View child) {
251         return child.getMeasuredHeight();
252     }
253 
getChildWidth(View child)254     int getChildWidth(View child) {
255         return child.getMeasuredWidth();
256     }
257 
258     @Override
generateDefaultLayoutParams()259     protected ViewGroup.LayoutParams generateDefaultLayoutParams() {
260         return new ViewGroup.LayoutParams(
261                 ViewGroup.LayoutParams.MATCH_PARENT,
262                 ViewGroup.LayoutParams.WRAP_CONTENT);
263     }
264 
recycleAllViews()265     void recycleAllViews() {
266         final int childCount = getChildCount();
267         final AbsSpinner.RecycleBin recycleBin = mRecycler;
268         final int position = mFirstPosition;
269 
270         // All views go in recycler
271         for (int i = 0; i < childCount; i++) {
272             View v = getChildAt(i);
273             int index = position + i;
274             recycleBin.put(index, v);
275         }
276     }
277 
278     /**
279      * Jump directly to a specific item in the adapter data.
280      */
setSelection(int position, boolean animate)281     public void setSelection(int position, boolean animate) {
282         // Animate only if requested position is already on screen somewhere
283         boolean shouldAnimate = animate && mFirstPosition <= position &&
284                 position <= mFirstPosition + getChildCount() - 1;
285         setSelectionInt(position, shouldAnimate);
286     }
287 
288     @Override
setSelection(int position)289     public void setSelection(int position) {
290         setNextSelectedPositionInt(position);
291         requestLayout();
292         invalidate();
293     }
294 
295 
296     /**
297      * Makes the item at the supplied position selected.
298      *
299      * @param position Position to select
300      * @param animate Should the transition be animated
301      *
302      */
setSelectionInt(int position, boolean animate)303     void setSelectionInt(int position, boolean animate) {
304         if (position != mOldSelectedPosition) {
305             mBlockLayoutRequests = true;
306             int delta  = position - mSelectedPosition;
307             setNextSelectedPositionInt(position);
308             layout(delta, animate);
309             mBlockLayoutRequests = false;
310         }
311     }
312 
313     @SuppressWarnings("HiddenAbstractMethod")
layout(int delta, boolean animate)314     abstract void layout(int delta, boolean animate);
315 
316     @Override
getSelectedView()317     public View getSelectedView() {
318         if (mItemCount > 0 && mSelectedPosition >= 0) {
319             return getChildAt(mSelectedPosition - mFirstPosition);
320         } else {
321             return null;
322         }
323     }
324 
325     /**
326      * Override to prevent spamming ourselves with layout requests
327      * as we place views
328      *
329      * @see android.view.View#requestLayout()
330      */
331     @Override
requestLayout()332     public void requestLayout() {
333         if (!mBlockLayoutRequests) {
334             super.requestLayout();
335         }
336     }
337 
338     @Override
getAdapter()339     public SpinnerAdapter getAdapter() {
340         return mAdapter;
341     }
342 
343     @Override
getCount()344     public int getCount() {
345         return mItemCount;
346     }
347 
348     /**
349      * Maps a point to a position in the list.
350      *
351      * @param x X in local coordinate
352      * @param y Y in local coordinate
353      * @return The position of the item which contains the specified point, or
354      *         {@link #INVALID_POSITION} if the point does not intersect an item.
355      */
pointToPosition(int x, int y)356     public int pointToPosition(int x, int y) {
357         Rect frame = mTouchFrame;
358         if (frame == null) {
359             mTouchFrame = new Rect();
360             frame = mTouchFrame;
361         }
362 
363         final int count = getChildCount();
364         for (int i = count - 1; i >= 0; i--) {
365             View child = getChildAt(i);
366             if (child.getVisibility() == View.VISIBLE) {
367                 child.getHitRect(frame);
368                 if (frame.contains(x, y)) {
369                     return mFirstPosition + i;
370                 }
371             }
372         }
373         return INVALID_POSITION;
374     }
375 
376     @Override
dispatchRestoreInstanceState(SparseArray<Parcelable> container)377     protected void dispatchRestoreInstanceState(SparseArray<Parcelable> container) {
378         super.dispatchRestoreInstanceState(container);
379         // Restores the selected position when Spinner gets restored,
380         // rather than wait until the next measure/layout pass to do it.
381         handleDataChanged();
382     }
383 
384     static class SavedState extends BaseSavedState {
385         long selectedId;
386         int position;
387 
388         /**
389          * Constructor called from {@link AbsSpinner#onSaveInstanceState()}
390          */
SavedState(Parcelable superState)391         SavedState(Parcelable superState) {
392             super(superState);
393         }
394 
395         /**
396          * Constructor called from {@link #CREATOR}
397          */
SavedState(Parcel in)398         SavedState(Parcel in) {
399             super(in);
400             selectedId = in.readLong();
401             position = in.readInt();
402         }
403 
404         @Override
writeToParcel(Parcel out, int flags)405         public void writeToParcel(Parcel out, int flags) {
406             super.writeToParcel(out, flags);
407             out.writeLong(selectedId);
408             out.writeInt(position);
409         }
410 
411         @Override
toString()412         public String toString() {
413             return "AbsSpinner.SavedState{"
414                     + Integer.toHexString(System.identityHashCode(this))
415                     + " selectedId=" + selectedId
416                     + " position=" + position + "}";
417         }
418 
419         public static final @android.annotation.NonNull Parcelable.Creator<SavedState> CREATOR
420                 = new Parcelable.Creator<SavedState>() {
421             public SavedState createFromParcel(Parcel in) {
422                 return new SavedState(in);
423             }
424 
425             public SavedState[] newArray(int size) {
426                 return new SavedState[size];
427             }
428         };
429     }
430 
431     @Override
onSaveInstanceState()432     public Parcelable onSaveInstanceState() {
433         Parcelable superState = super.onSaveInstanceState();
434         SavedState ss = new SavedState(superState);
435         ss.selectedId = getSelectedItemId();
436         if (ss.selectedId >= 0) {
437             ss.position = getSelectedItemPosition();
438         } else {
439             ss.position = INVALID_POSITION;
440         }
441         return ss;
442     }
443 
444     @Override
onRestoreInstanceState(Parcelable state)445     public void onRestoreInstanceState(Parcelable state) {
446         SavedState ss = (SavedState) state;
447 
448         super.onRestoreInstanceState(ss.getSuperState());
449 
450         if (ss.selectedId >= 0) {
451             mDataChanged = true;
452             mNeedSync = true;
453             mSyncRowId = ss.selectedId;
454             mSyncPosition = ss.position;
455             mSyncMode = SYNC_SELECTED_POSITION;
456             requestLayout();
457         }
458     }
459 
460     class RecycleBin {
461         private final SparseArray<View> mScrapHeap = new SparseArray<View>();
462 
put(int position, View v)463         public void put(int position, View v) {
464             mScrapHeap.put(position, v);
465         }
466 
get(int position)467         View get(int position) {
468             // System.out.print("Looking for " + position);
469             View result = mScrapHeap.get(position);
470             if (result != null) {
471                 // System.out.println(" HIT");
472                 mScrapHeap.delete(position);
473             } else {
474                 // System.out.println(" MISS");
475             }
476             return result;
477         }
478 
clear()479         void clear() {
480             final SparseArray<View> scrapHeap = mScrapHeap;
481             final int count = scrapHeap.size();
482             for (int i = 0; i < count; i++) {
483                 final View view = scrapHeap.valueAt(i);
484                 if (view != null) {
485                     removeDetachedView(view, true);
486                 }
487             }
488             scrapHeap.clear();
489         }
490     }
491 
492     @Override
getAccessibilityClassName()493     public CharSequence getAccessibilityClassName() {
494         return AbsSpinner.class.getName();
495     }
496 
497     @Override
autofill(AutofillValue value)498     public void autofill(AutofillValue value) {
499         if (!isEnabled()) return;
500 
501         if (!value.isList()) {
502             Log.w(LOG_TAG, value + " could not be autofilled into " + this);
503             return;
504         }
505 
506         setSelection(value.getListValue());
507     }
508 
509     @Override
getAutofillType()510     public @AutofillType int getAutofillType() {
511         return isEnabled() ? AUTOFILL_TYPE_LIST : AUTOFILL_TYPE_NONE;
512     }
513 
514     @Override
getAutofillValue()515     public AutofillValue getAutofillValue() {
516         return isEnabled() ? AutofillValue.forList(getSelectedItemPosition()) : null;
517     }
518 }
519