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