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