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