/* * Copyright (C) 2006 The Android Open Source Project * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package com.aviary.android.feather.widget; import java.util.ArrayList; import java.util.Collections; import java.util.LinkedList; import java.util.List; import java.util.Queue; import android.content.Context; import android.database.DataSetObserver; import android.graphics.Rect; import android.os.Parcel; import android.os.Parcelable; import android.util.AttributeSet; import android.util.SparseArray; import android.view.View; import android.view.ViewGroup; import android.widget.Adapter; // TODO: Auto-generated Javadoc /** * An abstract base class for spinner widgets. SDK users will probably not need to use this class. * * @attr ref android.R.styleable#AbsSpinner_entries */ public abstract class AbsSpinner extends AdapterView<Adapter> { /** The m adapter. */ Adapter mAdapter; /** The m height measure spec. */ int mHeightMeasureSpec; /** The m width measure spec. */ int mWidthMeasureSpec; /** The m block layout requests. */ boolean mBlockLayoutRequests; /** The m selection left padding. */ int mSelectionLeftPadding = 0; /** The m selection top padding. */ int mSelectionTopPadding = 0; /** The m selection right padding. */ int mSelectionRightPadding = 0; /** The m selection bottom padding. */ int mSelectionBottomPadding = 0; /** The m spinner padding. */ final Rect mSpinnerPadding = new Rect(); /** The m padding left. */ int mPaddingLeft; /** The m padding right. */ int mPaddingRight; /** The m padding top. */ int mPaddingTop; /** The m padding bottom. */ int mPaddingBottom; /** The m recycler. */ protected final List<Queue<View>> mRecycleBin; /** The m data set observer. */ private DataSetObserver mDataSetObserver; /** Temporary frame to hold a child View's frame rectangle. */ private Rect mTouchFrame; /** * Instantiates a new abs spinner. * * @param context * the context */ public AbsSpinner( Context context ) { super( context ); mRecycleBin = Collections.synchronizedList( new ArrayList<Queue<View>>() ); initAbsSpinner(); } /** * Instantiates a new abs spinner. * * @param context * the context * @param attrs * the attrs */ public AbsSpinner( Context context, AttributeSet attrs ) { this( context, attrs, 0 ); } /** * Instantiates a new abs spinner. * * @param context * the context * @param attrs * the attrs * @param defStyle * the def style */ public AbsSpinner( Context context, AttributeSet attrs, int defStyle ) { super( context, attrs, defStyle ); mRecycleBin = Collections.synchronizedList( new ArrayList<Queue<View>>() ); initAbsSpinner(); /* * * TypedArray a = context.obtainStyledAttributes(attrs, com.android.internal.R.styleable.AbsSpinner, defStyle, 0); * * CharSequence[] entries = a.getTextArray(R.styleable.AbsSpinner_entries); if (entries != null) { ArrayAdapter<CharSequence> * adapter = new ArrayAdapter<CharSequence>(context, R.layout.simple_spinner_item, entries); * adapter.setDropDownViewResource(R.layout.simple_spinner_dropdown_item); setAdapter(adapter); } a.recycle(); */ } /* * (non-Javadoc) * * @see android.view.ViewGroup#setPadding(int, int, int, int) */ @Override public void setPadding( int left, int top, int right, int bottom ) { super.setPadding( left, top, right, bottom ); mPaddingLeft = left; mPaddingBottom = bottom; mPaddingTop = top; mPaddingRight = right; } /** * Common code for different constructor flavors. */ private void initAbsSpinner() { setFocusable( true ); setWillNotDraw( false ); } /** * The Adapter is used to provide the data which backs this Spinner. It also provides methods to transform spinner items based on * their position relative to the selected item. * * @param adapter * The SpinnerAdapter to use for this Spinner */ @Override public void setAdapter( Adapter adapter ) { if ( null != mAdapter ) { mAdapter.unregisterDataSetObserver( mDataSetObserver ); emptyRecycler(); resetList(); } mAdapter = adapter; mOldSelectedPosition = INVALID_POSITION; mOldSelectedRowId = INVALID_ROW_ID; if ( mAdapter != null ) { mOldItemCount = mItemCount; mItemCount = mAdapter.getCount(); checkFocus(); mDataSetObserver = new AdapterDataSetObserver(); mAdapter.registerDataSetObserver( mDataSetObserver ); int position = mItemCount > 0 ? 0 : INVALID_POSITION; int total = mAdapter.getViewTypeCount(); for ( int i = 0; i < total; i++ ) { mRecycleBin.add( new LinkedList<View>() ); } setSelectedPositionInt( position ); setNextSelectedPositionInt( position ); if ( mItemCount == 0 ) { // Nothing selected checkSelectionChanged(); } } else { checkFocus(); resetList(); // Nothing selected checkSelectionChanged(); } requestLayout(); } private void emptyRecycler() { emptySubRecycler(); if ( null != mRecycleBin ) { mRecycleBin.clear(); } } protected void emptySubRecycler() { if ( null != mRecycleBin ) { for( int i = 0; i < mRecycleBin.size(); i++ ){ mRecycleBin.get( i ).clear(); } } } /** * Clear out all children from the list. */ void resetList() { mDataChanged = false; mNeedSync = false; removeAllViewsInLayout(); mOldSelectedPosition = INVALID_POSITION; mOldSelectedRowId = INVALID_ROW_ID; setSelectedPositionInt( INVALID_POSITION ); setNextSelectedPositionInt( INVALID_POSITION ); invalidate(); } /** * On measure. * * @param widthMeasureSpec * the width measure spec * @param heightMeasureSpec * the height measure spec * @see android.view.View#measure(int, int) * * Figure out the dimensions of this Spinner. The width comes from the widthMeasureSpec as Spinnners can't have their width * set to UNSPECIFIED. The height is based on the height of the selected item plus padding. */ @Override protected void onMeasure( int widthMeasureSpec, int heightMeasureSpec ) { int widthMode = MeasureSpec.getMode( widthMeasureSpec ); int widthSize; int heightSize; mSpinnerPadding.left = mPaddingLeft > mSelectionLeftPadding ? mPaddingLeft : mSelectionLeftPadding; mSpinnerPadding.top = mPaddingTop > mSelectionTopPadding ? mPaddingTop : mSelectionTopPadding; mSpinnerPadding.right = mPaddingRight > mSelectionRightPadding ? mPaddingRight : mSelectionRightPadding; mSpinnerPadding.bottom = mPaddingBottom > mSelectionBottomPadding ? mPaddingBottom : mSelectionBottomPadding; if ( mDataChanged ) { handleDataChanged(); } int preferredHeight = 0; int preferredWidth = 0; boolean needsMeasuring = true; int selectedPosition = getSelectedItemPosition(); if ( selectedPosition >= 0 && mAdapter != null && selectedPosition < mAdapter.getCount() ) { // Try looking in the recycler. (Maybe we were measured once already) int viewType = mAdapter.getItemViewType( selectedPosition ); View view = mRecycleBin.get( viewType ).poll(); if ( view == null ) { // Make a new one view = mAdapter.getView( selectedPosition, null, this ); } if ( view != null ) { // Put in recycler for re-measuring and/or layout mRecycleBin.get( viewType ).offer( view ); } if ( view != null ) { if ( view.getLayoutParams() == null ) { mBlockLayoutRequests = true; view.setLayoutParams( generateDefaultLayoutParams() ); mBlockLayoutRequests = false; } measureChild( view, widthMeasureSpec, heightMeasureSpec ); preferredHeight = getChildHeight( view ) + mSpinnerPadding.top + mSpinnerPadding.bottom; preferredWidth = getChildWidth( view ) + mSpinnerPadding.left + mSpinnerPadding.right; needsMeasuring = false; } } if ( needsMeasuring ) { // No views -- just use padding preferredHeight = mSpinnerPadding.top + mSpinnerPadding.bottom; if ( widthMode == MeasureSpec.UNSPECIFIED ) { preferredWidth = mSpinnerPadding.left + mSpinnerPadding.right; } } preferredHeight = Math.max( preferredHeight, getSuggestedMinimumHeight() ); preferredWidth = Math.max( preferredWidth, getSuggestedMinimumWidth() ); heightSize = resolveSize( preferredHeight, heightMeasureSpec ); widthSize = resolveSize( preferredWidth, widthMeasureSpec ); setMeasuredDimension( widthSize, heightSize ); mHeightMeasureSpec = heightMeasureSpec; mWidthMeasureSpec = widthMeasureSpec; } /** * Gets the child height. * * @param child * the child * @return the child height */ int getChildHeight( View child ) { return child.getMeasuredHeight(); } /** * Gets the child width. * * @param child * the child * @return the child width */ int getChildWidth( View child ) { return child.getMeasuredWidth(); } /* * (non-Javadoc) * * @see android.view.ViewGroup#generateDefaultLayoutParams() */ @Override protected ViewGroup.LayoutParams generateDefaultLayoutParams() { return new ViewGroup.LayoutParams( ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.WRAP_CONTENT ); } /** * Recycle all views. */ void recycleAllViews() { final int childCount = getChildCount(); final int position = mFirstPosition; // All views go in recycler for ( int i = 0; i < childCount; i++ ) { View v = getChildAt( i ); int index = position + i; int viewType = mAdapter.getItemViewType( index ); mRecycleBin.get( viewType ).offer( v ); // if ( position + i < 0 ) { // recycleBin2.put( index, v ); // } else { // recycleBin.put( index, v ); // } } // recycleBin2.clear(); } /** * Jump directly to a specific item in the adapter data. * * @param position * the position * @param animate * the animate */ public void setSelection( int position, boolean animate, boolean changed ) { // Animate only if requested position is already on screen somewhere boolean shouldAnimate = animate && mFirstPosition <= position && position <= mFirstPosition + getChildCount() - 1; setSelectionInt( position, shouldAnimate, changed ); } /* * (non-Javadoc) * * @see com.aviary.android.feather.widget.AdapterView#setSelection(int) */ @Override public void setSelection( int position ) { setNextSelectedPositionInt( position ); requestLayout(); invalidate(); } /** * Makes the item at the supplied position selected. * * @param position * Position to select * @param animate * Should the transition be animated * */ void setSelectionInt( int position, boolean animate, boolean changed ) { if ( position != mOldSelectedPosition ) { mBlockLayoutRequests = true; int delta = position - mSelectedPosition; setNextSelectedPositionInt( position ); layout( delta, animate, changed ); mBlockLayoutRequests = false; } } /** * Layout. * * @param delta * the delta * @param animate * the animate */ abstract void layout( int delta, boolean animate, boolean changed ); /* * (non-Javadoc) * * @see com.aviary.android.feather.widget.AdapterView#getSelectedView() */ @Override public View getSelectedView() { if ( mItemCount > 0 && mSelectedPosition >= 0 ) { return getChildAt( mSelectedPosition - mFirstPosition ); } else { return null; } } /** * Override to prevent spamming ourselves with layout requests as we place views. * * @see android.view.View#requestLayout() */ @Override public void requestLayout() { if ( !mBlockLayoutRequests ) { super.requestLayout(); } } /* * (non-Javadoc) * * @see com.aviary.android.feather.widget.AdapterView#getAdapter() */ @Override public Adapter getAdapter() { return mAdapter; } /* * (non-Javadoc) * * @see com.aviary.android.feather.widget.AdapterView#getCount() */ @Override public int getCount() { return mItemCount; } /** * Maps a point to a position in the list. * * @param x * X in local coordinate * @param y * Y in local coordinate * @return The position of the item which contains the specified point, or {@link #INVALID_POSITION} if the point does not * intersect an item. */ public int pointToPosition( int x, int y ) { Rect frame = mTouchFrame; if ( frame == null ) { mTouchFrame = new Rect(); frame = mTouchFrame; } final int count = getChildCount(); for ( int i = count - 1; i >= 0; i-- ) { View child = getChildAt( i ); if ( child.getVisibility() == View.VISIBLE ) { child.getHitRect( frame ); if ( frame.contains( x, y ) ) { return mFirstPosition + i; } } } return INVALID_POSITION; } /** * The Class SavedState. */ static class SavedState extends BaseSavedState { /** The selected id. */ long selectedId; /** The position. */ int position; /** * Constructor called from {@link AbsSpinner#onSaveInstanceState()}. * * @param superState * the super state */ SavedState( Parcelable superState ) { super( superState ); } /** * Constructor called from {@link #CREATOR}. * * @param in * the in */ private SavedState( Parcel in ) { super( in ); selectedId = in.readLong(); position = in.readInt(); } /* * (non-Javadoc) * * @see android.view.AbsSavedState#writeToParcel(android.os.Parcel, int) */ @Override public void writeToParcel( Parcel out, int flags ) { super.writeToParcel( out, flags ); out.writeLong( selectedId ); out.writeInt( position ); } /* * (non-Javadoc) * * @see java.lang.Object#toString() */ @Override public String toString() { return "AbsSpinner.SavedState{" + Integer.toHexString( System.identityHashCode( this ) ) + " selectedId=" + selectedId + " position=" + position + "}"; } /** The Constant CREATOR. */ public static final Parcelable.Creator<SavedState> CREATOR = new Parcelable.Creator<SavedState>() { public SavedState createFromParcel( Parcel in ) { return new SavedState( in ); } public SavedState[] newArray( int size ) { return new SavedState[size]; } }; } /* * (non-Javadoc) * * @see android.view.View#onSaveInstanceState() */ @Override public Parcelable onSaveInstanceState() { Parcelable superState = super.onSaveInstanceState(); SavedState ss = new SavedState( superState ); ss.selectedId = getSelectedItemId(); if ( ss.selectedId >= 0 ) { ss.position = getSelectedItemPosition(); } else { ss.position = INVALID_POSITION; } return ss; } /* * (non-Javadoc) * * @see android.view.View#onRestoreInstanceState(android.os.Parcelable) */ @Override public void onRestoreInstanceState( Parcelable state ) { SavedState ss = (SavedState) state; super.onRestoreInstanceState( ss.getSuperState() ); if ( ss.selectedId >= 0 ) { mDataChanged = true; mNeedSync = true; mSyncRowId = ss.selectedId; mSyncPosition = ss.position; mSyncMode = SYNC_SELECTED_POSITION; requestLayout(); } } /** * The Class RecycleBin. */ class RecycleBin { /** The m scrap heap. */ @SuppressWarnings("unused") private final SparseArray<View> mScrapHeap = new SparseArray<View>(); /** The m heap. */ private final ArrayList<View> mHeap = new ArrayList<View>( 100 ); /** * Put. * * @param position * the position * @param v * the v */ public void put( int position, View v ) { mHeap.add( v ); // mScrapHeap.put( position, v ); } /** * Gets the. * * @param position * the position * @return the view */ View get( int position ) { if ( mHeap.size() < 1 ) return null; View result = mHeap.remove( 0 ); return result; /* * View result = mScrapHeap.get( position ); if ( result != null ) { mScrapHeap.delete( position ); } else { } return * result; */ } /** * Clear. */ void clear() { /* * final SparseArray<View> scrapHeap = mScrapHeap; final int count = scrapHeap.size(); for ( int i = 0; i < count; i++ ) { * final View view = scrapHeap.valueAt( i ); if ( view != null ) { removeDetachedView( view, true ); } } scrapHeap.clear(); */ final int count = mHeap.size(); for ( int i = 0; i < count; i++ ) { final View view = mHeap.remove( 0 ); if ( view != null ) { removeDetachedView( view, true ); } } mHeap.clear(); } } }