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());