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