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