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                 if (view.getLayoutParams() == null) {
    205                     mBlockLayoutRequests = true;
    206                     view.setLayoutParams(generateDefaultLayoutParams());
    207                     mBlockLayoutRequests = false;
    208                 }
    209                 measureChild(view, widthMeasureSpec, heightMeasureSpec);
    210 
    211                 preferredHeight = getChildHeight(view) + mSpinnerPadding.top + mSpinnerPadding.bottom;
    212                 preferredWidth = getChildWidth(view) + mSpinnerPadding.left + mSpinnerPadding.right;
    213 
    214                 needsMeasuring = false;
    215             }
    216         }
    217 
    218         if (needsMeasuring) {
    219             // No views -- just use padding
    220             preferredHeight = mSpinnerPadding.top + mSpinnerPadding.bottom;
    221             if (widthMode == MeasureSpec.UNSPECIFIED) {
    222                 preferredWidth = mSpinnerPadding.left + mSpinnerPadding.right;
    223             }
    224         }
    225 
    226         preferredHeight = Math.max(preferredHeight, getSuggestedMinimumHeight());
    227         preferredWidth = Math.max(preferredWidth, getSuggestedMinimumWidth());
    228 
    229         heightSize = resolveSizeAndState(preferredHeight, heightMeasureSpec, 0);
    230         widthSize = resolveSizeAndState(preferredWidth, widthMeasureSpec, 0);
    231 
    232         setMeasuredDimension(widthSize, heightSize);
    233         mHeightMeasureSpec = heightMeasureSpec;
    234         mWidthMeasureSpec = widthMeasureSpec;
    235     }
    236 
    237     int getChildHeight(View child) {
    238         return child.getMeasuredHeight();
    239     }
    240 
    241     int getChildWidth(View child) {
    242         return child.getMeasuredWidth();
    243     }
    244 
    245     @Override
    246     protected ViewGroup.LayoutParams generateDefaultLayoutParams() {
    247         return new ViewGroup.LayoutParams(
    248                 ViewGroup.LayoutParams.MATCH_PARENT,
    249                 ViewGroup.LayoutParams.WRAP_CONTENT);
    250     }
    251 
    252     void recycleAllViews() {
    253         final int childCount = getChildCount();
    254         final AbsSpinner.RecycleBin recycleBin = mRecycler;
    255         final int position = mFirstPosition;
    256 
    257         // All views go in recycler
    258         for (int i = 0; i < childCount; i++) {
    259             View v = getChildAt(i);
    260             int index = position + i;
    261             recycleBin.put(index, v);
    262         }
    263     }
    264 
    265     /**
    266      * Jump directly to a specific item in the adapter data.
    267      */
    268     public void setSelection(int position, boolean animate) {
    269         // Animate only if requested position is already on screen somewhere
    270         boolean shouldAnimate = animate && mFirstPosition <= position &&
    271                 position <= mFirstPosition + getChildCount() - 1;
    272         setSelectionInt(position, shouldAnimate);
    273     }
    274 
    275     @Override
    276     public void setSelection(int position) {
    277         setNextSelectedPositionInt(position);
    278         requestLayout();
    279         invalidate();
    280     }
    281 
    282 
    283     /**
    284      * Makes the item at the supplied position selected.
    285      *
    286      * @param position Position to select
    287      * @param animate Should the transition be animated
    288      *
    289      */
    290     void setSelectionInt(int position, boolean animate) {
    291         if (position != mOldSelectedPosition) {
    292             mBlockLayoutRequests = true;
    293             int delta  = position - mSelectedPosition;
    294             setNextSelectedPositionInt(position);
    295             layout(delta, animate);
    296             mBlockLayoutRequests = false;
    297         }
    298     }
    299 
    300     abstract void layout(int delta, boolean animate);
    301 
    302     @Override
    303     public View getSelectedView() {
    304         if (mItemCount > 0 && mSelectedPosition >= 0) {
    305             return getChildAt(mSelectedPosition - mFirstPosition);
    306         } else {
    307             return null;
    308         }
    309     }
    310 
    311     /**
    312      * Override to prevent spamming ourselves with layout requests
    313      * as we place views
    314      *
    315      * @see android.view.View#requestLayout()
    316      */
    317     @Override
    318     public void requestLayout() {
    319         if (!mBlockLayoutRequests) {
    320             super.requestLayout();
    321         }
    322     }
    323 
    324     @Override
    325     public SpinnerAdapter getAdapter() {
    326         return mAdapter;
    327     }
    328 
    329     @Override
    330     public int getCount() {
    331         return mItemCount;
    332     }
    333 
    334     /**
    335      * Maps a point to a position in the list.
    336      *
    337      * @param x X in local coordinate
    338      * @param y Y in local coordinate
    339      * @return The position of the item which contains the specified point, or
    340      *         {@link #INVALID_POSITION} if the point does not intersect an item.
    341      */
    342     public int pointToPosition(int x, int y) {
    343         Rect frame = mTouchFrame;
    344         if (frame == null) {
    345             mTouchFrame = new Rect();
    346             frame = mTouchFrame;
    347         }
    348 
    349         final int count = getChildCount();
    350         for (int i = count - 1; i >= 0; i--) {
    351             View child = getChildAt(i);
    352             if (child.getVisibility() == View.VISIBLE) {
    353                 child.getHitRect(frame);
    354                 if (frame.contains(x, y)) {
    355                     return mFirstPosition + i;
    356                 }
    357             }
    358         }
    359         return INVALID_POSITION;
    360     }
    361 
    362     static class SavedState extends BaseSavedState {
    363         long selectedId;
    364         int position;
    365 
    366         /**
    367          * Constructor called from {@link AbsSpinner#onSaveInstanceState()}
    368          */
    369         SavedState(Parcelable superState) {
    370             super(superState);
    371         }
    372 
    373         /**
    374          * Constructor called from {@link #CREATOR}
    375          */
    376         SavedState(Parcel in) {
    377             super(in);
    378             selectedId = in.readLong();
    379             position = in.readInt();
    380         }
    381 
    382         @Override
    383         public void writeToParcel(Parcel out, int flags) {
    384             super.writeToParcel(out, flags);
    385             out.writeLong(selectedId);
    386             out.writeInt(position);
    387         }
    388 
    389         @Override
    390         public String toString() {
    391             return "AbsSpinner.SavedState{"
    392                     + Integer.toHexString(System.identityHashCode(this))
    393                     + " selectedId=" + selectedId
    394                     + " position=" + position + "}";
    395         }
    396 
    397         public static final Parcelable.Creator<SavedState> CREATOR
    398                 = new Parcelable.Creator<SavedState>() {
    399             public SavedState createFromParcel(Parcel in) {
    400                 return new SavedState(in);
    401             }
    402 
    403             public SavedState[] newArray(int size) {
    404                 return new SavedState[size];
    405             }
    406         };
    407     }
    408 
    409     @Override
    410     public Parcelable onSaveInstanceState() {
    411         Parcelable superState = super.onSaveInstanceState();
    412         SavedState ss = new SavedState(superState);
    413         ss.selectedId = getSelectedItemId();
    414         if (ss.selectedId >= 0) {
    415             ss.position = getSelectedItemPosition();
    416         } else {
    417             ss.position = INVALID_POSITION;
    418         }
    419         return ss;
    420     }
    421 
    422     @Override
    423     public void onRestoreInstanceState(Parcelable state) {
    424         SavedState ss = (SavedState) state;
    425 
    426         super.onRestoreInstanceState(ss.getSuperState());
    427 
    428         if (ss.selectedId >= 0) {
    429             mDataChanged = true;
    430             mNeedSync = true;
    431             mSyncRowId = ss.selectedId;
    432             mSyncPosition = ss.position;
    433             mSyncMode = SYNC_SELECTED_POSITION;
    434             requestLayout();
    435         }
    436     }
    437 
    438     class RecycleBin {
    439         private final SparseArray<View> mScrapHeap = new SparseArray<View>();
    440 
    441         public void put(int position, View v) {
    442             mScrapHeap.put(position, v);
    443         }
    444 
    445         View get(int position) {
    446             // System.out.print("Looking for " + position);
    447             View result = mScrapHeap.get(position);
    448             if (result != null) {
    449                 // System.out.println(" HIT");
    450                 mScrapHeap.delete(position);
    451             } else {
    452                 // System.out.println(" MISS");
    453             }
    454             return result;
    455         }
    456 
    457         void clear() {
    458             final SparseArray<View> scrapHeap = mScrapHeap;
    459             final int count = scrapHeap.size();
    460             for (int i = 0; i < count; i++) {
    461                 final View view = scrapHeap.valueAt(i);
    462                 if (view != null) {
    463                     removeDetachedView(view, true);
    464                 }
    465             }
    466             scrapHeap.clear();
    467         }
    468     }
    469 
    470     @Override
    471     public void onInitializeAccessibilityEvent(AccessibilityEvent event) {
    472         super.onInitializeAccessibilityEvent(event);
    473         event.setClassName(AbsSpinner.class.getName());
    474     }
    475 
    476     @Override
    477     public void onInitializeAccessibilityNodeInfo(AccessibilityNodeInfo info) {
    478         super.onInitializeAccessibilityNodeInfo(info);
    479         info.setClassName(AbsSpinner.class.getName());
    480     }
    481 }
    482