blob: 2b3b98d60f1268077e55f73f1145f96feaab3a62 [file] [log] [blame]
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08001/*
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
17package android.widget;
18
19import com.android.internal.R;
20
21import android.content.Context;
22import android.content.res.TypedArray;
23import android.database.DataSetObserver;
24import android.graphics.Rect;
25import android.os.Parcel;
26import android.os.Parcelable;
27import android.util.AttributeSet;
28import android.util.SparseArray;
29import android.view.View;
30import android.view.ViewGroup;
The Android Open Source Project9066cfe2009-03-03 19:31:44 -080031
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 */
38public abstract class AbsSpinner extends AdapterView<SpinnerAdapter> {
The Android Open Source Project9066cfe2009-03-03 19:31:44 -080039 SpinnerAdapter mAdapter;
40
41 int mHeightMeasureSpec;
42 int mWidthMeasureSpec;
43 boolean mBlockLayoutRequests;
Romain Guyafc01552010-02-22 16:48:47 -080044
The Android Open Source Project9066cfe2009-03-03 19:31:44 -080045 int mSelectionLeftPadding = 0;
46 int mSelectionTopPadding = 0;
47 int mSelectionRightPadding = 0;
48 int mSelectionBottomPadding = 0;
Romain Guyafc01552010-02-22 16:48:47 -080049 final Rect mSpinnerPadding = new Rect();
The Android Open Source Project9066cfe2009-03-03 19:31:44 -080050
Romain Guyafc01552010-02-22 16:48:47 -080051 final RecycleBin mRecycler = new RecycleBin();
The Android Open Source Project9066cfe2009-03-03 19:31:44 -080052 private DataSetObserver mDataSetObserver;
53
The Android Open Source Project9066cfe2009-03-03 19:31:44 -080054 /** Temporary frame to hold a child View's frame rectangle */
55 private Rect mTouchFrame;
56
57 public AbsSpinner(Context context) {
58 super(context);
59 initAbsSpinner();
60 }
61
62 public AbsSpinner(Context context, AttributeSet attrs) {
63 this(context, attrs, 0);
64 }
65
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 */
88 private void initAbsSpinner() {
89 setFocusable(true);
90 setWillNotDraw(false);
91 }
92
The Android Open Source Project9066cfe2009-03-03 19:31:44 -080093 /**
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
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 */
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
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();
Romain Guyafc01552010-02-22 16:48:47 -0800187 if (selectedPosition >= 0 && mAdapter != null && selectedPosition < mAdapter.getCount()) {
The Android Open Source Project9066cfe2009-03-03 19:31:44 -0800188 // 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 = resolveSize(preferredHeight, heightMeasureSpec);
227 widthSize = resolveSize(preferredWidth, widthMeasureSpec);
228
229 setMeasuredDimension(widthSize, heightSize);
230 mHeightMeasureSpec = heightMeasureSpec;
231 mWidthMeasureSpec = widthMeasureSpec;
232 }
233
The Android Open Source Project9066cfe2009-03-03 19:31:44 -0800234 int getChildHeight(View child) {
235 return child.getMeasuredHeight();
236 }
237
238 int getChildWidth(View child) {
239 return child.getMeasuredWidth();
240 }
241
242 @Override
243 protected ViewGroup.LayoutParams generateDefaultLayoutParams() {
244 return new ViewGroup.LayoutParams(
Romain Guy980a9382010-01-08 15:06:28 -0800245 ViewGroup.LayoutParams.MATCH_PARENT,
The Android Open Source Project9066cfe2009-03-03 19:31:44 -0800246 ViewGroup.LayoutParams.WRAP_CONTENT);
247 }
248
249 void recycleAllViews() {
Romain Guyafc01552010-02-22 16:48:47 -0800250 final int childCount = getChildCount();
The Android Open Source Project9066cfe2009-03-03 19:31:44 -0800251 final AbsSpinner.RecycleBin recycleBin = mRecycler;
Romain Guyafc01552010-02-22 16:48:47 -0800252 final int position = mFirstPosition;
The Android Open Source Project9066cfe2009-03-03 19:31:44 -0800253
254 // All views go in recycler
Romain Guyafc01552010-02-22 16:48:47 -0800255 for (int i = 0; i < childCount; i++) {
The Android Open Source Project9066cfe2009-03-03 19:31:44 -0800256 View v = getChildAt(i);
Romain Guyafc01552010-02-22 16:48:47 -0800257 int index = position + i;
The Android Open Source Project9066cfe2009-03-03 19:31:44 -0800258 recycleBin.put(index, v);
259 }
260 }
The Android Open Source Project9066cfe2009-03-03 19:31:44 -0800261
262 /**
263 * Jump directly to a specific item in the adapter data.
264 */
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 }
The Android Open Source Project9066cfe2009-03-03 19:31:44 -0800271
272 @Override
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 */
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
297 abstract void layout(int delta, boolean animate);
298
299 @Override
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
315 public void requestLayout() {
316 if (!mBlockLayoutRequests) {
317 super.requestLayout();
318 }
319 }
320
The Android Open Source Project9066cfe2009-03-03 19:31:44 -0800321 @Override
322 public SpinnerAdapter getAdapter() {
323 return mAdapter;
324 }
325
326 @Override
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 */
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 */
366 SavedState(Parcelable superState) {
367 super(superState);
368 }
369
370 /**
371 * Constructor called from {@link #CREATOR}
372 */
373 private SavedState(Parcel in) {
374 super(in);
375 selectedId = in.readLong();
376 position = in.readInt();
377 }
378
379 @Override
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
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
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
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 {
Romain Guyafc01552010-02-22 16:48:47 -0800436 private final SparseArray<View> mScrapHeap = new SparseArray<View>();
The Android Open Source Project9066cfe2009-03-03 19:31:44 -0800437
438 public void put(int position, View v) {
439 mScrapHeap.put(position, v);
440 }
441
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 }
Romain Guyafc01552010-02-22 16:48:47 -0800453
The Android Open Source Project9066cfe2009-03-03 19:31:44 -0800454 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}