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