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