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