create changelog entry
[debian/openrocket] / android-libraries / ActionBarSherlock / src / com / actionbarsherlock / internal / widget / IcsSpinner.java
1 /*
2  * Copyright (C) 2007 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 com.actionbarsherlock.internal.widget;
18
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;
39
40
41 /**
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
44  * this view.
45  *
46  * <p>See the <a href="{@docRoot}resources/tutorials/views/hello-spinner.html">Spinner
47  * tutorial</a>.</p>
48  *
49  * @attr ref android.R.styleable#Spinner_prompt
50  */
51 public class IcsSpinner extends IcsAbsSpinner implements OnClickListener {
52     //private static final String TAG = "Spinner";
53
54     // Only measure this many items to get a decent max width.
55     private static final int MAX_ITEMS_MEASURED = 15;
56
57     /**
58      * Use a dialog window for selecting spinner options.
59      */
60     //public static final int MODE_DIALOG = 0;
61
62     /**
63      * Use a dropdown anchored to the Spinner for selecting spinner options.
64      */
65     public static final int MODE_DROPDOWN = 1;
66
67     /**
68      * Use the theme-supplied value to select the dropdown mode.
69      */
70     //private static final int MODE_THEME = -1;
71
72     private SpinnerPopup mPopup;
73     private DropDownAdapter mTempAdapter;
74     int mDropDownWidth;
75
76     private int mGravity;
77     private boolean mDisableChildrenWhenDisabled;
78
79     private Rect mTempRect = new Rect();
80
81     public IcsSpinner(Context context, AttributeSet attrs) {
82         this(context, attrs, R.attr.actionDropDownStyle);
83     }
84
85     /**
86      * Construct a new spinner with the given context's theme, the supplied attribute set,
87      * and default style.
88      *
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.
96      */
97     public IcsSpinner(Context context, AttributeSet attrs, int defStyle) {
98         super(context, attrs, defStyle);
99
100         TypedArray a = context.obtainStyledAttributes(attrs,
101                 R.styleable.SherlockSpinner, defStyle, 0);
102
103
104         DropdownPopup popup = new DropdownPopup(context, attrs, defStyle);
105
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);
115         }
116
117         final int horizontalOffset = a.getDimensionPixelOffset(
118                 R.styleable.SherlockSpinner_android_dropDownHorizontalOffset, 0);
119         if (horizontalOffset != 0) {
120             popup.setHorizontalOffset(horizontalOffset);
121         }
122
123         mPopup = popup;
124
125         mGravity = a.getInt(R.styleable.SherlockSpinner_android_gravity, Gravity.CENTER);
126
127         mPopup.setPromptText(a.getString(R.styleable.SherlockSpinner_android_prompt));
128
129         mDisableChildrenWhenDisabled = true;
130
131         a.recycle();
132
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);
137             mTempAdapter = null;
138         }
139     }
140
141     @Override
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);
148             }
149         }
150     }
151
152     /**
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.
155      *
156      * @param gravity See {@link android.view.Gravity}
157      *
158      * @attr ref android.R.styleable#Spinner_gravity
159      */
160     public void setGravity(int gravity) {
161         if (mGravity != gravity) {
162             if ((gravity & Gravity.HORIZONTAL_GRAVITY_MASK) == 0) {
163                 gravity |= Gravity.LEFT;
164             }
165             mGravity = gravity;
166             requestLayout();
167         }
168     }
169
170     @Override
171     public void setAdapter(SpinnerAdapter adapter) {
172         super.setAdapter(adapter);
173
174         if (mPopup != null) {
175             mPopup.setAdapter(new DropDownAdapter(adapter));
176         } else {
177             mTempAdapter = new DropDownAdapter(adapter);
178         }
179     }
180
181     @Override
182     public int getBaseline() {
183         View child = null;
184
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();
191         }
192
193         if (child != null) {
194             final int childBaseline = child.getBaseline();
195             return childBaseline >= 0 ? child.getTop() + childBaseline : -1;
196         } else {
197             return -1;
198         }
199     }
200
201     @Override
202     protected void onDetachedFromWindow() {
203         super.onDetachedFromWindow();
204
205         if (mPopup != null && mPopup.isShowing()) {
206             mPopup.dismiss();
207         }
208     }
209
210     /**
211      * <p>A spinner does not support item click events. Calling this method
212      * will raise an exception.</p>
213      *
214      * @param l this listener will be ignored
215      */
216     @Override
217     public void setOnItemClickListener(OnItemClickListener l) {
218         throw new RuntimeException("setOnItemClickListener cannot be used with a spinner.");
219     }
220
221     @Override
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());
230         }
231     }
232
233     /**
234      * @see android.view.View#onLayout(boolean,int,int,int,int)
235      *
236      * Creates and positions all views
237      *
238      */
239     @Override
240     protected void onLayout(boolean changed, int l, int t, int r, int b) {
241         super.onLayout(changed, l, t, r, b);
242         mInLayout = true;
243         layout(0, false);
244         mInLayout = false;
245     }
246
247     /**
248      * Creates and positions all views for this Spinner.
249      *
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.
252      */
253     @Override
254     void layout(int delta, boolean animate) {
255         int childrenLeft = mSpinnerPadding.left;
256         int childrenWidth = getRight() - getLeft() - mSpinnerPadding.left - mSpinnerPadding.right;
257
258         if (mDataChanged) {
259             handleDataChanged();
260         }
261
262         // Handle the empty set by removing all views
263         if (mItemCount == 0) {
264             resetList();
265             return;
266         }
267
268         if (mNextSelectedPosition >= 0) {
269             setSelectedPositionInt(mNextSelectedPosition);
270         }
271
272         recycleAllViews();
273
274         // Clear out old views
275         removeAllViewsInLayout();
276
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);
285                 break;
286             case Gravity.RIGHT:
287                 selectedOffset = childrenLeft + childrenWidth - width;
288                 break;
289         }
290         sel.offsetLeftAndRight(selectedOffset);
291
292         // Flush any cached views that did not get reused above
293         mRecycler.clear();
294
295         invalidate();
296
297         checkSelectionChanged();
298
299         mDataChanged = false;
300         mNeedSync = false;
301         setNextSelectedPositionInt(mSelectedPosition);
302     }
303
304     /**
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.
309      *
310      * @param position Position in the spinner for the view to obtain
311      * @return A view that has been added to the spinner
312      */
313     private View makeAndAddView(int position) {
314
315         View child;
316
317         if (!mDataChanged) {
318             child = mRecycler.get(position);
319             if (child != null) {
320                 // Position the view
321                 setUpChild(child);
322
323                 return child;
324             }
325         }
326
327         // Nothing found in the recycler -- ask the adapter for a view
328         child = mAdapter.getView(position, null, this);
329
330         // Position the view
331         setUpChild(child);
332
333         return child;
334     }
335
336     /**
337      * Helper for makeAndAddView to set the position of a view
338      * and fill out its layout paramters.
339      *
340      * @param child The view to position
341      */
342     private void setUpChild(View child) {
343
344         // Respect layout params that are already in the view. Otherwise
345         // make some up...
346         ViewGroup.LayoutParams lp = child.getLayoutParams();
347         if (lp == null) {
348             lp = generateDefaultLayoutParams();
349         }
350
351         addViewInLayout(child, 0, lp);
352
353         child.setSelected(hasFocus());
354         if (mDisableChildrenWhenDisabled) {
355             child.setEnabled(isEnabled());
356         }
357
358         // Get measure specs
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);
363
364         // Measure child
365         child.measure(childWidthSpec, childHeightSpec);
366
367         int childLeft;
368         int childRight;
369
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();
375
376         int width = child.getMeasuredWidth();
377         childLeft = 0;
378         childRight = childLeft + width;
379
380         child.layout(childLeft, childTop, childRight, childBottom);
381     }
382
383     @Override
384     public boolean performClick() {
385         boolean handled = super.performClick();
386
387         if (!handled) {
388             handled = true;
389
390             if (!mPopup.isShowing()) {
391                 mPopup.show();
392             }
393         }
394
395         return handled;
396     }
397
398     public void onClick(DialogInterface dialog, int which) {
399         setSelection(which);
400         dialog.dismiss();
401     }
402
403     /**
404      * Sets the prompt to display when the dialog is shown.
405      * @param prompt the prompt to set
406      */
407     public void setPrompt(CharSequence prompt) {
408         mPopup.setPromptText(prompt);
409     }
410
411     /**
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
414      */
415     public void setPromptId(int promptId) {
416         setPrompt(getContext().getText(promptId));
417     }
418
419     /**
420      * @return The prompt to display when the dialog is shown
421      */
422     public CharSequence getPrompt() {
423         return mPopup.getHintText();
424     }
425
426     int measureContentWidth(SpinnerAdapter adapter, Drawable background) {
427         if (adapter == null) {
428             return 0;
429         }
430
431         int width = 0;
432         View itemView = null;
433         int itemType = 0;
434         final int widthMeasureSpec =
435             MeasureSpec.makeMeasureSpec(0, MeasureSpec.UNSPECIFIED);
436         final int heightMeasureSpec =
437             MeasureSpec.makeMeasureSpec(0, MeasureSpec.UNSPECIFIED);
438
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;
449                 itemView = null;
450             }
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));
456             }
457             itemView.measure(widthMeasureSpec, heightMeasureSpec);
458             width = Math.max(width, itemView.getMeasuredWidth());
459         }
460
461         // Add background padding to measured width
462         if (background != null) {
463             background.getPadding(mTempRect);
464             width += mTempRect.left + mTempRect.right;
465         }
466
467         return width;
468     }
469
470     /**
471      * <p>Wrapper class for an Adapter. Transforms the embedded Adapter instance
472      * into a ListAdapter.</p>
473      */
474     private static class DropDownAdapter implements ListAdapter, SpinnerAdapter {
475         private SpinnerAdapter mAdapter;
476         private ListAdapter mListAdapter;
477
478         /**
479          * <p>Creates a new ListAdapter wrapper for the specified adapter.</p>
480          *
481          * @param adapter the Adapter to transform into a ListAdapter
482          */
483         public DropDownAdapter(SpinnerAdapter adapter) {
484             this.mAdapter = adapter;
485             if (adapter instanceof ListAdapter) {
486                 this.mListAdapter = (ListAdapter) adapter;
487             }
488         }
489
490         public int getCount() {
491             return mAdapter == null ? 0 : mAdapter.getCount();
492         }
493
494         public Object getItem(int position) {
495             return mAdapter == null ? null : mAdapter.getItem(position);
496         }
497
498         public long getItemId(int position) {
499             return mAdapter == null ? -1 : mAdapter.getItemId(position);
500         }
501
502         public View getView(int position, View convertView, ViewGroup parent) {
503             return getDropDownView(position, convertView, parent);
504         }
505
506         public View getDropDownView(int position, View convertView, ViewGroup parent) {
507             return mAdapter == null ? null :
508                     mAdapter.getDropDownView(position, convertView, parent);
509         }
510
511         public boolean hasStableIds() {
512             return mAdapter != null && mAdapter.hasStableIds();
513         }
514
515         public void registerDataSetObserver(DataSetObserver observer) {
516             if (mAdapter != null) {
517                 mAdapter.registerDataSetObserver(observer);
518             }
519         }
520
521         public void unregisterDataSetObserver(DataSetObserver observer) {
522             if (mAdapter != null) {
523                 mAdapter.unregisterDataSetObserver(observer);
524             }
525         }
526
527         /**
528          * If the wrapped SpinnerAdapter is also a ListAdapter, delegate this call.
529          * Otherwise, return true.
530          */
531         public boolean areAllItemsEnabled() {
532             final ListAdapter adapter = mListAdapter;
533             if (adapter != null) {
534                 return adapter.areAllItemsEnabled();
535             } else {
536                 return true;
537             }
538         }
539
540         /**
541          * If the wrapped SpinnerAdapter is also a ListAdapter, delegate this call.
542          * Otherwise, return true.
543          */
544         public boolean isEnabled(int position) {
545             final ListAdapter adapter = mListAdapter;
546             if (adapter != null) {
547                 return adapter.isEnabled(position);
548             } else {
549                 return true;
550             }
551         }
552
553         public int getItemViewType(int position) {
554             return 0;
555         }
556
557         public int getViewTypeCount() {
558             return 1;
559         }
560
561         public boolean isEmpty() {
562             return getCount() == 0;
563         }
564     }
565
566     /**
567      * Implements some sort of popup selection interface for selecting a spinner option.
568      * Allows for different spinner modes.
569      */
570     private interface SpinnerPopup {
571         public void setAdapter(ListAdapter adapter);
572
573         /**
574          * Show the popup
575          */
576         public void show();
577
578         /**
579          * Dismiss the popup
580          */
581         public void dismiss();
582
583         /**
584          * @return true if the popup is showing, false otherwise.
585          */
586         public boolean isShowing();
587
588         /**
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.
592          */
593         public void setPromptText(CharSequence hintText);
594         public CharSequence getHintText();
595     }
596
597     /*
598     private class DialogPopup implements SpinnerPopup, DialogInterface.OnClickListener {
599         private AlertDialog mPopup;
600         private ListAdapter mListAdapter;
601         private CharSequence mPrompt;
602
603         public void dismiss() {
604             mPopup.dismiss();
605             mPopup = null;
606         }
607
608         public boolean isShowing() {
609             return mPopup != null ? mPopup.isShowing() : false;
610         }
611
612         public void setAdapter(ListAdapter adapter) {
613             mListAdapter = adapter;
614         }
615
616         public void setPromptText(CharSequence hintText) {
617             mPrompt = hintText;
618         }
619
620         public CharSequence getHintText() {
621             return mPrompt;
622         }
623
624         public void show() {
625             AlertDialog.Builder builder = new AlertDialog.Builder(getContext());
626             if (mPrompt != null) {
627                 builder.setTitle(mPrompt);
628             }
629             mPopup = builder.setSingleChoiceItems(mListAdapter,
630                     getSelectedItemPosition(), this).show();
631         }
632
633         public void onClick(DialogInterface dialog, int which) {
634             setSelection(which);
635             dismiss();
636         }
637     }
638     */
639
640     private class DropdownPopup extends IcsListPopupWindow implements SpinnerPopup {
641         private CharSequence mHintText;
642         private ListAdapter mAdapter;
643
644         public DropdownPopup(Context context, AttributeSet attrs, int defStyleRes) {
645             super(context, attrs, 0, defStyleRes);
646
647             setAnchorView(IcsSpinner.this);
648             setModal(true);
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);
654                     dismiss();
655                 }
656             });
657         }
658
659         @Override
660         public void setAdapter(ListAdapter adapter) {
661             super.setAdapter(adapter);
662             mAdapter = adapter;
663         }
664
665         public CharSequence getHintText() {
666             return mHintText;
667         }
668
669         public void setPromptText(CharSequence hintText) {
670             // Hint text is ignored for dropdowns, but maintain it here.
671             mHintText = hintText;
672         }
673
674         @Override
675         public void show() {
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);
687             } else {
688                 setContentWidth(mDropDownWidth);
689             }
690             final Drawable background = getBackground();
691             int bgOffset = 0;
692             if (background != null) {
693                 background.getPadding(mTempRect);
694                 bgOffset = -mTempRect.left;
695             }
696             setHorizontalOffset(bgOffset + spinnerPaddingLeft);
697             setInputMethodMode(PopupWindow.INPUT_METHOD_NOT_NEEDED);
698             super.show();
699             getListView().setChoiceMode(ListView.CHOICE_MODE_SINGLE);
700             setSelection(IcsSpinner.this.getSelectedItemPosition());
701         }
702     }
703 }