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