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