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