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 Spinnners 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 
layout(int delta, boolean animate)313     abstract void layout(int delta, boolean animate);
314 
315     @Override
getSelectedView()316     public View getSelectedView() {
317         if (mItemCount > 0 && mSelectedPosition >= 0) {
318             return getChildAt(mSelectedPosition - mFirstPosition);
319         } else {
320             return null;
321         }
322     }
323 
324     /**
325      * Override to prevent spamming ourselves with layout requests
326      * as we place views
327      *
328      * @see android.view.View#requestLayout()
329      */
330     @Override
requestLayout()331     public void requestLayout() {
332         if (!mBlockLayoutRequests) {
333             super.requestLayout();
334         }
335     }
336 
337     @Override
getAdapter()338     public SpinnerAdapter getAdapter() {
339         return mAdapter;
340     }
341 
342     @Override
getCount()343     public int getCount() {
344         return mItemCount;
345     }
346 
347     /**
348      * Maps a point to a position in the list.
349      *
350      * @param x X in local coordinate
351      * @param y Y in local coordinate
352      * @return The position of the item which contains the specified point, or
353      *         {@link #INVALID_POSITION} if the point does not intersect an item.
354      */
pointToPosition(int x, int y)355     public int pointToPosition(int x, int y) {
356         Rect frame = mTouchFrame;
357         if (frame == null) {
358             mTouchFrame = new Rect();
359             frame = mTouchFrame;
360         }
361 
362         final int count = getChildCount();
363         for (int i = count - 1; i >= 0; i--) {
364             View child = getChildAt(i);
365             if (child.getVisibility() == View.VISIBLE) {
366                 child.getHitRect(frame);
367                 if (frame.contains(x, y)) {
368                     return mFirstPosition + i;
369                 }
370             }
371         }
372         return INVALID_POSITION;
373     }
374 
375     @Override
dispatchRestoreInstanceState(SparseArray<Parcelable> container)376     protected void dispatchRestoreInstanceState(SparseArray<Parcelable> container) {
377         super.dispatchRestoreInstanceState(container);
378         // Restores the selected position when Spinner gets restored,
379         // rather than wait until the next measure/layout pass to do it.
380         handleDataChanged();
381     }
382 
383     static class SavedState extends BaseSavedState {
384         long selectedId;
385         int position;
386 
387         /**
388          * Constructor called from {@link AbsSpinner#onSaveInstanceState()}
389          */
SavedState(Parcelable superState)390         SavedState(Parcelable superState) {
391             super(superState);
392         }
393 
394         /**
395          * Constructor called from {@link #CREATOR}
396          */
SavedState(Parcel in)397         SavedState(Parcel in) {
398             super(in);
399             selectedId = in.readLong();
400             position = in.readInt();
401         }
402 
403         @Override
writeToParcel(Parcel out, int flags)404         public void writeToParcel(Parcel out, int flags) {
405             super.writeToParcel(out, flags);
406             out.writeLong(selectedId);
407             out.writeInt(position);
408         }
409 
410         @Override
toString()411         public String toString() {
412             return "AbsSpinner.SavedState{"
413                     + Integer.toHexString(System.identityHashCode(this))
414                     + " selectedId=" + selectedId
415                     + " position=" + position + "}";
416         }
417 
418         public static final @android.annotation.NonNull Parcelable.Creator<SavedState> CREATOR
419                 = new Parcelable.Creator<SavedState>() {
420             public SavedState createFromParcel(Parcel in) {
421                 return new SavedState(in);
422             }
423 
424             public SavedState[] newArray(int size) {
425                 return new SavedState[size];
426             }
427         };
428     }
429 
430     @Override
onSaveInstanceState()431     public Parcelable onSaveInstanceState() {
432         Parcelable superState = super.onSaveInstanceState();
433         SavedState ss = new SavedState(superState);
434         ss.selectedId = getSelectedItemId();
435         if (ss.selectedId >= 0) {
436             ss.position = getSelectedItemPosition();
437         } else {
438             ss.position = INVALID_POSITION;
439         }
440         return ss;
441     }
442 
443     @Override
onRestoreInstanceState(Parcelable state)444     public void onRestoreInstanceState(Parcelable state) {
445         SavedState ss = (SavedState) state;
446 
447         super.onRestoreInstanceState(ss.getSuperState());
448 
449         if (ss.selectedId >= 0) {
450             mDataChanged = true;
451             mNeedSync = true;
452             mSyncRowId = ss.selectedId;
453             mSyncPosition = ss.position;
454             mSyncMode = SYNC_SELECTED_POSITION;
455             requestLayout();
456         }
457     }
458 
459     class RecycleBin {
460         private final SparseArray<View> mScrapHeap = new SparseArray<View>();
461 
put(int position, View v)462         public void put(int position, View v) {
463             mScrapHeap.put(position, v);
464         }
465 
get(int position)466         View get(int position) {
467             // System.out.print("Looking for " + position);
468             View result = mScrapHeap.get(position);
469             if (result != null) {
470                 // System.out.println(" HIT");
471                 mScrapHeap.delete(position);
472             } else {
473                 // System.out.println(" MISS");
474             }
475             return result;
476         }
477 
clear()478         void clear() {
479             final SparseArray<View> scrapHeap = mScrapHeap;
480             final int count = scrapHeap.size();
481             for (int i = 0; i < count; i++) {
482                 final View view = scrapHeap.valueAt(i);
483                 if (view != null) {
484                     removeDetachedView(view, true);
485                 }
486             }
487             scrapHeap.clear();
488         }
489     }
490 
491     @Override
getAccessibilityClassName()492     public CharSequence getAccessibilityClassName() {
493         return AbsSpinner.class.getName();
494     }
495 
496     @Override
autofill(AutofillValue value)497     public void autofill(AutofillValue value) {
498         if (!isEnabled()) return;
499 
500         if (!value.isList()) {
501             Log.w(LOG_TAG, value + " could not be autofilled into " + this);
502             return;
503         }
504 
505         setSelection(value.getListValue());
506     }
507 
508     @Override
getAutofillType()509     public @AutofillType int getAutofillType() {
510         return isEnabled() ? AUTOFILL_TYPE_LIST : AUTOFILL_TYPE_NONE;
511     }
512 
513     @Override
getAutofillValue()514     public AutofillValue getAutofillValue() {
515         return isEnabled() ? AutofillValue.forList(getSelectedItemPosition()) : null;
516     }
517 }
518