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