2 * Copyright (C) 2007 The Android Open Source Project
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
8 * http://www.apache.org/licenses/LICENSE-2.0
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.
17 package com.actionbarsherlock.internal.widget;
19 import static android.view.ViewGroup.LayoutParams.MATCH_PARENT;
20 import static android.view.ViewGroup.LayoutParams.WRAP_CONTENT;
21 import com.actionbarsherlock.R;
22 import android.content.Context;
23 import android.content.DialogInterface;
24 import android.content.DialogInterface.OnClickListener;
25 import android.content.res.TypedArray;
26 import android.database.DataSetObserver;
27 import android.graphics.Rect;
28 import android.graphics.drawable.Drawable;
29 import android.util.AttributeSet;
30 import android.view.Gravity;
31 import android.view.View;
32 import android.view.ViewGroup;
33 import android.widget.AdapterView;
34 import android.widget.AdapterView.OnItemClickListener;
35 import android.widget.ListAdapter;
36 import android.widget.ListView;
37 import android.widget.PopupWindow;
38 import android.widget.SpinnerAdapter;
42 * A view that displays one child at a time and lets the user pick among them.
43 * The items in the Spinner come from the {@link Adapter} associated with
46 * <p>See the <a href="{@docRoot}resources/tutorials/views/hello-spinner.html">Spinner
49 * @attr ref android.R.styleable#Spinner_prompt
51 public class IcsSpinner extends IcsAbsSpinner implements OnClickListener {
52 //private static final String TAG = "Spinner";
54 // Only measure this many items to get a decent max width.
55 private static final int MAX_ITEMS_MEASURED = 15;
58 * Use a dialog window for selecting spinner options.
60 //public static final int MODE_DIALOG = 0;
63 * Use a dropdown anchored to the Spinner for selecting spinner options.
65 public static final int MODE_DROPDOWN = 1;
68 * Use the theme-supplied value to select the dropdown mode.
70 //private static final int MODE_THEME = -1;
72 private SpinnerPopup mPopup;
73 private DropDownAdapter mTempAdapter;
77 private boolean mDisableChildrenWhenDisabled;
79 private Rect mTempRect = new Rect();
81 public IcsSpinner(Context context, AttributeSet attrs) {
82 this(context, attrs, R.attr.actionDropDownStyle);
86 * Construct a new spinner with the given context's theme, the supplied attribute set,
89 * @param context The Context the view is running in, through which it can
90 * access the current theme, resources, etc.
91 * @param attrs The attributes of the XML tag that is inflating the view.
92 * @param defStyle The default style to apply to this view. If 0, no style
93 * will be applied (beyond what is included in the theme). This may
94 * either be an attribute resource, whose value will be retrieved
95 * from the current theme, or an explicit style resource.
97 public IcsSpinner(Context context, AttributeSet attrs, int defStyle) {
98 super(context, attrs, defStyle);
100 TypedArray a = context.obtainStyledAttributes(attrs,
101 R.styleable.SherlockSpinner, defStyle, 0);
104 DropdownPopup popup = new DropdownPopup(context, attrs, defStyle);
106 mDropDownWidth = a.getLayoutDimension(
107 R.styleable.SherlockSpinner_android_dropDownWidth,
108 ViewGroup.LayoutParams.WRAP_CONTENT);
109 popup.setBackgroundDrawable(a.getDrawable(
110 R.styleable.SherlockSpinner_android_popupBackground));
111 final int verticalOffset = a.getDimensionPixelOffset(
112 R.styleable.SherlockSpinner_android_dropDownVerticalOffset, 0);
113 if (verticalOffset != 0) {
114 popup.setVerticalOffset(verticalOffset);
117 final int horizontalOffset = a.getDimensionPixelOffset(
118 R.styleable.SherlockSpinner_android_dropDownHorizontalOffset, 0);
119 if (horizontalOffset != 0) {
120 popup.setHorizontalOffset(horizontalOffset);
125 mGravity = a.getInt(R.styleable.SherlockSpinner_android_gravity, Gravity.CENTER);
127 mPopup.setPromptText(a.getString(R.styleable.SherlockSpinner_android_prompt));
129 mDisableChildrenWhenDisabled = true;
133 // Base constructor can call setAdapter before we initialize mPopup.
134 // Finish setting things up if this happened.
135 if (mTempAdapter != null) {
136 mPopup.setAdapter(mTempAdapter);
142 public void setEnabled(boolean enabled) {
143 super.setEnabled(enabled);
144 if (mDisableChildrenWhenDisabled) {
145 final int count = getChildCount();
146 for (int i = 0; i < count; i++) {
147 getChildAt(i).setEnabled(enabled);
153 * Describes how the selected item view is positioned. Currently only the horizontal component
154 * is used. The default is determined by the current theme.
156 * @param gravity See {@link android.view.Gravity}
158 * @attr ref android.R.styleable#Spinner_gravity
160 public void setGravity(int gravity) {
161 if (mGravity != gravity) {
162 if ((gravity & Gravity.HORIZONTAL_GRAVITY_MASK) == 0) {
163 gravity |= Gravity.LEFT;
171 public void setAdapter(SpinnerAdapter adapter) {
172 super.setAdapter(adapter);
174 if (mPopup != null) {
175 mPopup.setAdapter(new DropDownAdapter(adapter));
177 mTempAdapter = new DropDownAdapter(adapter);
182 public int getBaseline() {
185 if (getChildCount() > 0) {
186 child = getChildAt(0);
187 } else if (mAdapter != null && mAdapter.getCount() > 0) {
188 child = makeAndAddView(0);
189 mRecycler.put(0, child);
190 removeAllViewsInLayout();
194 final int childBaseline = child.getBaseline();
195 return childBaseline >= 0 ? child.getTop() + childBaseline : -1;
202 protected void onDetachedFromWindow() {
203 super.onDetachedFromWindow();
205 if (mPopup != null && mPopup.isShowing()) {
211 * <p>A spinner does not support item click events. Calling this method
212 * will raise an exception.</p>
214 * @param l this listener will be ignored
217 public void setOnItemClickListener(OnItemClickListener l) {
218 throw new RuntimeException("setOnItemClickListener cannot be used with a spinner.");
222 protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
223 super.onMeasure(widthMeasureSpec, heightMeasureSpec);
224 if (mPopup != null && MeasureSpec.getMode(widthMeasureSpec) == MeasureSpec.AT_MOST) {
225 final int measuredWidth = getMeasuredWidth();
226 setMeasuredDimension(Math.min(Math.max(measuredWidth,
227 measureContentWidth(getAdapter(), getBackground())),
228 MeasureSpec.getSize(widthMeasureSpec)),
229 getMeasuredHeight());
234 * @see android.view.View#onLayout(boolean,int,int,int,int)
236 * Creates and positions all views
240 protected void onLayout(boolean changed, int l, int t, int r, int b) {
241 super.onLayout(changed, l, t, r, b);
248 * Creates and positions all views for this Spinner.
250 * @param delta Change in the selected position. +1 moves selection is moving to the right,
251 * so views are scrolling to the left. -1 means selection is moving to the left.
254 void layout(int delta, boolean animate) {
255 int childrenLeft = mSpinnerPadding.left;
256 int childrenWidth = getRight() - getLeft() - mSpinnerPadding.left - mSpinnerPadding.right;
262 // Handle the empty set by removing all views
263 if (mItemCount == 0) {
268 if (mNextSelectedPosition >= 0) {
269 setSelectedPositionInt(mNextSelectedPosition);
274 // Clear out old views
275 removeAllViewsInLayout();
277 // Make selected view and position it
278 mFirstPosition = mSelectedPosition;
279 View sel = makeAndAddView(mSelectedPosition);
280 int width = sel.getMeasuredWidth();
281 int selectedOffset = childrenLeft;
282 switch (mGravity & Gravity.HORIZONTAL_GRAVITY_MASK) {
283 case Gravity.CENTER_HORIZONTAL:
284 selectedOffset = childrenLeft + (childrenWidth / 2) - (width / 2);
287 selectedOffset = childrenLeft + childrenWidth - width;
290 sel.offsetLeftAndRight(selectedOffset);
292 // Flush any cached views that did not get reused above
297 checkSelectionChanged();
299 mDataChanged = false;
301 setNextSelectedPositionInt(mSelectedPosition);
305 * Obtain a view, either by pulling an existing view from the recycler or
306 * by getting a new one from the adapter. If we are animating, make sure
307 * there is enough information in the view's layout parameters to animate
308 * from the old to new positions.
310 * @param position Position in the spinner for the view to obtain
311 * @return A view that has been added to the spinner
313 private View makeAndAddView(int position) {
318 child = mRecycler.get(position);
327 // Nothing found in the recycler -- ask the adapter for a view
328 child = mAdapter.getView(position, null, this);
337 * Helper for makeAndAddView to set the position of a view
338 * and fill out its layout paramters.
340 * @param child The view to position
342 private void setUpChild(View child) {
344 // Respect layout params that are already in the view. Otherwise
346 ViewGroup.LayoutParams lp = child.getLayoutParams();
348 lp = generateDefaultLayoutParams();
351 addViewInLayout(child, 0, lp);
353 child.setSelected(hasFocus());
354 if (mDisableChildrenWhenDisabled) {
355 child.setEnabled(isEnabled());
359 int childHeightSpec = ViewGroup.getChildMeasureSpec(mHeightMeasureSpec,
360 mSpinnerPadding.top + mSpinnerPadding.bottom, lp.height);
361 int childWidthSpec = ViewGroup.getChildMeasureSpec(mWidthMeasureSpec,
362 mSpinnerPadding.left + mSpinnerPadding.right, lp.width);
365 child.measure(childWidthSpec, childHeightSpec);
370 // Position vertically based on gravity setting
371 int childTop = mSpinnerPadding.top
372 + ((getMeasuredHeight() - mSpinnerPadding.bottom -
373 mSpinnerPadding.top - child.getMeasuredHeight()) / 2);
374 int childBottom = childTop + child.getMeasuredHeight();
376 int width = child.getMeasuredWidth();
378 childRight = childLeft + width;
380 child.layout(childLeft, childTop, childRight, childBottom);
384 public boolean performClick() {
385 boolean handled = super.performClick();
390 if (!mPopup.isShowing()) {
398 public void onClick(DialogInterface dialog, int which) {
404 * Sets the prompt to display when the dialog is shown.
405 * @param prompt the prompt to set
407 public void setPrompt(CharSequence prompt) {
408 mPopup.setPromptText(prompt);
412 * Sets the prompt to display when the dialog is shown.
413 * @param promptId the resource ID of the prompt to display when the dialog is shown
415 public void setPromptId(int promptId) {
416 setPrompt(getContext().getText(promptId));
420 * @return The prompt to display when the dialog is shown
422 public CharSequence getPrompt() {
423 return mPopup.getHintText();
426 int measureContentWidth(SpinnerAdapter adapter, Drawable background) {
427 if (adapter == null) {
432 View itemView = null;
434 final int widthMeasureSpec =
435 MeasureSpec.makeMeasureSpec(0, MeasureSpec.UNSPECIFIED);
436 final int heightMeasureSpec =
437 MeasureSpec.makeMeasureSpec(0, MeasureSpec.UNSPECIFIED);
439 // Make sure the number of items we'll measure is capped. If it's a huge data set
440 // with wildly varying sizes, oh well.
441 int start = Math.max(0, getSelectedItemPosition());
442 final int end = Math.min(adapter.getCount(), start + MAX_ITEMS_MEASURED);
443 final int count = end - start;
444 start = Math.max(0, start - (MAX_ITEMS_MEASURED - count));
445 for (int i = start; i < end; i++) {
446 final int positionType = adapter.getItemViewType(i);
447 if (positionType != itemType) {
448 itemType = positionType;
451 itemView = adapter.getView(i, itemView, this);
452 if (itemView.getLayoutParams() == null) {
453 itemView.setLayoutParams(new ViewGroup.LayoutParams(
454 ViewGroup.LayoutParams.WRAP_CONTENT,
455 ViewGroup.LayoutParams.WRAP_CONTENT));
457 itemView.measure(widthMeasureSpec, heightMeasureSpec);
458 width = Math.max(width, itemView.getMeasuredWidth());
461 // Add background padding to measured width
462 if (background != null) {
463 background.getPadding(mTempRect);
464 width += mTempRect.left + mTempRect.right;
471 * <p>Wrapper class for an Adapter. Transforms the embedded Adapter instance
472 * into a ListAdapter.</p>
474 private static class DropDownAdapter implements ListAdapter, SpinnerAdapter {
475 private SpinnerAdapter mAdapter;
476 private ListAdapter mListAdapter;
479 * <p>Creates a new ListAdapter wrapper for the specified adapter.</p>
481 * @param adapter the Adapter to transform into a ListAdapter
483 public DropDownAdapter(SpinnerAdapter adapter) {
484 this.mAdapter = adapter;
485 if (adapter instanceof ListAdapter) {
486 this.mListAdapter = (ListAdapter) adapter;
490 public int getCount() {
491 return mAdapter == null ? 0 : mAdapter.getCount();
494 public Object getItem(int position) {
495 return mAdapter == null ? null : mAdapter.getItem(position);
498 public long getItemId(int position) {
499 return mAdapter == null ? -1 : mAdapter.getItemId(position);
502 public View getView(int position, View convertView, ViewGroup parent) {
503 return getDropDownView(position, convertView, parent);
506 public View getDropDownView(int position, View convertView, ViewGroup parent) {
507 return mAdapter == null ? null :
508 mAdapter.getDropDownView(position, convertView, parent);
511 public boolean hasStableIds() {
512 return mAdapter != null && mAdapter.hasStableIds();
515 public void registerDataSetObserver(DataSetObserver observer) {
516 if (mAdapter != null) {
517 mAdapter.registerDataSetObserver(observer);
521 public void unregisterDataSetObserver(DataSetObserver observer) {
522 if (mAdapter != null) {
523 mAdapter.unregisterDataSetObserver(observer);
528 * If the wrapped SpinnerAdapter is also a ListAdapter, delegate this call.
529 * Otherwise, return true.
531 public boolean areAllItemsEnabled() {
532 final ListAdapter adapter = mListAdapter;
533 if (adapter != null) {
534 return adapter.areAllItemsEnabled();
541 * If the wrapped SpinnerAdapter is also a ListAdapter, delegate this call.
542 * Otherwise, return true.
544 public boolean isEnabled(int position) {
545 final ListAdapter adapter = mListAdapter;
546 if (adapter != null) {
547 return adapter.isEnabled(position);
553 public int getItemViewType(int position) {
557 public int getViewTypeCount() {
561 public boolean isEmpty() {
562 return getCount() == 0;
567 * Implements some sort of popup selection interface for selecting a spinner option.
568 * Allows for different spinner modes.
570 private interface SpinnerPopup {
571 public void setAdapter(ListAdapter adapter);
581 public void dismiss();
584 * @return true if the popup is showing, false otherwise.
586 public boolean isShowing();
589 * Set hint text to be displayed to the user. This should provide
590 * a description of the choice being made.
591 * @param hintText Hint text to set.
593 public void setPromptText(CharSequence hintText);
594 public CharSequence getHintText();
598 private class DialogPopup implements SpinnerPopup, DialogInterface.OnClickListener {
599 private AlertDialog mPopup;
600 private ListAdapter mListAdapter;
601 private CharSequence mPrompt;
603 public void dismiss() {
608 public boolean isShowing() {
609 return mPopup != null ? mPopup.isShowing() : false;
612 public void setAdapter(ListAdapter adapter) {
613 mListAdapter = adapter;
616 public void setPromptText(CharSequence hintText) {
620 public CharSequence getHintText() {
625 AlertDialog.Builder builder = new AlertDialog.Builder(getContext());
626 if (mPrompt != null) {
627 builder.setTitle(mPrompt);
629 mPopup = builder.setSingleChoiceItems(mListAdapter,
630 getSelectedItemPosition(), this).show();
633 public void onClick(DialogInterface dialog, int which) {
640 private class DropdownPopup extends IcsListPopupWindow implements SpinnerPopup {
641 private CharSequence mHintText;
642 private ListAdapter mAdapter;
644 public DropdownPopup(Context context, AttributeSet attrs, int defStyleRes) {
645 super(context, attrs, 0, defStyleRes);
647 setAnchorView(IcsSpinner.this);
649 setPromptPosition(POSITION_PROMPT_ABOVE);
650 setOnItemClickListener(new OnItemClickListener() {
651 @SuppressWarnings("rawtypes")
652 public void onItemClick(AdapterView parent, View v, int position, long id) {
653 IcsSpinner.this.setSelection(position);
660 public void setAdapter(ListAdapter adapter) {
661 super.setAdapter(adapter);
665 public CharSequence getHintText() {
669 public void setPromptText(CharSequence hintText) {
670 // Hint text is ignored for dropdowns, but maintain it here.
671 mHintText = hintText;
676 final int spinnerPaddingLeft = IcsSpinner.this.getPaddingLeft();
677 if (mDropDownWidth == WRAP_CONTENT) {
678 final int spinnerWidth = IcsSpinner.this.getWidth();
679 final int spinnerPaddingRight = IcsSpinner.this.getPaddingRight();
680 setContentWidth(Math.max(
681 measureContentWidth((SpinnerAdapter) mAdapter, getBackground()),
682 spinnerWidth - spinnerPaddingLeft - spinnerPaddingRight));
683 } else if (mDropDownWidth == MATCH_PARENT) {
684 final int spinnerWidth = IcsSpinner.this.getWidth();
685 final int spinnerPaddingRight = IcsSpinner.this.getPaddingRight();
686 setContentWidth(spinnerWidth - spinnerPaddingLeft - spinnerPaddingRight);
688 setContentWidth(mDropDownWidth);
690 final Drawable background = getBackground();
692 if (background != null) {
693 background.getPadding(mTempRect);
694 bgOffset = -mTempRect.left;
696 setHorizontalOffset(bgOffset + spinnerPaddingLeft);
697 setInputMethodMode(PopupWindow.INPUT_METHOD_NOT_NEEDED);
699 getListView().setChoiceMode(ListView.CHOICE_MODE_SINGLE);
700 setSelection(IcsSpinner.this.getSelectedItemPosition());