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