Spinner.java revision 385a655b8e8bf85024e4f24f1d7f6c2d7d7e900d
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 17package android.widget; 18 19import android.annotation.Widget; 20import android.app.AlertDialog; 21import android.content.Context; 22import android.content.DialogInterface; 23import android.content.DialogInterface.OnClickListener; 24import android.content.res.TypedArray; 25import android.database.DataSetObserver; 26import android.util.AttributeSet; 27import android.view.LayoutInflater; 28import android.view.View; 29import android.view.ViewGroup; 30 31 32/** 33 * A view that displays one child at a time and lets the user pick among them. 34 * The items in the Spinner come from the {@link Adapter} associated with 35 * this view. 36 * 37 * <p>See the <a href="{@docRoot}resources/tutorials/views/hello-spinner.html">Spinner 38 * tutorial</a>.</p> 39 * 40 * @attr ref android.R.styleable#Spinner_prompt 41 */ 42@Widget 43public class Spinner extends AbsSpinner implements OnClickListener { 44 private static final String TAG = "Spinner"; 45 46 /** 47 * Use a dialog window for selecting spinner options. 48 */ 49 public static final int MODE_DIALOG = 0; 50 51 /** 52 * Use a dropdown anchored to the Spinner for selecting spinner options. 53 */ 54 public static final int MODE_DROPDOWN = 1; 55 56 private SpinnerPopup mPopup; 57 private DropDownAdapter mTempAdapter; 58 59 public Spinner(Context context) { 60 this(context, null); 61 } 62 63 public Spinner(Context context, AttributeSet attrs) { 64 this(context, attrs, com.android.internal.R.attr.spinnerStyle); 65 } 66 67 public Spinner(Context context, AttributeSet attrs, int defStyle) { 68 super(context, attrs, defStyle); 69 70 TypedArray a = context.obtainStyledAttributes(attrs, 71 com.android.internal.R.styleable.Spinner, defStyle, 0); 72 73 final int mode = a.getInt(com.android.internal.R.styleable.Spinner_spinnerMode, 74 MODE_DIALOG); 75 76 switch (mode) { 77 case MODE_DIALOG: { 78 mPopup = new DialogPopup(); 79 break; 80 } 81 82 case MODE_DROPDOWN: { 83 final int hintResource = a.getResourceId( 84 com.android.internal.R.styleable.Spinner_popupPromptView, 0); 85 86 DropdownPopup popup = new DropdownPopup(context, attrs, defStyle, hintResource); 87 88 popup.setBackgroundDrawable(a.getDrawable( 89 com.android.internal.R.styleable.Spinner_popupBackground)); 90 popup.setVerticalOffset(a.getDimensionPixelOffset( 91 com.android.internal.R.styleable.Spinner_dropDownVerticalOffset, 0)); 92 popup.setHorizontalOffset(a.getDimensionPixelOffset( 93 com.android.internal.R.styleable.Spinner_dropDownHorizontalOffset, 0)); 94 95 mPopup = popup; 96 break; 97 } 98 } 99 100 mPopup.setPromptText(a.getString(com.android.internal.R.styleable.Spinner_prompt)); 101 102 a.recycle(); 103 104 // Base constructor can call setAdapter before we initialize mPopup. 105 // Finish setting things up if this happened. 106 if (mTempAdapter != null) { 107 mPopup.setAdapter(mTempAdapter); 108 mTempAdapter = null; 109 } 110 } 111 112 @Override 113 public void setAdapter(SpinnerAdapter adapter) { 114 super.setAdapter(adapter); 115 116 if (mPopup != null) { 117 mPopup.setAdapter(new DropDownAdapter(adapter)); 118 } else { 119 mTempAdapter = new DropDownAdapter(adapter); 120 } 121 } 122 123 @Override 124 public int getBaseline() { 125 View child = null; 126 127 if (getChildCount() > 0) { 128 child = getChildAt(0); 129 } else if (mAdapter != null && mAdapter.getCount() > 0) { 130 child = makeAndAddView(0); 131 // TODO: We should probably put the child in the recycler 132 } 133 134 if (child != null) { 135 return child.getTop() + child.getBaseline(); 136 } else { 137 return -1; 138 } 139 } 140 141 @Override 142 protected void onDetachedFromWindow() { 143 super.onDetachedFromWindow(); 144 145 if (mPopup != null && mPopup.isShowing()) { 146 mPopup.dismiss(); 147 mPopup = null; 148 } 149 } 150 151 /** 152 * <p>A spinner does not support item click events. Calling this method 153 * will raise an exception.</p> 154 * 155 * @param l this listener will be ignored 156 */ 157 @Override 158 public void setOnItemClickListener(OnItemClickListener l) { 159 throw new RuntimeException("setOnItemClickListener cannot be used with a spinner."); 160 } 161 162 /** 163 * @see android.view.View#onLayout(boolean,int,int,int,int) 164 * 165 * Creates and positions all views 166 * 167 */ 168 @Override 169 protected void onLayout(boolean changed, int l, int t, int r, int b) { 170 super.onLayout(changed, l, t, r, b); 171 mInLayout = true; 172 layout(0, false); 173 mInLayout = false; 174 } 175 176 /** 177 * Creates and positions all views for this Spinner. 178 * 179 * @param delta Change in the selected position. +1 moves selection is moving to the right, 180 * so views are scrolling to the left. -1 means selection is moving to the left. 181 */ 182 @Override 183 void layout(int delta, boolean animate) { 184 int childrenLeft = mSpinnerPadding.left; 185 int childrenWidth = mRight - mLeft - mSpinnerPadding.left - mSpinnerPadding.right; 186 187 if (mDataChanged) { 188 handleDataChanged(); 189 } 190 191 // Handle the empty set by removing all views 192 if (mItemCount == 0) { 193 resetList(); 194 return; 195 } 196 197 if (mNextSelectedPosition >= 0) { 198 setSelectedPositionInt(mNextSelectedPosition); 199 } 200 201 recycleAllViews(); 202 203 // Clear out old views 204 removeAllViewsInLayout(); 205 206 // Make selected view and center it 207 mFirstPosition = mSelectedPosition; 208 View sel = makeAndAddView(mSelectedPosition); 209 int width = sel.getMeasuredWidth(); 210 int selectedOffset = childrenLeft + (childrenWidth / 2) - (width / 2); 211 sel.offsetLeftAndRight(selectedOffset); 212 213 // Flush any cached views that did not get reused above 214 mRecycler.clear(); 215 216 invalidate(); 217 218 checkSelectionChanged(); 219 220 mDataChanged = false; 221 mNeedSync = false; 222 setNextSelectedPositionInt(mSelectedPosition); 223 } 224 225 /** 226 * Obtain a view, either by pulling an existing view from the recycler or 227 * by getting a new one from the adapter. If we are animating, make sure 228 * there is enough information in the view's layout parameters to animate 229 * from the old to new positions. 230 * 231 * @param position Position in the spinner for the view to obtain 232 * @return A view that has been added to the spinner 233 */ 234 private View makeAndAddView(int position) { 235 236 View child; 237 238 if (!mDataChanged) { 239 child = mRecycler.get(position); 240 if (child != null) { 241 // Position the view 242 setUpChild(child); 243 244 return child; 245 } 246 } 247 248 // Nothing found in the recycler -- ask the adapter for a view 249 child = mAdapter.getView(position, null, this); 250 251 // Position the view 252 setUpChild(child); 253 254 return child; 255 } 256 257 /** 258 * Helper for makeAndAddView to set the position of a view 259 * and fill out its layout paramters. 260 * 261 * @param child The view to position 262 */ 263 private void setUpChild(View child) { 264 265 // Respect layout params that are already in the view. Otherwise 266 // make some up... 267 ViewGroup.LayoutParams lp = child.getLayoutParams(); 268 if (lp == null) { 269 lp = generateDefaultLayoutParams(); 270 } 271 272 addViewInLayout(child, 0, lp); 273 274 child.setSelected(hasFocus()); 275 276 // Get measure specs 277 int childHeightSpec = ViewGroup.getChildMeasureSpec(mHeightMeasureSpec, 278 mSpinnerPadding.top + mSpinnerPadding.bottom, lp.height); 279 int childWidthSpec = ViewGroup.getChildMeasureSpec(mWidthMeasureSpec, 280 mSpinnerPadding.left + mSpinnerPadding.right, lp.width); 281 282 // Measure child 283 child.measure(childWidthSpec, childHeightSpec); 284 285 int childLeft; 286 int childRight; 287 288 // Position vertically based on gravity setting 289 int childTop = mSpinnerPadding.top 290 + ((mMeasuredHeight - mSpinnerPadding.bottom - 291 mSpinnerPadding.top - child.getMeasuredHeight()) / 2); 292 int childBottom = childTop + child.getMeasuredHeight(); 293 294 int width = child.getMeasuredWidth(); 295 childLeft = 0; 296 childRight = childLeft + width; 297 298 child.layout(childLeft, childTop, childRight, childBottom); 299 } 300 301 @Override 302 public boolean performClick() { 303 boolean handled = super.performClick(); 304 305 if (!handled) { 306 handled = true; 307 308 if (!mPopup.isShowing()) { 309 mPopup.show(); 310 } 311 } 312 313 return handled; 314 } 315 316 public void onClick(DialogInterface dialog, int which) { 317 setSelection(which); 318 dialog.dismiss(); 319 mPopup = null; 320 } 321 322 /** 323 * Sets the prompt to display when the dialog is shown. 324 * @param prompt the prompt to set 325 */ 326 public void setPrompt(CharSequence prompt) { 327 mPopup.setPromptText(prompt); 328 } 329 330 /** 331 * Sets the prompt to display when the dialog is shown. 332 * @param promptId the resource ID of the prompt to display when the dialog is shown 333 */ 334 public void setPromptId(int promptId) { 335 setPrompt(getContext().getText(promptId)); 336 } 337 338 /** 339 * @return The prompt to display when the dialog is shown 340 */ 341 public CharSequence getPrompt() { 342 return mPopup.getHintText(); 343 } 344 345 /** 346 * <p>Wrapper class for an Adapter. Transforms the embedded Adapter instance 347 * into a ListAdapter.</p> 348 */ 349 private static class DropDownAdapter implements ListAdapter, SpinnerAdapter { 350 private SpinnerAdapter mAdapter; 351 private ListAdapter mListAdapter; 352 353 /** 354 * <p>Creates a new ListAdapter wrapper for the specified adapter.</p> 355 * 356 * @param adapter the Adapter to transform into a ListAdapter 357 */ 358 public DropDownAdapter(SpinnerAdapter adapter) { 359 this.mAdapter = adapter; 360 if (adapter instanceof ListAdapter) { 361 this.mListAdapter = (ListAdapter) adapter; 362 } 363 } 364 365 public int getCount() { 366 return mAdapter == null ? 0 : mAdapter.getCount(); 367 } 368 369 public Object getItem(int position) { 370 return mAdapter == null ? null : mAdapter.getItem(position); 371 } 372 373 public long getItemId(int position) { 374 return mAdapter == null ? -1 : mAdapter.getItemId(position); 375 } 376 377 public View getView(int position, View convertView, ViewGroup parent) { 378 return getDropDownView(position, convertView, parent); 379 } 380 381 public View getDropDownView(int position, View convertView, ViewGroup parent) { 382 return mAdapter == null ? null : 383 mAdapter.getDropDownView(position, convertView, parent); 384 } 385 386 public boolean hasStableIds() { 387 return mAdapter != null && mAdapter.hasStableIds(); 388 } 389 390 public void registerDataSetObserver(DataSetObserver observer) { 391 if (mAdapter != null) { 392 mAdapter.registerDataSetObserver(observer); 393 } 394 } 395 396 public void unregisterDataSetObserver(DataSetObserver observer) { 397 if (mAdapter != null) { 398 mAdapter.unregisterDataSetObserver(observer); 399 } 400 } 401 402 /** 403 * If the wrapped SpinnerAdapter is also a ListAdapter, delegate this call. 404 * Otherwise, return true. 405 */ 406 public boolean areAllItemsEnabled() { 407 final ListAdapter adapter = mListAdapter; 408 if (adapter != null) { 409 return adapter.areAllItemsEnabled(); 410 } else { 411 return true; 412 } 413 } 414 415 /** 416 * If the wrapped SpinnerAdapter is also a ListAdapter, delegate this call. 417 * Otherwise, return true. 418 */ 419 public boolean isEnabled(int position) { 420 final ListAdapter adapter = mListAdapter; 421 if (adapter != null) { 422 return adapter.isEnabled(position); 423 } else { 424 return true; 425 } 426 } 427 428 public int getItemViewType(int position) { 429 return 0; 430 } 431 432 public int getViewTypeCount() { 433 return 1; 434 } 435 436 public boolean isEmpty() { 437 return getCount() == 0; 438 } 439 } 440 441 /** 442 * Implements some sort of popup selection interface for selecting a spinner option. 443 * Allows for different spinner modes. 444 */ 445 private interface SpinnerPopup { 446 public void setAdapter(ListAdapter adapter); 447 448 /** 449 * Show the popup 450 */ 451 public void show(); 452 453 /** 454 * Dismiss the popup 455 */ 456 public void dismiss(); 457 458 /** 459 * @return true if the popup is showing, false otherwise. 460 */ 461 public boolean isShowing(); 462 463 /** 464 * Set hint text to be displayed to the user. This should provide 465 * a description of the choice being made. 466 * @param hintText Hint text to set. 467 */ 468 public void setPromptText(CharSequence hintText); 469 public CharSequence getHintText(); 470 } 471 472 private class DialogPopup implements SpinnerPopup, DialogInterface.OnClickListener { 473 private AlertDialog mPopup; 474 private ListAdapter mListAdapter; 475 private CharSequence mPrompt; 476 477 public void dismiss() { 478 mPopup.dismiss(); 479 mPopup = null; 480 } 481 482 public boolean isShowing() { 483 return mPopup != null ? mPopup.isShowing() : false; 484 } 485 486 public void setAdapter(ListAdapter adapter) { 487 mListAdapter = adapter; 488 } 489 490 public void setPromptText(CharSequence hintText) { 491 mPrompt = hintText; 492 } 493 494 public CharSequence getHintText() { 495 return mPrompt; 496 } 497 498 public void show() { 499 AlertDialog.Builder builder = new AlertDialog.Builder(getContext()); 500 if (mPrompt != null) { 501 builder.setTitle(mPrompt); 502 } 503 mPopup = builder.setSingleChoiceItems(mListAdapter, 504 getSelectedItemPosition(), this).show(); 505 } 506 507 public void onClick(DialogInterface dialog, int which) { 508 setSelection(which); 509 dismiss(); 510 } 511 } 512 513 private class DropdownPopup extends ListPopupWindow implements SpinnerPopup { 514 private CharSequence mHintText; 515 private TextView mHintView; 516 private int mHintResource; 517 518 public DropdownPopup(Context context, AttributeSet attrs, 519 int defStyleRes, int hintResource) { 520 super(context, attrs, 0, defStyleRes); 521 522 mHintResource = hintResource; 523 524 setAnchorView(Spinner.this); 525 setModal(true); 526 setPromptPosition(POSITION_PROMPT_BELOW); 527 setOnItemClickListener(new OnItemClickListener() { 528 public void onItemClick(AdapterView parent, View v, int position, long id) { 529 Spinner.this.setSelection(position); 530 dismiss(); 531 } 532 }); 533 } 534 535 public CharSequence getHintText() { 536 return mHintText; 537 } 538 539 public void setPromptText(CharSequence hintText) { 540 mHintText = hintText; 541 if (mHintView != null) { 542 mHintView.setText(hintText); 543 } 544 } 545 546 public void show() { 547 if (mHintView == null) { 548 final TextView textView = (TextView) LayoutInflater.from(getContext()).inflate( 549 mHintResource, null).findViewById(com.android.internal.R.id.text1); 550 textView.setText(mHintText); 551 setPromptView(textView); 552 mHintView = textView; 553 } 554 super.show(); 555 getListView().setChoiceMode(ListView.CHOICE_MODE_SINGLE); 556 setSelection(Spinner.this.getSelectedItemPosition()); 557 } 558 } 559} 560