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.View; 28import android.view.ViewGroup; 29 30 31/** 32 * A view that displays one child at a time and lets the user pick among them. 33 * The items in the Spinner come from the {@link Adapter} associated with 34 * this view. 35 * 36 * <p>See the <a href="{@docRoot}resources/tutorials/views/hello-spinner.html">Spinner 37 * tutorial</a>.</p> 38 * 39 * @attr ref android.R.styleable#Spinner_prompt 40 */ 41@Widget 42public class Spinner extends AbsSpinner implements OnClickListener { 43 44 private CharSequence mPrompt; 45 private AlertDialog mPopup; 46 47 public Spinner(Context context) { 48 this(context, null); 49 } 50 51 public Spinner(Context context, AttributeSet attrs) { 52 this(context, attrs, com.android.internal.R.attr.spinnerStyle); 53 } 54 55 public Spinner(Context context, AttributeSet attrs, int defStyle) { 56 super(context, attrs, defStyle); 57 58 TypedArray a = context.obtainStyledAttributes(attrs, 59 com.android.internal.R.styleable.Spinner, defStyle, 0); 60 61 mPrompt = a.getString(com.android.internal.R.styleable.Spinner_prompt); 62 63 a.recycle(); 64 } 65 66 @Override 67 public int getBaseline() { 68 View child = null; 69 70 if (getChildCount() > 0) { 71 child = getChildAt(0); 72 } else if (mAdapter != null && mAdapter.getCount() > 0) { 73 child = makeAndAddView(0); 74 // TODO: We should probably put the child in the recycler 75 } 76 77 if (child != null) { 78 return child.getTop() + child.getBaseline(); 79 } else { 80 return -1; 81 } 82 } 83 84 @Override 85 protected void onDetachedFromWindow() { 86 super.onDetachedFromWindow(); 87 88 if (mPopup != null && mPopup.isShowing()) { 89 mPopup.dismiss(); 90 mPopup = null; 91 } 92 } 93 94 /** 95 * <p>A spinner does not support item click events. Calling this method 96 * will raise an exception.</p> 97 * 98 * @param l this listener will be ignored 99 */ 100 @Override 101 public void setOnItemClickListener(OnItemClickListener l) { 102 throw new RuntimeException("setOnItemClickListener cannot be used with a spinner."); 103 } 104 105 /** 106 * @see android.view.View#onLayout(boolean,int,int,int,int) 107 * 108 * Creates and positions all views 109 * 110 */ 111 @Override 112 protected void onLayout(boolean changed, int l, int t, int r, int b) { 113 super.onLayout(changed, l, t, r, b); 114 mInLayout = true; 115 layout(0, false); 116 mInLayout = false; 117 } 118 119 /** 120 * Creates and positions all views for this Spinner. 121 * 122 * @param delta Change in the selected position. +1 moves selection is moving to the right, 123 * so views are scrolling to the left. -1 means selection is moving to the left. 124 */ 125 @Override 126 void layout(int delta, boolean animate) { 127 int childrenLeft = mSpinnerPadding.left; 128 int childrenWidth = mRight - mLeft - mSpinnerPadding.left - mSpinnerPadding.right; 129 130 if (mDataChanged) { 131 handleDataChanged(); 132 } 133 134 // Handle the empty set by removing all views 135 if (mItemCount == 0) { 136 resetList(); 137 return; 138 } 139 140 if (mNextSelectedPosition >= 0) { 141 setSelectedPositionInt(mNextSelectedPosition); 142 } 143 144 recycleAllViews(); 145 146 // Clear out old views 147 removeAllViewsInLayout(); 148 149 // Make selected view and center it 150 mFirstPosition = mSelectedPosition; 151 View sel = makeAndAddView(mSelectedPosition); 152 int width = sel.getMeasuredWidth(); 153 int selectedOffset = childrenLeft + (childrenWidth / 2) - (width / 2); 154 sel.offsetLeftAndRight(selectedOffset); 155 156 // Flush any cached views that did not get reused above 157 mRecycler.clear(); 158 159 invalidate(); 160 161 checkSelectionChanged(); 162 163 mDataChanged = false; 164 mNeedSync = false; 165 setNextSelectedPositionInt(mSelectedPosition); 166 } 167 168 /** 169 * Obtain a view, either by pulling an existing view from the recycler or 170 * by getting a new one from the adapter. If we are animating, make sure 171 * there is enough information in the view's layout parameters to animate 172 * from the old to new positions. 173 * 174 * @param position Position in the spinner for the view to obtain 175 * @return A view that has been added to the spinner 176 */ 177 private View makeAndAddView(int position) { 178 179 View child; 180 181 if (!mDataChanged) { 182 child = mRecycler.get(position); 183 if (child != null) { 184 // Position the view 185 setUpChild(child); 186 187 return child; 188 } 189 } 190 191 // Nothing found in the recycler -- ask the adapter for a view 192 child = mAdapter.getView(position, null, this); 193 194 // Position the view 195 setUpChild(child); 196 197 return child; 198 } 199 200 201 202 /** 203 * Helper for makeAndAddView to set the position of a view 204 * and fill out its layout paramters. 205 * 206 * @param child The view to position 207 */ 208 private void setUpChild(View child) { 209 210 // Respect layout params that are already in the view. Otherwise 211 // make some up... 212 ViewGroup.LayoutParams lp = child.getLayoutParams(); 213 if (lp == null) { 214 lp = generateDefaultLayoutParams(); 215 } 216 217 addViewInLayout(child, 0, lp); 218 219 child.setSelected(hasFocus()); 220 221 // Get measure specs 222 int childHeightSpec = ViewGroup.getChildMeasureSpec(mHeightMeasureSpec, 223 mSpinnerPadding.top + mSpinnerPadding.bottom, lp.height); 224 int childWidthSpec = ViewGroup.getChildMeasureSpec(mWidthMeasureSpec, 225 mSpinnerPadding.left + mSpinnerPadding.right, lp.width); 226 227 // Measure child 228 child.measure(childWidthSpec, childHeightSpec); 229 230 int childLeft; 231 int childRight; 232 233 // Position vertically based on gravity setting 234 int childTop = mSpinnerPadding.top 235 + ((mMeasuredHeight - mSpinnerPadding.bottom - 236 mSpinnerPadding.top - child.getMeasuredHeight()) / 2); 237 int childBottom = childTop + child.getMeasuredHeight(); 238 239 int width = child.getMeasuredWidth(); 240 childLeft = 0; 241 childRight = childLeft + width; 242 243 child.layout(childLeft, childTop, childRight, childBottom); 244 } 245 246 @Override 247 public boolean performClick() { 248 boolean handled = super.performClick(); 249 250 if (!handled) { 251 handled = true; 252 Context context = getContext(); 253 254 final DropDownAdapter adapter = new DropDownAdapter(getAdapter()); 255 256 AlertDialog.Builder builder = new AlertDialog.Builder(context); 257 if (mPrompt != null) { 258 builder.setTitle(mPrompt); 259 } 260 mPopup = builder.setSingleChoiceItems(adapter, getSelectedItemPosition(), this).show(); 261 } 262 263 return handled; 264 } 265 266 public void onClick(DialogInterface dialog, int which) { 267 setSelection(which); 268 dialog.dismiss(); 269 mPopup = null; 270 } 271 272 /** 273 * Sets the prompt to display when the dialog is shown. 274 * @param prompt the prompt to set 275 */ 276 public void setPrompt(CharSequence prompt) { 277 mPrompt = prompt; 278 } 279 280 /** 281 * Sets the prompt to display when the dialog is shown. 282 * @param promptId the resource ID of the prompt to display when the dialog is shown 283 */ 284 public void setPromptId(int promptId) { 285 mPrompt = getContext().getText(promptId); 286 } 287 288 /** 289 * @return The prompt to display when the dialog is shown 290 */ 291 public CharSequence getPrompt() { 292 return mPrompt; 293 } 294 295 /** 296 * <p>Wrapper class for an Adapter. Transforms the embedded Adapter instance 297 * into a ListAdapter.</p> 298 */ 299 private static class DropDownAdapter implements ListAdapter, SpinnerAdapter { 300 private SpinnerAdapter mAdapter; 301 private ListAdapter mListAdapter; 302 303 /** 304 * <p>Creates a new ListAdapter wrapper for the specified adapter.</p> 305 * 306 * @param adapter the Adapter to transform into a ListAdapter 307 */ 308 public DropDownAdapter(SpinnerAdapter adapter) { 309 this.mAdapter = adapter; 310 if (adapter instanceof ListAdapter) { 311 this.mListAdapter = (ListAdapter) adapter; 312 } 313 } 314 315 public int getCount() { 316 return mAdapter == null ? 0 : mAdapter.getCount(); 317 } 318 319 public Object getItem(int position) { 320 return mAdapter == null ? null : mAdapter.getItem(position); 321 } 322 323 public long getItemId(int position) { 324 return mAdapter == null ? -1 : mAdapter.getItemId(position); 325 } 326 327 public View getView(int position, View convertView, ViewGroup parent) { 328 return getDropDownView(position, convertView, parent); 329 } 330 331 public View getDropDownView(int position, View convertView, ViewGroup parent) { 332 return mAdapter == null ? null : 333 mAdapter.getDropDownView(position, convertView, parent); 334 } 335 336 public boolean hasStableIds() { 337 return mAdapter != null && mAdapter.hasStableIds(); 338 } 339 340 public void registerDataSetObserver(DataSetObserver observer) { 341 if (mAdapter != null) { 342 mAdapter.registerDataSetObserver(observer); 343 } 344 } 345 346 public void unregisterDataSetObserver(DataSetObserver observer) { 347 if (mAdapter != null) { 348 mAdapter.unregisterDataSetObserver(observer); 349 } 350 } 351 352 /** 353 * If the wrapped SpinnerAdapter is also a ListAdapter, delegate this call. 354 * Otherwise, return true. 355 */ 356 public boolean areAllItemsEnabled() { 357 final ListAdapter adapter = mListAdapter; 358 if (adapter != null) { 359 return adapter.areAllItemsEnabled(); 360 } else { 361 return true; 362 } 363 } 364 365 /** 366 * If the wrapped SpinnerAdapter is also a ListAdapter, delegate this call. 367 * Otherwise, return true. 368 */ 369 public boolean isEnabled(int position) { 370 final ListAdapter adapter = mListAdapter; 371 if (adapter != null) { 372 return adapter.isEnabled(position); 373 } else { 374 return true; 375 } 376 } 377 378 public int getItemViewType(int position) { 379 return 0; 380 } 381 382 public int getViewTypeCount() { 383 return 1; 384 } 385 386 public boolean isEmpty() { 387 return getCount() == 0; 388 } 389 } 390} 391