• 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                 if (view.getLayoutParams() == null) {
205                     mBlockLayoutRequests = true;
206                     view.setLayoutParams(generateDefaultLayoutParams());
207                     mBlockLayoutRequests = false;
208                 }
209                 measureChild(view, widthMeasureSpec, heightMeasureSpec);
210 
211                 preferredHeight = getChildHeight(view) + mSpinnerPadding.top + mSpinnerPadding.bottom;
212                 preferredWidth = getChildWidth(view) + mSpinnerPadding.left + mSpinnerPadding.right;
213 
214                 needsMeasuring = false;
215             }
216         }
217 
218         if (needsMeasuring) {
219             // No views -- just use padding
220             preferredHeight = mSpinnerPadding.top + mSpinnerPadding.bottom;
221             if (widthMode == MeasureSpec.UNSPECIFIED) {
222                 preferredWidth = mSpinnerPadding.left + mSpinnerPadding.right;
223             }
224         }
225 
226         preferredHeight = Math.max(preferredHeight, getSuggestedMinimumHeight());
227         preferredWidth = Math.max(preferredWidth, getSuggestedMinimumWidth());
228 
229         heightSize = resolveSizeAndState(preferredHeight, heightMeasureSpec, 0);
230         widthSize = resolveSizeAndState(preferredWidth, widthMeasureSpec, 0);
231 
232         setMeasuredDimension(widthSize, heightSize);
233         mHeightMeasureSpec = heightMeasureSpec;
234         mWidthMeasureSpec = widthMeasureSpec;
235     }
236 
getChildHeight(View child)237     int getChildHeight(View child) {
238         return child.getMeasuredHeight();
239     }
240 
getChildWidth(View child)241     int getChildWidth(View child) {
242         return child.getMeasuredWidth();
243     }
244 
245     @Override
generateDefaultLayoutParams()246     protected ViewGroup.LayoutParams generateDefaultLayoutParams() {
247         return new ViewGroup.LayoutParams(
248                 ViewGroup.LayoutParams.MATCH_PARENT,
249                 ViewGroup.LayoutParams.WRAP_CONTENT);
250     }
251 
recycleAllViews()252     void recycleAllViews() {
253         final int childCount = getChildCount();
254         final AbsSpinner.RecycleBin recycleBin = mRecycler;
255         final int position = mFirstPosition;
256 
257         // All views go in recycler
258         for (int i = 0; i < childCount; i++) {
259             View v = getChildAt(i);
260             int index = position + i;
261             recycleBin.put(index, v);
262         }
263     }
264 
265     /**
266      * Jump directly to a specific item in the adapter data.
267      */
setSelection(int position, boolean animate)268     public void setSelection(int position, boolean animate) {
269         // Animate only if requested position is already on screen somewhere
270         boolean shouldAnimate = animate && mFirstPosition <= position &&
271                 position <= mFirstPosition + getChildCount() - 1;
272         setSelectionInt(position, shouldAnimate);
273     }
274 
275     @Override
setSelection(int position)276     public void setSelection(int position) {
277         setNextSelectedPositionInt(position);
278         requestLayout();
279         invalidate();
280     }
281 
282 
283     /**
284      * Makes the item at the supplied position selected.
285      *
286      * @param position Position to select
287      * @param animate Should the transition be animated
288      *
289      */
setSelectionInt(int position, boolean animate)290     void setSelectionInt(int position, boolean animate) {
291         if (position != mOldSelectedPosition) {
292             mBlockLayoutRequests = true;
293             int delta  = position - mSelectedPosition;
294             setNextSelectedPositionInt(position);
295             layout(delta, animate);
296             mBlockLayoutRequests = false;
297         }
298     }
299 
layout(int delta, boolean animate)300     abstract void layout(int delta, boolean animate);
301 
302     @Override
getSelectedView()303     public View getSelectedView() {
304         if (mItemCount > 0 && mSelectedPosition >= 0) {
305             return getChildAt(mSelectedPosition - mFirstPosition);
306         } else {
307             return null;
308         }
309     }
310 
311     /**
312      * Override to prevent spamming ourselves with layout requests
313      * as we place views
314      *
315      * @see android.view.View#requestLayout()
316      */
317     @Override
requestLayout()318     public void requestLayout() {
319         if (!mBlockLayoutRequests) {
320             super.requestLayout();
321         }
322     }
323 
324     @Override
getAdapter()325     public SpinnerAdapter getAdapter() {
326         return mAdapter;
327     }
328 
329     @Override
getCount()330     public int getCount() {
331         return mItemCount;
332     }
333 
334     /**
335      * Maps a point to a position in the list.
336      *
337      * @param x X in local coordinate
338      * @param y Y in local coordinate
339      * @return The position of the item which contains the specified point, or
340      *         {@link #INVALID_POSITION} if the point does not intersect an item.
341      */
pointToPosition(int x, int y)342     public int pointToPosition(int x, int y) {
343         Rect frame = mTouchFrame;
344         if (frame == null) {
345             mTouchFrame = new Rect();
346             frame = mTouchFrame;
347         }
348 
349         final int count = getChildCount();
350         for (int i = count - 1; i >= 0; i--) {
351             View child = getChildAt(i);
352             if (child.getVisibility() == View.VISIBLE) {
353                 child.getHitRect(frame);
354                 if (frame.contains(x, y)) {
355                     return mFirstPosition + i;
356                 }
357             }
358         }
359         return INVALID_POSITION;
360     }
361 
362     static class SavedState extends BaseSavedState {
363         long selectedId;
364         int position;
365 
366         /**
367          * Constructor called from {@link AbsSpinner#onSaveInstanceState()}
368          */
SavedState(Parcelable superState)369         SavedState(Parcelable superState) {
370             super(superState);
371         }
372 
373         /**
374          * Constructor called from {@link #CREATOR}
375          */
SavedState(Parcel in)376         SavedState(Parcel in) {
377             super(in);
378             selectedId = in.readLong();
379             position = in.readInt();
380         }
381 
382         @Override
writeToParcel(Parcel out, int flags)383         public void writeToParcel(Parcel out, int flags) {
384             super.writeToParcel(out, flags);
385             out.writeLong(selectedId);
386             out.writeInt(position);
387         }
388 
389         @Override
toString()390         public String toString() {
391             return "AbsSpinner.SavedState{"
392                     + Integer.toHexString(System.identityHashCode(this))
393                     + " selectedId=" + selectedId
394                     + " position=" + position + "}";
395         }
396 
397         public static final Parcelable.Creator<SavedState> CREATOR
398                 = new Parcelable.Creator<SavedState>() {
399             public SavedState createFromParcel(Parcel in) {
400                 return new SavedState(in);
401             }
402 
403             public SavedState[] newArray(int size) {
404                 return new SavedState[size];
405             }
406         };
407     }
408 
409     @Override
onSaveInstanceState()410     public Parcelable onSaveInstanceState() {
411         Parcelable superState = super.onSaveInstanceState();
412         SavedState ss = new SavedState(superState);
413         ss.selectedId = getSelectedItemId();
414         if (ss.selectedId >= 0) {
415             ss.position = getSelectedItemPosition();
416         } else {
417             ss.position = INVALID_POSITION;
418         }
419         return ss;
420     }
421 
422     @Override
onRestoreInstanceState(Parcelable state)423     public void onRestoreInstanceState(Parcelable state) {
424         SavedState ss = (SavedState) state;
425 
426         super.onRestoreInstanceState(ss.getSuperState());
427 
428         if (ss.selectedId >= 0) {
429             mDataChanged = true;
430             mNeedSync = true;
431             mSyncRowId = ss.selectedId;
432             mSyncPosition = ss.position;
433             mSyncMode = SYNC_SELECTED_POSITION;
434             requestLayout();
435         }
436     }
437 
438     class RecycleBin {
439         private final SparseArray<View> mScrapHeap = new SparseArray<View>();
440 
put(int position, View v)441         public void put(int position, View v) {
442             mScrapHeap.put(position, v);
443         }
444 
get(int position)445         View get(int position) {
446             // System.out.print("Looking for " + position);
447             View result = mScrapHeap.get(position);
448             if (result != null) {
449                 // System.out.println(" HIT");
450                 mScrapHeap.delete(position);
451             } else {
452                 // System.out.println(" MISS");
453             }
454             return result;
455         }
456 
clear()457         void clear() {
458             final SparseArray<View> scrapHeap = mScrapHeap;
459             final int count = scrapHeap.size();
460             for (int i = 0; i < count; i++) {
461                 final View view = scrapHeap.valueAt(i);
462                 if (view != null) {
463                     removeDetachedView(view, true);
464                 }
465             }
466             scrapHeap.clear();
467         }
468     }
469 
470     @Override
onInitializeAccessibilityEvent(AccessibilityEvent event)471     public void onInitializeAccessibilityEvent(AccessibilityEvent event) {
472         super.onInitializeAccessibilityEvent(event);
473         event.setClassName(AbsSpinner.class.getName());
474     }
475 
476     @Override
onInitializeAccessibilityNodeInfo(AccessibilityNodeInfo info)477     public void onInitializeAccessibilityNodeInfo(AccessibilityNodeInfo info) {
478         super.onInitializeAccessibilityNodeInfo(info);
479         info.setClassName(AbsSpinner.class.getName());
480     }
481 }
482