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