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