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