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