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