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