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