1/* 2 * Copyright (C) 2006 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 com.android.internal.R; 20 21import android.content.Context; 22import android.content.res.TypedArray; 23import android.database.DataSetObserver; 24import android.graphics.Rect; 25import android.os.Parcel; 26import android.os.Parcelable; 27import android.util.AttributeSet; 28import android.util.SparseArray; 29import android.view.View; 30import android.view.ViewGroup; 31 32/** 33 * An abstract base class for spinner widgets. SDK users will probably not 34 * need to use this class. 35 * 36 * @attr ref android.R.styleable#AbsSpinner_entries 37 */ 38public abstract class AbsSpinner extends AdapterView<SpinnerAdapter> { 39 SpinnerAdapter mAdapter; 40 41 int mHeightMeasureSpec; 42 int mWidthMeasureSpec; 43 44 int mSelectionLeftPadding = 0; 45 int mSelectionTopPadding = 0; 46 int mSelectionRightPadding = 0; 47 int mSelectionBottomPadding = 0; 48 final Rect mSpinnerPadding = new Rect(); 49 50 final RecycleBin mRecycler = new RecycleBin(); 51 private DataSetObserver mDataSetObserver; 52 53 /** Temporary frame to hold a child View's frame rectangle */ 54 private Rect mTouchFrame; 55 56 public AbsSpinner(Context context) { 57 super(context); 58 initAbsSpinner(); 59 } 60 61 public AbsSpinner(Context context, AttributeSet attrs) { 62 this(context, attrs, 0); 63 } 64 65 public AbsSpinner(Context context, AttributeSet attrs, int defStyleAttr) { 66 this(context, attrs, defStyleAttr, 0); 67 } 68 69 public AbsSpinner(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) { 70 super(context, attrs, defStyleAttr, defStyleRes); 71 initAbsSpinner(); 72 73 final TypedArray a = context.obtainStyledAttributes( 74 attrs, R.styleable.AbsSpinner, defStyleAttr, defStyleRes); 75 76 final CharSequence[] entries = a.getTextArray(R.styleable.AbsSpinner_entries); 77 if (entries != null) { 78 final ArrayAdapter<CharSequence> adapter = new ArrayAdapter<CharSequence>( 79 context, R.layout.simple_spinner_item, entries); 80 adapter.setDropDownViewResource(R.layout.simple_spinner_dropdown_item); 81 setAdapter(adapter); 82 } 83 84 a.recycle(); 85 } 86 87 /** 88 * Common code for different constructor flavors 89 */ 90 private void initAbsSpinner() { 91 setFocusable(true); 92 setWillNotDraw(false); 93 } 94 95 /** 96 * The Adapter is used to provide the data which backs this Spinner. 97 * It also provides methods to transform spinner items based on their position 98 * relative to the selected item. 99 * @param adapter The SpinnerAdapter to use for this Spinner 100 */ 101 @Override 102 public void setAdapter(SpinnerAdapter adapter) { 103 if (null != mAdapter) { 104 mAdapter.unregisterDataSetObserver(mDataSetObserver); 105 resetList(); 106 } 107 108 mAdapter = adapter; 109 110 mOldSelectedPosition = INVALID_POSITION; 111 mOldSelectedRowId = INVALID_ROW_ID; 112 113 if (mAdapter != null) { 114 mOldItemCount = mItemCount; 115 mItemCount = mAdapter.getCount(); 116 checkFocus(); 117 118 mDataSetObserver = new AdapterDataSetObserver(); 119 mAdapter.registerDataSetObserver(mDataSetObserver); 120 121 int position = mItemCount > 0 ? 0 : INVALID_POSITION; 122 123 setSelectedPositionInt(position); 124 setNextSelectedPositionInt(position); 125 126 if (mItemCount == 0) { 127 // Nothing selected 128 checkSelectionChanged(); 129 } 130 131 } else { 132 checkFocus(); 133 resetList(); 134 // Nothing selected 135 checkSelectionChanged(); 136 } 137 138 requestLayout(); 139 } 140 141 /** 142 * Clear out all children from the list 143 */ 144 void resetList() { 145 mDataChanged = false; 146 mNeedSync = false; 147 148 removeAllViewsInLayout(); 149 mOldSelectedPosition = INVALID_POSITION; 150 mOldSelectedRowId = INVALID_ROW_ID; 151 152 setSelectedPositionInt(INVALID_POSITION); 153 setNextSelectedPositionInt(INVALID_POSITION); 154 invalidate(); 155 } 156 157 /** 158 * @see android.view.View#measure(int, int) 159 * 160 * Figure out the dimensions of this Spinner. The width comes from 161 * the widthMeasureSpec as Spinnners can't have their width set to 162 * UNSPECIFIED. The height is based on the height of the selected item 163 * plus padding. 164 */ 165 @Override 166 protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { 167 int widthMode = MeasureSpec.getMode(widthMeasureSpec); 168 int widthSize; 169 int heightSize; 170 171 mSpinnerPadding.left = mPaddingLeft > mSelectionLeftPadding ? mPaddingLeft 172 : mSelectionLeftPadding; 173 mSpinnerPadding.top = mPaddingTop > mSelectionTopPadding ? mPaddingTop 174 : mSelectionTopPadding; 175 mSpinnerPadding.right = mPaddingRight > mSelectionRightPadding ? mPaddingRight 176 : mSelectionRightPadding; 177 mSpinnerPadding.bottom = mPaddingBottom > mSelectionBottomPadding ? mPaddingBottom 178 : mSelectionBottomPadding; 179 180 if (mDataChanged) { 181 handleDataChanged(); 182 } 183 184 int preferredHeight = 0; 185 int preferredWidth = 0; 186 boolean needsMeasuring = true; 187 188 int selectedPosition = getSelectedItemPosition(); 189 if (selectedPosition >= 0 && mAdapter != null && selectedPosition < mAdapter.getCount()) { 190 // Try looking in the recycler. (Maybe we were measured once already) 191 View view = mRecycler.get(selectedPosition); 192 if (view == null) { 193 // Make a new one 194 view = mAdapter.getView(selectedPosition, null, this); 195 196 if (view.getImportantForAccessibility() == IMPORTANT_FOR_ACCESSIBILITY_AUTO) { 197 view.setImportantForAccessibility(IMPORTANT_FOR_ACCESSIBILITY_YES); 198 } 199 } 200 201 if (view != null) { 202 // Put in recycler for re-measuring and/or layout 203 mRecycler.put(selectedPosition, view); 204 205 if (view.getLayoutParams() == null) { 206 mBlockLayoutRequests = true; 207 view.setLayoutParams(generateDefaultLayoutParams()); 208 mBlockLayoutRequests = false; 209 } 210 measureChild(view, widthMeasureSpec, heightMeasureSpec); 211 212 preferredHeight = getChildHeight(view) + mSpinnerPadding.top + mSpinnerPadding.bottom; 213 preferredWidth = getChildWidth(view) + mSpinnerPadding.left + mSpinnerPadding.right; 214 215 needsMeasuring = false; 216 } 217 } 218 219 if (needsMeasuring) { 220 // No views -- just use padding 221 preferredHeight = mSpinnerPadding.top + mSpinnerPadding.bottom; 222 if (widthMode == MeasureSpec.UNSPECIFIED) { 223 preferredWidth = mSpinnerPadding.left + mSpinnerPadding.right; 224 } 225 } 226 227 preferredHeight = Math.max(preferredHeight, getSuggestedMinimumHeight()); 228 preferredWidth = Math.max(preferredWidth, getSuggestedMinimumWidth()); 229 230 heightSize = resolveSizeAndState(preferredHeight, heightMeasureSpec, 0); 231 widthSize = resolveSizeAndState(preferredWidth, widthMeasureSpec, 0); 232 233 setMeasuredDimension(widthSize, heightSize); 234 mHeightMeasureSpec = heightMeasureSpec; 235 mWidthMeasureSpec = widthMeasureSpec; 236 } 237 238 int getChildHeight(View child) { 239 return child.getMeasuredHeight(); 240 } 241 242 int getChildWidth(View child) { 243 return child.getMeasuredWidth(); 244 } 245 246 @Override 247 protected ViewGroup.LayoutParams generateDefaultLayoutParams() { 248 return new ViewGroup.LayoutParams( 249 ViewGroup.LayoutParams.MATCH_PARENT, 250 ViewGroup.LayoutParams.WRAP_CONTENT); 251 } 252 253 void recycleAllViews() { 254 final int childCount = getChildCount(); 255 final AbsSpinner.RecycleBin recycleBin = mRecycler; 256 final int position = mFirstPosition; 257 258 // All views go in recycler 259 for (int i = 0; i < childCount; i++) { 260 View v = getChildAt(i); 261 int index = position + i; 262 recycleBin.put(index, v); 263 } 264 } 265 266 /** 267 * Jump directly to a specific item in the adapter data. 268 */ 269 public void setSelection(int position, boolean animate) { 270 // Animate only if requested position is already on screen somewhere 271 boolean shouldAnimate = animate && mFirstPosition <= position && 272 position <= mFirstPosition + getChildCount() - 1; 273 setSelectionInt(position, shouldAnimate); 274 } 275 276 @Override 277 public void setSelection(int position) { 278 setNextSelectedPositionInt(position); 279 requestLayout(); 280 invalidate(); 281 } 282 283 284 /** 285 * Makes the item at the supplied position selected. 286 * 287 * @param position Position to select 288 * @param animate Should the transition be animated 289 * 290 */ 291 void setSelectionInt(int position, boolean animate) { 292 if (position != mOldSelectedPosition) { 293 mBlockLayoutRequests = true; 294 int delta = position - mSelectedPosition; 295 setNextSelectedPositionInt(position); 296 layout(delta, animate); 297 mBlockLayoutRequests = false; 298 } 299 } 300 301 abstract void layout(int delta, boolean animate); 302 303 @Override 304 public View getSelectedView() { 305 if (mItemCount > 0 && mSelectedPosition >= 0) { 306 return getChildAt(mSelectedPosition - mFirstPosition); 307 } else { 308 return null; 309 } 310 } 311 312 /** 313 * Override to prevent spamming ourselves with layout requests 314 * as we place views 315 * 316 * @see android.view.View#requestLayout() 317 */ 318 @Override 319 public void requestLayout() { 320 if (!mBlockLayoutRequests) { 321 super.requestLayout(); 322 } 323 } 324 325 @Override 326 public SpinnerAdapter getAdapter() { 327 return mAdapter; 328 } 329 330 @Override 331 public int getCount() { 332 return mItemCount; 333 } 334 335 /** 336 * Maps a point to a position in the list. 337 * 338 * @param x X in local coordinate 339 * @param y Y in local coordinate 340 * @return The position of the item which contains the specified point, or 341 * {@link #INVALID_POSITION} if the point does not intersect an item. 342 */ 343 public int pointToPosition(int x, int y) { 344 Rect frame = mTouchFrame; 345 if (frame == null) { 346 mTouchFrame = new Rect(); 347 frame = mTouchFrame; 348 } 349 350 final int count = getChildCount(); 351 for (int i = count - 1; i >= 0; i--) { 352 View child = getChildAt(i); 353 if (child.getVisibility() == View.VISIBLE) { 354 child.getHitRect(frame); 355 if (frame.contains(x, y)) { 356 return mFirstPosition + i; 357 } 358 } 359 } 360 return INVALID_POSITION; 361 } 362 363 @Override 364 protected void dispatchRestoreInstanceState(SparseArray<Parcelable> container) { 365 super.dispatchRestoreInstanceState(container); 366 // Restores the selected position when Spinner gets restored, 367 // rather than wait until the next measure/layout pass to do it. 368 handleDataChanged(); 369 } 370 371 static class SavedState extends BaseSavedState { 372 long selectedId; 373 int position; 374 375 /** 376 * Constructor called from {@link AbsSpinner#onSaveInstanceState()} 377 */ 378 SavedState(Parcelable superState) { 379 super(superState); 380 } 381 382 /** 383 * Constructor called from {@link #CREATOR} 384 */ 385 SavedState(Parcel in) { 386 super(in); 387 selectedId = in.readLong(); 388 position = in.readInt(); 389 } 390 391 @Override 392 public void writeToParcel(Parcel out, int flags) { 393 super.writeToParcel(out, flags); 394 out.writeLong(selectedId); 395 out.writeInt(position); 396 } 397 398 @Override 399 public String toString() { 400 return "AbsSpinner.SavedState{" 401 + Integer.toHexString(System.identityHashCode(this)) 402 + " selectedId=" + selectedId 403 + " position=" + position + "}"; 404 } 405 406 public static final Parcelable.Creator<SavedState> CREATOR 407 = new Parcelable.Creator<SavedState>() { 408 public SavedState createFromParcel(Parcel in) { 409 return new SavedState(in); 410 } 411 412 public SavedState[] newArray(int size) { 413 return new SavedState[size]; 414 } 415 }; 416 } 417 418 @Override 419 public Parcelable onSaveInstanceState() { 420 Parcelable superState = super.onSaveInstanceState(); 421 SavedState ss = new SavedState(superState); 422 ss.selectedId = getSelectedItemId(); 423 if (ss.selectedId >= 0) { 424 ss.position = getSelectedItemPosition(); 425 } else { 426 ss.position = INVALID_POSITION; 427 } 428 return ss; 429 } 430 431 @Override 432 public void onRestoreInstanceState(Parcelable state) { 433 SavedState ss = (SavedState) state; 434 435 super.onRestoreInstanceState(ss.getSuperState()); 436 437 if (ss.selectedId >= 0) { 438 mDataChanged = true; 439 mNeedSync = true; 440 mSyncRowId = ss.selectedId; 441 mSyncPosition = ss.position; 442 mSyncMode = SYNC_SELECTED_POSITION; 443 requestLayout(); 444 } 445 } 446 447 class RecycleBin { 448 private final SparseArray<View> mScrapHeap = new SparseArray<View>(); 449 450 public void put(int position, View v) { 451 mScrapHeap.put(position, v); 452 } 453 454 View get(int position) { 455 // System.out.print("Looking for " + position); 456 View result = mScrapHeap.get(position); 457 if (result != null) { 458 // System.out.println(" HIT"); 459 mScrapHeap.delete(position); 460 } else { 461 // System.out.println(" MISS"); 462 } 463 return result; 464 } 465 466 void clear() { 467 final SparseArray<View> scrapHeap = mScrapHeap; 468 final int count = scrapHeap.size(); 469 for (int i = 0; i < count; i++) { 470 final View view = scrapHeap.valueAt(i); 471 if (view != null) { 472 removeDetachedView(view, true); 473 } 474 } 475 scrapHeap.clear(); 476 } 477 } 478 479 @Override 480 public CharSequence getAccessibilityClassName() { 481 return AbsSpinner.class.getName(); 482 } 483} 484