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