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