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