038d1e0318a3af398b58271aa58974402fd95fb7
[pub/Android/ownCloud.git] / 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 }