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                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