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