Home | History | Annotate | Download | only in widget
      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 
     58     public AbsSpinner(Context context) {
     59         super(context);
     60         initAbsSpinner();
     61     }
     62 
     63     public AbsSpinner(Context context, AttributeSet attrs) {
     64         this(context, attrs, 0);
     65     }
     66 
     67     public AbsSpinner(Context context, AttributeSet attrs, int defStyleAttr) {
     68         this(context, attrs, defStyleAttr, 0);
     69     }
     70 
     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      */
     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
    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      */
    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
    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 
    241     int getChildHeight(View child) {
    242         return child.getMeasuredHeight();
    243     }
    244 
    245     int getChildWidth(View child) {
    246         return child.getMeasuredWidth();
    247     }
    248 
    249     @Override
    250     protected ViewGroup.LayoutParams generateDefaultLayoutParams() {
    251         return new ViewGroup.LayoutParams(
    252                 ViewGroup.LayoutParams.MATCH_PARENT,
    253                 ViewGroup.LayoutParams.WRAP_CONTENT);
    254     }
    255 
    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      */
    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
    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      */
    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 
    304     abstract void layout(int delta, boolean animate);
    305 
    306     @Override
    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
    322     public void requestLayout() {
    323         if (!mBlockLayoutRequests) {
    324             super.requestLayout();
    325         }
    326     }
    327 
    328     @Override
    329     public SpinnerAdapter getAdapter() {
    330         return mAdapter;
    331     }
    332 
    333     @Override
    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      */
    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          */
    373         SavedState(Parcelable superState) {
    374             super(superState);
    375         }
    376 
    377         /**
    378          * Constructor called from {@link #CREATOR}
    379          */
    380         SavedState(Parcel in) {
    381             super(in);
    382             selectedId = in.readLong();
    383             position = in.readInt();
    384         }
    385 
    386         @Override
    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
    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
    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
    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 
    445         public void put(int position, View v) {
    446             mScrapHeap.put(position, v);
    447         }
    448 
    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 
    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
    475     public void onInitializeAccessibilityEvent(AccessibilityEvent event) {
    476         super.onInitializeAccessibilityEvent(event);
    477         event.setClassName(AbsSpinner.class.getName());
    478     }
    479 
    480     @Override
    481     public void onInitializeAccessibilityNodeInfo(AccessibilityNodeInfo info) {
    482         super.onInitializeAccessibilityNodeInfo(info);
    483         info.setClassName(AbsSpinner.class.getName());
    484     }
    485 }
    486