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