You are on page 1of 54

attrs.xml <?xml version="1.0" encoding="utf-8"?

> <resources> <declare-styleable name="EcoGallery"> <attr name="gravity" format="integer" /> <attr name="animationDuration" format="integer" /> <attr name="unselectedAlpha" format="float" /> <attr name="spacing" format="dimension" /> </declare-styleable> <attr name="ecoGalleryStyle" format="reference" /> <declare-styleable name="CustomAbsSpinner"> <attr name="entries" format="reference" /> </declare-styleable> <attr name="customAbsSpinnerStyle" format="reference" /> </resources> CustomAdapterView public abstract class CustomAdapterView<T extends Adapter> extends ViewGroup { /** * The item view type returned by {@link Adapter#getItemViewType(int)} w hen * the adapter does not want the item's view recycled. */ public static final int ITEM_VIEW_TYPE_IGNORE = -1; /** * The item view type returned by {@link Adapter#getItemViewType(int)} w hen * the item is a header or footer. */ public static final int ITEM_VIEW_TYPE_HEADER_OR_FOOTER = -2; /** * The position of the first child displayed */ int mFirstPosition = 0; /** * The offset in pixels from the top of the AdapterView to the top * of the view to select during the next layout. */ int mSpecificTop; /** * Position from which to start looking for mSyncRowId */ int mSyncPosition; /** * Row id to look for when data has changed */ long mSyncRowId = INVALID_ROW_ID; /**

* Height of the view when mSyncPosition and mSyncRowId where set */ long mSyncHeight; /** * True if we need to sync to mSyncRowId */ boolean mNeedSync = false; /** * Indicates whether to sync based on the selection or position. Possibl e * values are {@link #SYNC_SELECTED_POSITION} or * {@link #SYNC_FIRST_POSITION}. */ int mSyncMode; /** * Our height after the last layout */ private int mLayoutHeight; /** * Sync based on the selected child */ static final int SYNC_SELECTED_POSITION = 0; /** * Sync based on the first child displayed */ static final int SYNC_FIRST_POSITION = 1; /** * Maximum amount of time to spend in {@link #findSyncPosition()} */ static final int SYNC_MAX_DURATION_MILLIS = 100; /** * Indicates that this view is currently being laid out. */ boolean mInLayout = false; /** * The listener that receives notifications when an item is selected. */ OnItemSelectedListener mOnItemSelectedListener; /** * The listener that receives notifications when an item is clicked. */ OnItemClickListener mOnItemClickListener; /** * The listener that receives notifications when an item is long clicked . */ OnItemLongClickListener mOnItemLongClickListener; /** * True if the data has changed since the last layout

*/ boolean mDataChanged; /** * The position within the adapter's data set of the item to select * during the next layout. */ int mNextSelectedPosition = INVALID_POSITION; /** * The item id of the item to select during the next layout. */ long mNextSelectedRowId = INVALID_ROW_ID; /** * The position within the adapter's data set of the currently selected item. */ int mSelectedPosition = INVALID_POSITION; /** * The item id of the currently selected item. */ long mSelectedRowId = INVALID_ROW_ID; /** * View to show if there are no items to show. */ private View mEmptyView; /** * The number of items in the current adapter. */ int mItemCount; /** * The number of items in the adapter before a data changed event occure d. */ int mOldItemCount; /** * Represents an invalid position. All valid positions are in the range 0 to 1 less than the * number of items in the current adapter. */ public static final int INVALID_POSITION = -1; /** * Represents an empty or invalid row id */ public static final long INVALID_ROW_ID = Long.MIN_VALUE; /** * The last selected position we used when notifying */ int mOldSelectedPosition = INVALID_POSITION; /** * The id of the last selected position we used when notifying

*/ long mOldSelectedRowId = INVALID_ROW_ID; /** * Indicates what focusable state is requested when calling setFocusable (). * In addition to this, this view has other criteria for actually * determining the focusable state (such as whether its empty or the tex t * filter is shown). * * @see #setFocusable(boolean) * @see #checkFocus() */ private boolean mDesiredFocusableState; private boolean mDesiredFocusableInTouchModeState; private SelectionNotifier mSelectionNotifier; /** * When set to true, calls to requestLayout() will not propagate up the parent hierarchy. * This is used to layout the children during a layout pass. */ boolean mBlockLayoutRequests = false; public CustomAdapterView(Context context) { super(context); } public CustomAdapterView(Context context, AttributeSet attrs) { super(context, attrs); } public CustomAdapterView(Context context, AttributeSet attrs, int defSty le) { super(context, attrs, defStyle); } /** * Interface definition for a callback to be invoked when an item in thi s * AdapterView has been clicked. */ public interface OnItemClickListener { /** * Callback method to be invoked when an item in this AdapterView ha s * * * * * * * s * will be a view provided by the adapter) * @param position The position of the view in the adapter. * @param id The row id of the item that was clicked. been clicked. <p> Implementers can call getItemAtPosition(position) if they need to access the data associated with the selected item. @param parent The AdapterView where the click happened. @param view The view within the AdapterView that was clicked (thi

*/ void onItemClick(CustomAdapterView<?> parent, View view, int positio n, long id); } /** * Register a callback to be invoked when an item in this AdapterView ha s * been clicked. * * @param listener The callback that will be invoked. */ public void setOnItemClickListener(OnItemClickListener listener) { mOnItemClickListener = listener; } /** * @return The callback to be invoked with an item in this AdapterView h as * been clicked, or null id no callback has been set. */ public final OnItemClickListener getOnItemClickListener() { return mOnItemClickListener; } /** * Call the OnItemClickListener, if it is defined. * * @param view The view within the AdapterView that was clicked. * @param position The position of the view in the adapter. * @param id The row id of the item that was clicked. * @return True if there was an assigned OnItemClickListener that was * called, false otherwise is returned. */ public boolean performItemClick(View view, int position, long id) { if (mOnItemClickListener != null) { playSoundEffect(SoundEffectConstants.CLICK); mOnItemClickListener.onItemClick(this, view, position, id); return true; } return false; } /** * Interface definition for a callback to be invoked when an item in thi s * view has been clicked and held. */ public interface OnItemLongClickListener { /** * Callback method to be invoked when an item in this view has been * clicked and held. * * Implementers can call getItemAtPosition(position) if they need to access * the data associated with the selected item. * * @param parent The AbsListView where the click happened * @param view The view within the AbsListView that was clicked

* @param position The position of the view in the list * @param id The row id of the item that was clicked * * @return true if the callback consumed the long click, false other wise */ boolean onItemLongClick(CustomAdapterView<?> parent, View view, int position, long id); } /** * Register a callback to be invoked when an item in this AdapterView ha s * been clicked and held * * @param listener The callback that will run */ public void setOnItemLongClickListener(OnItemLongClickListener listener) { if (!isLongClickable()) { setLongClickable(true); } mOnItemLongClickListener = listener; } /** * @return The callback to be invoked with an item in this AdapterView h as * been clicked and held, or null id no callback as been set. */ public final OnItemLongClickListener getOnItemLongClickListener() { return mOnItemLongClickListener; } /** * Interface definition for a callback to be invoked when * an item in this view has been selected. */ public interface OnItemSelectedListener { /** * Callback method to be invoked when an item in this view has been * selected. * * Impelmenters can call getItemAtPosition(position) if they need to access the * data associated with the selected item. * * @param parent The AdapterView where the selection happened * @param view The view within the AdapterView that was clicked * @param position The position of the view in the adapter * @param id The row id of the item that is selected */ void onItemSelected(CustomAdapterView<?> parent, View view, int posi tion, long id); /** * Callback method to be invoked when the selection disappears from this * view. The selection can disappear for instance when touch is acti

vated * or when the adapter becomes empty. * * @param parent The AdapterView that now contains no selected item. */ void onNothingSelected(CustomAdapterView<?> parent); } /** * Register a callback to be invoked when an item in this AdapterView ha s * been selected. * * @param listener The callback that will run */ public void setOnItemSelectedListener(OnItemSelectedListener listener) { mOnItemSelectedListener = listener; } public final OnItemSelectedListener getOnItemSelectedListener() { return mOnItemSelectedListener; } /** * Extra menu information provided to the * {@link android.view.View.OnCreateContextMenuListener#onCreateContextM enu(ContextMenu, View, ContextMenuInfo) } * callback when a context menu is brought up for this AdapterView. * */ public static class AdapterContextMenuInfo implements ContextMenu.Contex tMenuInfo { public AdapterContextMenuInfo(View targetView, int position, long id ) { this.targetView = targetView; this.position = position; this.id = id; } /** * The child view for which the context menu is being displayed. Thi s * will be one of the children of this AdapterView. */ public View targetView; /** * The position in the adapter for which the context menu is being * displayed. */ public int position; /** * The row id of the item for which the context menu is being displa yed. */ public long id; }

/** * Returns the adapter currently associated with this widget. * * @return The adapter used to provide this view's content. */ public abstract T getAdapter(); /** * Sets the adapter that provides the data and the views to represent th e data * in this widget. * * @param adapter The adapter to use to create this view's content. */ public abstract void setAdapter(T adapter); /** * This method is not supported and throws an UnsupportedOperationExcept ion when called. * * @param child Ignored. * * @throws UnsupportedOperationException Every time this method is invok ed. */ @Override public void addView(View child) { throw new UnsupportedOperationException("addView(View) is not suppor ted in AdapterView"); } /** * This method is not supported and throws an UnsupportedOperationExcept ion when called. * * @param child Ignored. * @param index Ignored. * * @throws UnsupportedOperationException Every time this method is invok ed. */ @Override public void addView(View child, int index) { throw new UnsupportedOperationException("addView(View, int) is not s upported in AdapterView"); } /** * This method is not supported and throws an UnsupportedOperationExcept ion when called. * * @param child Ignored. * @param params Ignored. * * @throws UnsupportedOperationException Every time this method is invok ed. */ @Override public void addView(View child, LayoutParams params) {

throw new UnsupportedOperationException("addView(View, LayoutParams) " + "is not supported in AdapterView"); } /** * This method is not supported and throws an UnsupportedOperationExcept ion when called. * * @param child Ignored. * @param index Ignored. * @param params Ignored. * * @throws UnsupportedOperationException Every time this method is invok ed. */ @Override public void addView(View child, int index, LayoutParams params) { throw new UnsupportedOperationException("addView(View, int, LayoutPa rams) " + "is not supported in AdapterView"); } /** * This method is not supported and throws an UnsupportedOperationExcept ion when called. * * @param child Ignored. * * @throws UnsupportedOperationException Every time this method is invok ed. */ @Override public void removeView(View child) { throw new UnsupportedOperationException("removeView(View) is not sup ported in AdapterView"); } /** * This method is not supported and throws an UnsupportedOperationExcept ion when called. * * @param index Ignored. * * @throws UnsupportedOperationException Every time this method is invok ed. */ @Override public void removeViewAt(int index) { throw new UnsupportedOperationException("removeViewAt(int) is not su pported in AdapterView"); } /** * This method is not supported and throws an UnsupportedOperationExcept ion when called. * * @throws UnsupportedOperationException Every time this method is invok ed. */

@Override public void removeAllViews() { throw new UnsupportedOperationException("removeAllViews() is not sup ported in AdapterView"); } @Override protected void onLayout(boolean changed, int left, int top, int right, i nt bottom) { mLayoutHeight = getHeight(); } /** * Return the position of the currently selected item within the adapter 's data set * * @return int Position (starting at 0), or {@link #INVALID_POSITION} if there is nothing selected. */ @ViewDebug.CapturedViewProperty public int getSelectedItemPosition() { return mNextSelectedPosition; } /** * @return The id corresponding to the currently selected item, or {@lin k #INVALID_ROW_ID} * if nothing is selected. */ @ViewDebug.CapturedViewProperty public long getSelectedItemId() { return mNextSelectedRowId; } /** * @return The view corresponding to the currently selected item, or nul l * if nothing is selected */ public abstract View getSelectedView(); /** * @return The data corresponding to the currently selected item, or * null if there is nothing selected. */ public Object getSelectedItem() { T adapter = getAdapter(); int selection = getSelectedItemPosition(); if (adapter != null && adapter.getCount() > 0 && selection >= 0) { return adapter.getItem(selection); } else { return null; } } /** * @return The number of items owned by the Adapter associated with this * AdapterView. (This is the number of data items, which may be * larger than the number of visible view.) */

@ViewDebug.CapturedViewProperty public int getCount() { return mItemCount; } /** * Get the position within the adapter's data set for the view, where vi ew is a an adapter item * or a descendant of an adapter item. * * @param view an adapter item, or a descendant of an adapter item. This must be visible in this * AdapterView at the time of the call. * @return the position within the adapter's data set of the view, or {@ link #INVALID_POSITION} * if the view does not correspond to a list item (or it is not currently visible). */ public int getPositionForView(View view) { View listItem = view; try { View v; while (!(v = (View) listItem.getParent()).equals(this)) { listItem = v; } } catch (ClassCastException e) { // We made it up to the window without find this list view return INVALID_POSITION; } // Search the children for the list item final int childCount = getChildCount(); for (int i = 0; i < childCount; i++) { if (getChildAt(i).equals(listItem)) { return mFirstPosition + i; } } // Child not found! return INVALID_POSITION; } /** * Returns the position within the adapter's data set for the first item * displayed on screen. * * @return The position within the adapter's data set */ public int getFirstVisiblePosition() { return mFirstPosition; } /** * Returns the position within the adapter's data set for the last item * displayed on screen. * * @return The position within the adapter's data set */ public int getLastVisiblePosition() { return mFirstPosition + getChildCount() - 1;

} /** * Sets the currently selected item. To support accessibility subclasses that * override this method must invoke the overriden super method first. * * @param position Index (starting at 0) of the data item to be selected . */ public abstract void setSelection(int position); /** * Sets the view to show if the adapter is empty */ public void setEmptyView(View emptyView) { mEmptyView = emptyView; final T adapter = getAdapter(); final boolean empty = ((adapter == null) || adapter.isEmpty()); updateEmptyStatus(empty); } /** * When the current adapter is empty, the AdapterView can display a spec ial view * call the empty view. The empty view is used to provide feedback to th e user * that no data is available in this AdapterView. * * @return The view to show if the adapter is empty. */ public View getEmptyView() { return mEmptyView; } /** * Indicates whether this view is in filter mode. Filter mode can for in stance * be enabled by a user when typing on the keyboard. * * @return True if the view is in filter mode, false otherwise. */ boolean isInFilterMode() { return false; } @Override public void setFocusable(boolean focusable) { final T adapter = getAdapter(); final boolean empty = adapter == null || adapter.getCount() == 0; mDesiredFocusableState = focusable; if (!focusable) { mDesiredFocusableInTouchModeState = false; } super.setFocusable(focusable && (!empty || isInFilterMode())); }

@Override public void setFocusableInTouchMode(boolean focusable) { final T adapter = getAdapter(); final boolean empty = adapter == null || adapter.getCount() == 0; mDesiredFocusableInTouchModeState = focusable; if (focusable) { mDesiredFocusableState = true; } super.setFocusableInTouchMode(focusable && (!empty || isInFilterMode ())); } void checkFocus() { final T adapter = getAdapter(); final boolean empty = adapter == null || adapter.getCount() == 0; final boolean focusable = !empty || isInFilterMode(); // The order in which we set focusable in touch mode/focusable may m atter // for the client, see View.setFocusableInTouchMode() comments for m ore // details super.setFocusableInTouchMode(focusable && mDesiredFocusableInTouchM odeState); super.setFocusable(focusable && mDesiredFocusableState); if (mEmptyView != null) { updateEmptyStatus((adapter == null) || adapter.isEmpty()); } } /** * Update the status of the list based on the is true and * we have an empty view, display it. In all that the listview * is VISIBLE and that the empty view is GONE */ private void updateEmptyStatus(boolean empty) if (isInFilterMode()) { empty = false; } empty parameter. If empty the other cases, make sure (if it's not null). {

if (empty) { if (mEmptyView != null) { mEmptyView.setVisibility(View.VISIBLE); setVisibility(View.GONE); } else { // If the caller just removed our empty view, make sure the list view is visible setVisibility(View.VISIBLE); } // We are now GONE, so pending layouts will not be dispatched. // Force one here to make sure that the state of the list matche s // the state of the adapter. if (mDataChanged) { this.onLayout(false, getLeft(), getTop(), getRight(), getBot tom());

} } else { if (mEmptyView != null) mEmptyView.setVisibility(View.GONE); setVisibility(View.VISIBLE); } } /** * Gets the data associated with the specified position in the list. * * @param position Which data to get * @return The data associated with the specified position in the list */ public Object getItemAtPosition(int position) { T adapter = getAdapter(); return (adapter == null || position < 0) ? null : adapter.getItem(po sition); } public long getItemIdAtPosition(int position) { T adapter = getAdapter(); return (adapter == null || position < 0) ? INVALID_ROW_ID : adapter. getItemId(position); } @Override public void setOnClickListener(OnClickListener l) { throw new RuntimeException("Don't call setOnClickListener for an Ada pterView. " + "You probably want setOnItemClickListener instead"); } /** * Override to prevent freezing of any views created by the adapter. */ @Override protected void dispatchSaveInstanceState(SparseArray<Parcelable> contain er) { dispatchFreezeSelfOnly(container); } /** * Override to prevent thawing of any views created by the adapter. */ @Override protected void dispatchRestoreInstanceState(SparseArray<Parcelable> cont ainer) { dispatchThawSelfOnly(container); } class AdapterDataSetObserver extends DataSetObserver { private Parcelable mInstanceState = null; @Override public void onChanged() { mDataChanged = true; mOldItemCount = mItemCount; mItemCount = getAdapter().getCount();

// Detect the case where a cursor that was previously invalidate d has // been repopulated with new data. if (CustomAdapterView.this.getAdapter().hasStableIds() && mInsta nceState != null && mOldItemCount == 0 && mItemCount > 0) { CustomAdapterView.this.onRestoreInstanceState(mInstanceState ); mInstanceState = null; } else { rememberSyncState(); } checkFocus(); requestLayout(); } @Override public void onInvalidated() { mDataChanged = true; if (CustomAdapterView.this.getAdapter().hasStableIds()) { // Remember the current state for the case where our hosting activity is being // stopped and later restarted mInstanceState = CustomAdapterView.this.onSaveInstanceState( ); } // Data is invalid so we should reset our state mOldItemCount = mItemCount; mItemCount = 0; mSelectedPosition = INVALID_POSITION; mSelectedRowId = INVALID_ROW_ID; mNextSelectedPosition = INVALID_POSITION; mNextSelectedRowId = INVALID_ROW_ID; mNeedSync = false; checkFocus(); requestLayout(); } public void clearSavedState() { mInstanceState = null; } } @Override protected void onDetachedFromWindow() { super.onDetachedFromWindow(); removeCallbacks(mSelectionNotifier); } private class SelectionNotifier implements Runnable { public void run() { if (mDataChanged) { // Data has changed between when this SelectionNotifier // was posted and now. We need to wait until the AdapterView // has been synched to the new data. if (getAdapter() != null) { post(this);

} } else { fireOnSelected(); } } } void selectionChanged() { if (mOnItemSelectedListener != null) { if (mInLayout || mBlockLayoutRequests) { // If we are in a layout traversal, defer notification // by posting. This ensures that the view tree is // in a consistent state and is able to accomodate // new layout or invalidate requests. if (mSelectionNotifier == null) { mSelectionNotifier = new SelectionNotifier(); } post(mSelectionNotifier); } else { fireOnSelected(); } } // we fire selection events here not in View if (mSelectedPosition != ListView.INVALID_POSITION && isShown() && ! isInTouchMode()) { sendAccessibilityEvent(AccessibilityEvent.TYPE_VIEW_SELECTED); } } private void fireOnSelected() { if (mOnItemSelectedListener == null) return; int selection = this.getSelectedItemPosition(); if (selection >= 0) { View v = getSelectedView(); mOnItemSelectedListener.onItemSelected(this, v, selection, getAdapter().getItemId(selection)); } else { mOnItemSelectedListener.onNothingSelected(this); } } @Override public boolean dispatchPopulateAccessibilityEvent(AccessibilityEvent eve nt) { boolean populated = false; // This is an exceptional case which occurs when a window gets the // focus and sends a focus event via its focused child to announce // current focus/selection. AdapterView fires selection but not focu s // events so we change the event type here. if (event.getEventType() == AccessibilityEvent.TYPE_VIEW_FOCUSED) { event.setEventType(AccessibilityEvent.TYPE_VIEW_SELECTED); } // we send selection events only from AdapterView to avoid // generation of such event for each child View selectedView = getSelectedView();

if (selectedView != null) { populated = selectedView.dispatchPopulateAccessibilityEvent(even t); } if (!populated) { if (selectedView != null) { event.setEnabled(selectedView.isEnabled()); } event.setItemCount(getCount()); event.setCurrentItemIndex(getSelectedItemPosition()); } return populated; } @Override protected boolean canAnimate() { return super.canAnimate() && mItemCount > 0; } void handleDataChanged() { final int count = mItemCount; boolean found = false; if (count > 0) { int newPos; // Find the row we are supposed to sync to if (mNeedSync) { // Update this first, since setNextSelectedPositionInt inspe cts // it mNeedSync = false; // See if we can find a position in the new data with the sa me // id as the old selection newPos = findSyncPosition(); if (newPos >= 0) { // Verify that new selection is selectable int selectablePos = lookForSelectablePosition(newPos, tr ue); if (selectablePos == newPos) { // Same row id is selected setNextSelectedPositionInt(newPos); found = true; } } } if (!found) { // Try to use the same position if we can't find matching da ta newPos = getSelectedItemPosition(); // Pin position to the available range if (newPos >= count) { newPos = count - 1; }

if (newPos < 0) { newPos = 0; } // Make sure we select something selectable -- first look do wn int selectablePos = if (selectablePos < // Looking down selectablePos = ; } if (selectablePos >= 0) { setNextSelectedPositionInt(selectablePos); checkSelectionChanged(); found = true; } } } if (!found) { // Nothing is selected mSelectedPosition = INVALID_POSITION; mSelectedRowId = INVALID_ROW_ID; mNextSelectedPosition = INVALID_POSITION; mNextSelectedRowId = INVALID_ROW_ID; mNeedSync = false; checkSelectionChanged(); } } void checkSelectionChanged() { if ((mSelectedPosition != mOldSelectedPosition) || (mSelectedRowId ! = mOldSelectedRowId)) { selectionChanged(); mOldSelectedPosition = mSelectedPosition; mOldSelectedRowId = mSelectedRowId; } } /** * Searches the adapter for a position matching mSyncRowId. The search s tarts at mSyncPosition * and then alternates between moving up and moving down until 1) we fin d the right position, or * 2) we run out of time, or 3) we have looked at every position * * @return Position of the row that matches mSyncRowId, or {@link #INVAL ID_POSITION} if it can't * be found */ int findSyncPosition() { int count = mItemCount; if (count == 0) { return INVALID_POSITION; } long idToMatch = mSyncRowId; int seed = mSyncPosition; lookForSelectablePosition(newPos, true); 0) { didn't work -- try looking up lookForSelectablePosition(newPos, false)

// If there isn't a selection don't hunt for it if (idToMatch == INVALID_ROW_ID) { return INVALID_POSITION; } // Pin seed to reasonable values seed = Math.max(0, seed); seed = Math.min(count - 1, seed); long endTime = SystemClock.uptimeMillis() + SYNC_MAX_DURATION_MILLIS ; long rowId; // first position scanned so far int first = seed; // last position scanned so far int last = seed; // True if we should move down on the next iteration boolean next = false; // True when we have looked at the first item in the data boolean hitFirst; // True when we have looked at the last item in the data boolean hitLast; // Get the item ID locally (instead of getItemIdAtPosition), so // we need the adapter T adapter = getAdapter(); if (adapter == null) { return INVALID_POSITION; } while (SystemClock.uptimeMillis() <= endTime) { rowId = adapter.getItemId(seed); if (rowId == idToMatch) { // Found it! return seed; } hitLast = last == count - 1; hitFirst = first == 0; if (hitLast && hitFirst) { // Looked at everything break; } if (hitFirst || (next && !hitLast)) { // Either we hit the top, or we are trying to move down last++; seed = last; // Try going up next time next = false; } else if (hitLast || (!next && !hitFirst)) { // Either we hit the bottom, or we are trying to move up first--;

seed = first; // Try going down next time next = true; } } return INVALID_POSITION; } /** * Find a position that can be selected (i.e., is not a separator). * * @param position The starting position to look at. * @param lookDown Whether to look down for other positions. * @return The next selectable position starting at position and then se arching either up or * down. Returns {@link #INVALID_POSITION} if nothing can be fou nd. */ int lookForSelectablePosition(int position, boolean lookDown) { return position; } /** * Utility to keep mSelectedPosition and mSelectedRowId in sync * @param position Our current position */ void setSelectedPositionInt(int position) { mSelectedPosition = position; mSelectedRowId = getItemIdAtPosition(position); } /** * Utility to keep mNextSelectedPosition and mNextSelectedRowId in sync * @param position Intended value for mSelectedPosition the next time we go * through layout */ void setNextSelectedPositionInt(int position) { mNextSelectedPosition = position; mNextSelectedRowId = getItemIdAtPosition(position); // If we are trying to sync to the selection, update that too if (mNeedSync && mSyncMode == SYNC_SELECTED_POSITION && position >= 0) { mSyncPosition = position; mSyncRowId = mNextSelectedRowId; } } /** * Remember enough information to restore the screen state when the data has * changed. * */ void rememberSyncState() { if (getChildCount() > 0) { mNeedSync = true; mSyncHeight = mLayoutHeight;

if (mSelectedPosition >= 0) { // Sync the selection state View v = getChildAt(mSelectedPosition - mFirstPosition); mSyncRowId = mNextSelectedRowId; mSyncPosition = mNextSelectedPosition; if (v != null) { mSpecificTop = v.getTop(); } mSyncMode = SYNC_SELECTED_POSITION; } else { // Sync the based on the offset of the first view View v = getChildAt(0); T adapter = getAdapter(); if (mFirstPosition >= 0 && mFirstPosition < adapter.getCount ()) { mSyncRowId = adapter.getItemId(mFirstPosition); } else { mSyncRowId = NO_ID; } mSyncPosition = mFirstPosition; if (v != null) { mSpecificTop = v.getTop(); } mSyncMode = SYNC_FIRST_POSITION; } } } } <br> CustomAbsSpinner: added the `add` and `get` methods to recycle bin that ignore p osition, modified recycler use in the class to use that. public abstract class CustomAbsSpinner extends CustomAdapterView<SpinnerAdap ter> { SpinnerAdapter mAdapter; int mHeightMeasureSpec; int mWidthMeasureSpec; boolean mBlockLayoutRequests; int mSelectionLeftPadding = 0; int mSelectionTopPadding = 0; int mSelectionRightPadding = 0; int mSelectionBottomPadding = 0; Rect mSpinnerPadding = new Rect(); View mSelectedView = null; Interpolator mInterpolator; RecycleBin mRecycler = new RecycleBin(); private DataSetObserver mDataSetObserver; /** Temporary frame to hold a child View's frame rectangle */ private Rect mTouchFrame; public CustomAbsSpinner(Context context) { super(context); initAbsSpinner(); }

public CustomAbsSpinner(Context context, AttributeSet attrs) { this(context, attrs, 0); } public CustomAbsSpinner(Context context, AttributeSet attrs, int defStyl e) { super(context, attrs, defStyle); initAbsSpinner(); TypedArray a = context.obtainStyledAttributes(attrs, R.styleable.CustomAbsSpinner, defStyle, 0); CharSequence[] entries = a.getTextArray(R.styleable.CustomAbsSpinner _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(); } /** * 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 po sition * relative to the selected item. * @param adapter The SpinnerAdapter to use for this Spinner */ @Override public void setAdapter(SpinnerAdapter adapter) { if (null != mAdapter) { mAdapter.unregisterDataSetObserver(mDataSetObserver); 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; setSelectedPositionInt(position); setNextSelectedPositionInt(position); if (mItemCount == 0) { // Nothing selected checkSelectionChanged(); } } else { checkFocus(); resetList(); // Nothing selected checkSelectionChanged(); } requestLayout(); } /** * 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(); } /** * @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; int int int int Left paddingLeft = getPaddingLeft(); paddingRight = getPaddingRight(); paddingTop = getPaddingTop(); paddingBottom = getPaddingBottom();

mSpinnerPadding.left = paddingLeft > mSelectionLeftPadding ? padding

: mSelectionLeftPadding; mSpinnerPadding.top = paddingTop > mSelectionTopPadding ? paddingTop : mSelectionTopPadding; mSpinnerPadding.right = paddingRight > mSelectionRightPadding ? padd ingRight : mSelectionRightPadding; mSpinnerPadding.bottom = paddingBottom > mSelectionBottomPadding ? p addingBottom : mSelectionBottomPadding; if (mDataChanged) { handleDataChanged(); } int preferredHeight = 0; int preferredWidth = 0; boolean needsMeasuring = true; int selectedPosition = getSelectedItemPosition(); if (selectedPosition >= 0 && mAdapter != null) { // Try looking in the recycler. (Maybe we were measured once alr eady) View view = mRecycler.get(); 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 mRecycler.add(selectedPosition, 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.righ t; } } preferredHeight = Math.max(preferredHeight, getSuggestedMinimumHeigh

t()); preferredWidth = Math.max(preferredWidth, getSuggestedMinimumWidth() ); heightSize = resolveSize(preferredHeight, heightMeasureSpec); widthSize = resolveSize(preferredWidth, widthMeasureSpec); setMeasuredDimension(widthSize, heightSize); mHeightMeasureSpec = heightMeasureSpec; mWidthMeasureSpec = widthMeasureSpec; } int getChildHeight(View child) { return child.getMeasuredHeight(); } int getChildWidth(View child) { return child.getMeasuredWidth(); } @Override protected ViewGroup.LayoutParams generateDefaultLayoutParams() { return new ViewGroup.LayoutParams( ViewGroup.LayoutParams.FILL_PARENT, ViewGroup.LayoutParams.WRAP_CONTENT); } void recycleAllViews() { int childCount = getChildCount(); final CustomAbsSpinner.RecycleBin recycleBin = mRecycler; // All views go in recycler for (int i=0; i<childCount; i++) { View v = getChildAt(i); int index = mFirstPosition + i; recycleBin.put(index, v); } } @Override void handleDataChanged() { // FIXME -- this is called from both measure and layout. // This is harmless right now, but we don't want to do redundant wor k if // this gets more complicated super.handleDataChanged(); }

/** * Jump directly to a specific item in the adapter data. */ public void setSelection(int position, boolean animate) { // Animate only if requested position is already on screen somewhere boolean shouldAnimate = animate && mFirstPosition <= position && position <= mFirstPosition + getChildCount() - 1; setSelectionInt(position, shouldAnimate); }

@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) { if (position != mOldSelectedPosition) { mBlockLayoutRequests = true; int delta = position - mSelectedPosition; setNextSelectedPositionInt(position); layout(delta, animate); mBlockLayoutRequests = false; } } abstract void layout(int delta, boolean animate); @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(); } }

@Override public SpinnerAdapter getAdapter() { return mAdapter; } @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 * 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; } static class SavedState extends BaseSavedState { long selectedId; int position; /** * Constructor called from {@link CustomAbsSpinner#onSaveInstanceSta te()} */ SavedState(Parcelable superState) { super(superState); } /** * Constructor called from {@link #CREATOR} */ private SavedState(Parcel in) { super(in); selectedId = in.readLong(); position = in.readInt(); } @Override public void writeToParcel(Parcel out, int flags) { super.writeToParcel(out, flags); out.writeLong(selectedId); out.writeInt(position); } {@link #INVALID_POSITION} if the point does not intersect an

@Override public String toString() { return "AbsSpinner.SavedState{" + Integer.toHexString(System.identityHashCode(this)) + " selectedId=" + selectedId + " position=" + position + "}"; } 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]; } }; } @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; } @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(); } } class RecycleBin { private SparseArray<View> mScrapHeap = new SparseArray<View>(); public void put(int position, View v) { mScrapHeap.put(position, v); } public void add(int position, View v) { mScrapHeap.put(mScrapHeap.size(), v); }

public View get() { if (mScrapHeap.size() < 1) return null; View result = mScrapHeap.valueAt(0); int key = mScrapHeap.keyAt(0); if (result != null) { mScrapHeap.delete(key); } return result; } View get(int position) { // System.out.print("Looking for " + position); View result = mScrapHeap.get(position); if (result != null) { // System.out.println(" HIT"); mScrapHeap.delete(position); } else { // System.out.println(" MISS"); } return result; } View peek(int position) { // System.out.print("Looking for " + position); return mScrapHeap.get(position); } 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(); } } } <br> EcoGallery: (it recycles) public class EcoGallery extends CustomAbsSpinner implements GestureDetector. OnGestureListener { private static final String TAG = "Gallery"; private static final boolean localLOGV = false; /** * Duration in milliseconds from the start of a scroll during which we'r e * unsure whether the user is scrolling or flinging. */

private static final int SCROLL_TO_FLING_UNCERTAINTY_TIMEOUT = 250; private static final String LOG_TAG = null; /** * Horizontal spacing between items. */ private int mSpacing = 0; /** * How long the transition animation should run when a child view change s * position, measured in milliseconds. */ private int mAnimationDuration = 200; /** * The alpha of items that are not selected. */ private float mUnselectedAlpha; /** * Left most edge of a child seen so far during layout. */ private int mLeftMost; /** * Right most edge of a child seen so far during layout. */ private int mRightMost; private int mGravity; /** * Helper for detecting touch gestures. */ private GestureDetector mGestureDetector; /** * The position of the item that received the user's down touch. */ private int mDownTouchPosition; /** * The view of the item that received the user's down touch. */ private View mDownTouchView; /** * Executes the delta scrolls from a fling or scroll movement. */ private FlingRunnable mFlingRunnable = new FlingRunnable(); /** * Sets mSuppressSelectionChanged = false. This is used to set it to fal se * in the future. It will also trigger a selection changed. */ private Runnable mDisableSuppressSelectionChangedRunnable = new Runnable () {

public void run() { mSuppressSelectionChanged = false; selectionChanged(); } }; /** * When fling runnable runs, it resets this to false. Any method along t he * path until the end of its run() can set this to true to abort any * remaining fling. For example, if we've reached either the leftmost or * rightmost item, we will set this to true. */ private boolean mShouldStopFling; /** * The currently selected item's child. */ private View mSelectedChild; /** * Whether to continuously callback on the item selected listener during a * fling. */ private boolean mShouldCallbackDuringFling = true; /** * Whether to callback when an item that is not selected is clicked. */ private boolean mShouldCallbackOnUnselectedItemClick = true; /** * If true, do not callback to item selected listener. */ private boolean mSuppressSelectionChanged; /** * If true, we have received the "invoke" (center or enter buttons) key * down. This is checked before we action on the "invoke" key up, and is * subsequently cleared. */ private boolean mReceivedInvokeKeyDown; private AdapterContextMenuInfo mContextMenuInfo; /** * If true, this onScroll is the first for this user's drag (remember, a * drag sends many onScrolls). */ private boolean mIsFirstScroll; /** * If true the reflection calls failed and this widget will behave * unpredictably if used further */ private boolean mBroken;

public EcoGallery(Context context) { this(context, null); } public EcoGallery(Context context, AttributeSet attrs) { this(context, attrs, R.attr.ecoGalleryStyle); } public EcoGallery(Context context, AttributeSet attrs, int defStyle) { super(context, attrs, defStyle); mBroken = true; mGestureDetector = new GestureDetector(context, this); mGestureDetector.setIsLongpressEnabled(true); TypedArray a = context.obtainStyledAttributes( attrs, R.styleable.EcoGallery, defStyle, 0); int index = a.getInt(R.styleable.EcoGallery_gravity, -1); if (index >= 0) { setGravity(index); } int animationDuration = a.getInt(R.styleable.EcoGallery_animationDuration, -1); if (animationDuration > 0) { setAnimationDuration(animationDuration); } int spacing = a.getDimensionPixelOffset(R.styleable.EcoGallery_spacing, 0) ; setSpacing(spacing); float unselectedAlpha = a.getFloat( R.styleable.EcoGallery_unselectedAlpha, 0.5f); setUnselectedAlpha(unselectedAlpha); a.recycle(); // We draw the selected item last (because otherwise the item to the // right overlaps it) int FLAG_USE_CHILD_DRAWING_ORDER = 0x400; int FLAG_SUPPORT_STATIC_TRANSFORMATIONS = 0x800; Class vgClass = ViewGroup.class; try { Field childDrawingOrder = vgClass .getDeclaredField("FLAG_USE_CHILD_DRAWIN G_ORDER"); Field supportStaticTrans = vgClass .getDeclaredField("FLAG_SUPPORT_STATIC_T RANSFORMATIONS"); childDrawingOrder.setAccessible(true); supportStaticTrans.setAccessible(true); FLAG_USE_CHILD_DRAWING_ORDER = childDrawingOrder.getInt( this);

FLAG_SUPPORT_STATIC_TRANSFORMATIONS = supportStaticTrans .getInt(this); } catch (NoSuchFieldException e) { Log.e(LOG_TAG, e.getMessage(), e); } catch (IllegalAccessException e) { Log.e(LOG_TAG, e.getMessage(), e); } try { // set new group flags Field groupFlags = vgClass.getDeclaredField("mGroupFlags "); groupFlags.setAccessible(true); int groupFlagsValue = groupFlags.getInt(this); groupFlagsValue |= FLAG_USE_CHILD_DRAWING_ORDER; groupFlagsValue |= FLAG_SUPPORT_STATIC_TRANSFORMATIONS; groupFlags.set(this, groupFlagsValue); // working! mBroken = false; } catch (NoSuchFieldException e) { Log.e(LOG_TAG, e.getMessage(), e); } catch (IllegalAccessException e) { Log.e(LOG_TAG, e.getMessage(), e); } } /** * @return Whether the widget is broken or working (functional) */ public boolean isBroken() { return mBroken; } /** * Whether or not to callback on any {@link #getOnItemSelectedListener() } * while the items are being flinged. If false, only the final selected item * will cause the callback. If true, all items between the first and the * final will cause callbacks. * * @param shouldCallback Whether or not to callback on the listener whil e * the items are being flinged. */ public void setCallbackDuringFling(boolean shouldCallback) { mShouldCallbackDuringFling = shouldCallback; } /** * Whether or not to callback when an item that is not selected is click ed. * If false, the item will become selected (and re-centered). If true, t he * {@link #getOnItemClickListener()} will get the callback. * * @param shouldCallback Whether or not to callback on the listener when a

* item that is not selected is clicked. * @hide */ public void setCallbackOnUnselectedItemClick(boolean shouldCallback) { mShouldCallbackOnUnselectedItemClick = shouldCallback; } /** * Sets how long the transition animation should run when a child view * changes position. Only relevant if animation is turned on. * * @param animationDurationMillis The duration of the transition, in * milliseconds. * * @attr ref android.R.styleable#Gallery_animationDuration */ public void setAnimationDuration(int animationDurationMillis) { mAnimationDuration = animationDurationMillis; } /** * Sets the spacing between items in a Gallery * * @param spacing The spacing in pixels between items in the Gallery * * @attr ref android.R.styleable#Gallery_spacing */ public void setSpacing(int spacing) { mSpacing = spacing; } /** * Sets the alpha of items that are not selected in the Gallery. * * @param unselectedAlpha the alpha for the items that are not selected. * * @attr ref android.R.styleable#Gallery_unselectedAlpha */ public void setUnselectedAlpha(float unselectedAlpha) { mUnselectedAlpha = unselectedAlpha; } @Override protected boolean getChildStaticTransformation(View child, Transformatio n t) { t.clear(); t.setAlpha(child == mSelectedChild ? 1.0f : mUnselectedAlpha); return true; } @Override protected int computeHorizontalScrollExtent() { // Only 1 item is considered to be selected return 1; } @Override protected int computeHorizontalScrollOffset() {

// Current scroll position is the same as the selected position return mSelectedPosition; } @Override protected int computeHorizontalScrollRange() { // Scroll range is the same as the item count return mItemCount; } @Override protected boolean checkLayoutParams(ViewGroup.LayoutParams p) { return p instanceof LayoutParams; } @Override protected ViewGroup.LayoutParams generateLayoutParams(ViewGroup.LayoutPa rams p) { return new LayoutParams(p); } @Override public ViewGroup.LayoutParams generateLayoutParams(AttributeSet attrs) { return new LayoutParams(getContext(), attrs); } @Override protected ViewGroup.LayoutParams generateDefaultLayoutParams() { /* * Gallery expects EcoGallery.LayoutParams. */ return new LayoutParams(ViewGroup.LayoutParams.WRAP_CONTENT, ViewGroup.LayoutParams.WRAP_CONTENT); } @Override protected void onLayout(boolean changed, int l, int t, int r, int b) { super.onLayout(changed, l, t, r, b); /* * Remember that we are in layout to prevent more layout request fro m * being generated. */ mInLayout = true; layout(0, false); mInLayout = false; } @Override int getChildHeight(View child) { return child.getMeasuredHeight(); } /** * Tracks a motion scroll. In reality, this is used to do just about any * movement to items (touch scroll, arrow-key scroll, set an item as sel ected). *

* @param deltaX Change in X from the previous event. */ void trackMotionScroll(int deltaX) { if (getChildCount() == 0) { return; } boolean toLeft = deltaX < 0; int limitedDeltaX = getLimitedMotionScrollAmount(toLeft, deltaX); if (limitedDeltaX != deltaX) { // The above call returned a limited amount, so stop any scrolls /flings mFlingRunnable.endFling(false); onFinishedMovement(); } offsetChildrenLeftAndRight(limitedDeltaX); detachOffScreenChildren(toLeft); if (toLeft) { // If moved left, there will be empty space on the right fillToGalleryRight(); } else { // Similarly, empty space on the left fillToGalleryLeft(); } setSelectionToCenterChild(); invalidate(); } int getLimitedMotionScrollAmount(boolean motionToLeft, int deltaX) { int extremeItemPosition = motionToLeft ? mItemCount - 1 : 0; View extremeChild = getChildAt(extremeItemPosition - mFirstPosition) ; if (extremeChild == null) { return deltaX; } int extremeChildCenter = getCenterOfView(extremeChild); int galleryCenter = getCenterOfGallery(); if (motionToLeft) { if (extremeChildCenter <= galleryCenter) { // The extreme child is past his boundary point! return 0; } } else { if (extremeChildCenter >= galleryCenter) { // The extreme child is past his boundary point! return 0; } }

int centerDifference = galleryCenter - extremeChildCenter; return motionToLeft ? Math.max(centerDifference, deltaX) : Math.min(centerDifference, deltaX); } /** * Offset the horizontal location of all children of this view by the * specified number of pixels. * * @param offset the number of pixels to offset */ private void offsetChildrenLeftAndRight(int offset) { for (int i = getChildCount() - 1; i >= 0; i--) { getChildAt(i).offsetLeftAndRight(offset); } } /** * @return The center of this Gallery. */ private int getCenterOfGallery() { int paddingLeft = getPaddingLeft(); return (getWidth() - paddingLeft - getPaddingRight()) / 2 + paddingL eft; } /** * @return The center of the given view. */ private static int getCenterOfView(View view) { return view.getLeft() + view.getWidth() / 2; } /** * Detaches children that are off the screen (i.e.: Gallery bounds). * * @param toLeft Whether to detach children to the left of the Gallery, or * */ private int int int int to the right. void detachOffScreenChildren(boolean toLeft) { numChildren = getChildCount(); firstPosition = mFirstPosition; start = 0; count = 0;

if (toLeft) { final int galleryLeft = getPaddingLeft(); for (int i = 0; i < numChildren; i++) { final View child = getChildAt(i); if (child.getRight() >= galleryLeft) { break; } else { count++; mRecycler.add(firstPosition + i, child); } }

} else { final int galleryRight = getWidth() - getPaddingRight(); for (int i = numChildren - 1; i >= 0; i--) { final View child = getChildAt(i); if (child.getLeft() <= galleryRight) { break; } else { start = i; count++; mRecycler.add(firstPosition + i, child); } } } detachViewsFromParent(start, count); if (toLeft) { mFirstPosition += count; } } /** * Scrolls the items so that the selected item is in its 'slot' (its cen ter * is the gallery's center). */ private void scrollIntoSlots() { if (getChildCount() == 0 || mSelectedChild == null) return; int selectedCenter = getCenterOfView(mSelectedChild); int targetCenter = getCenterOfGallery(); int scrollAmount = targetCenter - selectedCenter; if (scrollAmount != 0) { mFlingRunnable.startUsingDistance(scrollAmount); } else { onFinishedMovement(); } } private void onFinishedMovement() { if (mSuppressSelectionChanged) { mSuppressSelectionChanged = false; // We haven't been callbacking during the fling, so do it now super.selectionChanged(); } invalidate(); } @Override void selectionChanged() { if (!mSuppressSelectionChanged) { super.selectionChanged(); } } /** * Looks for the child that is closest to the center and sets it as the

* selected child. */ private void setSelectionToCenterChild() { View selView = mSelectedChild; if (mSelectedChild == null) return; int galleryCenter = getCenterOfGallery(); // Common case where the current selected position is correct if (selView.getLeft() <= galleryCenter && selView.getRight() >= gall eryCenter) { return; } // TODO better search int closestEdgeDistance = Integer.MAX_VALUE; int newSelectedChildIndex = 0; for (int i = getChildCount() - 1; i >= 0; i--) { View child = getChildAt(i); if (child.getLeft() <= galleryCenter && child.getRight() >= gal leryCenter) { // This child is in the center newSelectedChildIndex = i; break; } int childClosestEdgeDistance = Math.min(Math.abs(child.getLeft() - galleryCenter), Math.abs(child.getRight() - galleryCenter)); if (childClosestEdgeDistance < closestEdgeDistance) { closestEdgeDistance = childClosestEdgeDistance; newSelectedChildIndex = i; } } int newPos = mFirstPosition + newSelectedChildIndex; if (newPos != mSelectedPosition) { setSelectedPositionInt(newPos); setNextSelectedPositionInt(newPos); checkSelectionChanged(); } } /** * Creates and positions all views for this Gallery. * <p> * We layout rarely, most of the time {@link #trackMotionScroll(int)} ta kes * care of repositioning, adding, and removing children. * * @param delta Change in the selected position. +1 means the selection is * 1 * */ means the selection is moving to the left. moving to the right, so views are scrolling to the left. -

@Override void layout(int delta, boolean animate) { int childrenLeft = mSpinnerPadding.left; int childrenWidth = getRight() - getLeft() - mSpinnerPadding.left mSpinnerPadding.right; if (mDataChanged) { handleDataChanged(); } // Handle an empty gallery by removing all views. if (mItemCount == 0) { resetList(); return; } // Update to the new selected position. if (mNextSelectedPosition >= 0) { setSelectedPositionInt(mNextSelectedPosition); } // All views go in recycler while we are in layout recycleAllViews(); // Clear out old views detachAllViewsFromParent(); /* * These will be used to give initial positions to views entering th e * gallery as we scroll */ mRightMost = 0; mLeftMost = 0; // Make selected view and center it /* * mFirstPosition will be decreased as we add views to the left late r * on. The 0 for x will be offset in a couple lines down. */ mFirstPosition = mSelectedPosition; View sel = makeAndAddView(mSelectedPosition, 0, 0, true); // Put the selected child in the center int selectedOffset = childrenLeft + (childrenWidth / 2) - (sel.getWi dth() / 2); sel.offsetLeftAndRight(selectedOffset); fillToGalleryRight(); fillToGalleryLeft(); invalidate(); checkSelectionChanged(); mDataChanged = false; mNeedSync = false; setNextSelectedPositionInt(mSelectedPosition);

updateSelectedItemMetadata(); } private void fillToGalleryLeft() { int itemSpacing = mSpacing; int galleryLeft = getPaddingLeft(); // Set state for initial iteration View prevIterationView = getChildAt(0); int curPosition; int curRightEdge; if (prevIterationView != null) { curPosition = mFirstPosition - 1; curRightEdge = prevIterationView.getLeft() - itemSpacing; } else { // No children available! curPosition = 0; curRightEdge = getRight() - getLeft() - getPaddingRight(); mShouldStopFling = true; } while (curRightEdge > galleryLeft && curPosition >= 0) { prevIterationView = makeAndAddView(curPosition, curPosition - mS electedPosition, curRightEdge, false); // Remember some state mFirstPosition = curPosition; // Set state for next iteration curRightEdge = prevIterationView.getLeft() - itemSpacing; curPosition--; } } private int int int int void fillToGalleryRight() { itemSpacing = mSpacing; galleryRight = getRight() - getLeft() - getPaddingRight(); numChildren = getChildCount(); numItems = mItemCount;

// Set state for initial iteration View prevIterationView = getChildAt(numChildren - 1); int curPosition; int curLeftEdge; if (prevIterationView != null) { curPosition = mFirstPosition + numChildren; curLeftEdge = prevIterationView.getRight() + itemSpacing; } else { mFirstPosition = curPosition = mItemCount - 1; curLeftEdge = getPaddingLeft(); mShouldStopFling = true; } while (curLeftEdge < galleryRight && curPosition < numItems) { prevIterationView = makeAndAddView(curPosition, curPosition - mS electedPosition,

curLeftEdge, true); // Set state for next iteration curLeftEdge = prevIterationView.getRight() + itemSpacing; curPosition++; } } /** * Obtain a view, either by pulling an existing view from the recycler o r by * getting a new one from the adapter. If we are animating, make sure th ere * is enough information in the view's layout parameters to animate from the * * * * * is * n * the fromLeft parameter * @param fromLeft Are we positioning views based on the left edge? (i.e ., * building from left to right)? * @return A view that has been added to the gallery */ private View makeAndAddView(int position, int offset, int x, boolean fromLeft) { View child; child = mRecycler.get(); // pass child as convertview child = mAdapter.getView(position, child, this); // Position the view setUpChild(child, offset, x, fromLeft); return child; } /** * Helper for makeAndAddView to set the position of a view and fill out its * * * * * his * n * the fromLeft paramter * @param fromLeft Are we posiitoning views based on the left edge? (i.e ., * */ building from left to right)? will either be the left or right edge of the view, depending o layout paramters. @param child The view to position @param offset Offset from the selected position @param x X-coordintate indicating where this view should be placed. T will either be the left or right edge of the view, depending o old to new positions. @param position Position in the gallery for the view to obtain @param offset Offset from the selected position @param x X-coordinate indicating where this view should be placed. Th

private void setUpChild(View child, int offset, int x, boolean fromLeft) { // Respect layout params that are already in the view. Otherwise // make some up... LayoutParams lp = (LayoutParams) child.getLayoutParams(); if (lp == null) { lp = (LayoutParams) generateDefaultLayoutParams(); } addViewInLayout(child, fromLeft ? -1 : 0, lp); child.setSelected(offset == 0); // Get measure specs int childHeightSpec = ViewGroup.getChildMeasureSpec(mHeightMeasureSp ec, mSpinnerPadding.top + mSpinnerPadding.bottom, lp.height); int childWidthSpec = ViewGroup.getChildMeasureSpec(mWidthMeasureSpec , mSpinnerPadding.left + mSpinnerPadding.right, lp.width); // Measure child child.measure(childWidthSpec, childHeightSpec); int childLeft; int childRight; // Position vertically based on gravity setting int childTop = calculateTop(child, true); int childBottom = childTop + child.getMeasuredHeight(); int width = child.getMeasuredWidth(); if (fromLeft) { childLeft = x; childRight = childLeft + width; } else { childLeft = x - width; childRight = x; } child.layout(childLeft, childTop, childRight, childBottom); } /** * Figure out vertical placement based on mGravity * * @param child Child to place * @return Where the top of the child should be */ private int calculateTop(View child, boolean duringLayout) { int myHeight = duringLayout ? getMeasuredHeight() : getHeight(); int childHeight = duringLayout ? child.getMeasuredHeight() : child.g etHeight(); int childTop = 0; switch (mGravity) { case Gravity.TOP:

childTop = mSpinnerPadding.top; break; case Gravity.CENTER_VERTICAL: int availableSpace = myHeight - mSpinnerPadding.bottom - mSpinnerPadding.top - childHeight; childTop = mSpinnerPadding.top + (availableSpace / 2); break; case Gravity.BOTTOM: childTop = myHeight - mSpinnerPadding.bottom - childHeight; break; } return childTop; } @Override public boolean onTouchEvent(MotionEvent event) { // Give everything to the gesture detector boolean retValue = mGestureDetector.onTouchEvent(event); int action = event.getAction(); if (action == MotionEvent.ACTION_UP) { // Helper method for lifted finger onUp(); } else if (action == MotionEvent.ACTION_CANCEL) { onCancel(); } return retValue; } /** * {@inheritDoc} */ public boolean onSingleTapUp(MotionEvent e) { if (mDownTouchPosition >= 0) { // An item tap should make it selected, so scroll to this child. scrollToChild(mDownTouchPosition - mFirstPosition); // Also pass the click so the client knows, if it wants to. if (mShouldCallbackOnUnselectedItemClick || mDownTouchPosition = = mSelectedPosition) { performItemClick(mDownTouchView, mDownTouchPosition, mAdapte r .getItemId(mDownTouchPosition)); } return true; } return false; } /** * {@inheritDoc} */ public boolean onFling(MotionEvent e1, MotionEvent e2, float velocityX, float velocityY) {

if (!mShouldCallbackDuringFling) { // We want to suppress selection changes // Remove any future code to set mSuppressSelectionChanged = fal se removeCallbacks(mDisableSuppressSelectionChangedRunnable); // This will get reset once we scroll into slots if (!mSuppressSelectionChanged) mSuppressSelectionChanged = true ; } // Fling the gallery! mFlingRunnable.startUsingVelocity((int) -velocityX); return true; } /** * {@inheritDoc} */ public boolean onScroll(MotionEvent e1, MotionEvent e2, float distanceX, float distanceY) { if (localLOGV) Log.v(TAG, String.valueOf(e2.getX() - e1.getX())); /* * Now's a good time to tell our parent to stop intercepting our eve nts! * The user has moved more than the slop amount, since GestureDetect or * ensures this before calling this method. Also, if a parent is mor e * interested in this touch's events than we are, it would have * intercepted them by now (for example, we can assume when a Galler y is * in the ListView, a vertical scroll would not end up in this metho d * since a ListView would have intercepted it by now). */ getParent().requestDisallowInterceptTouchEvent(true); // As the user scrolls, we want to callback selection changes so rel ated// info on the screen is up-to-date with the gallery's selection if (!mShouldCallbackDuringFling) { if (mIsFirstScroll) { /* * We're not notifying the client of selection changes durin g * the fling, and this scroll could possibly be a fling. Don 't * do selection changes until we're sure it is not a fling. */ if (!mSuppressSelectionChanged) mSuppressSelectionChanged = true; postDelayed(mDisableSuppressSelectionChangedRunnable, SCROLL _TO_FLING_UNCERTAINTY_TIMEOUT); }

} else { if (mSuppressSelectionChanged) mSuppressSelectionChanged = false ; } // Track the motion trackMotionScroll(-1 * (int) distanceX); mIsFirstScroll = false; return true; } /** * {@inheritDoc} */ public boolean onDown(MotionEvent e) { // Kill any existing fling/scroll mFlingRunnable.stop(false); // Get the item's view that was touched mDownTouchPosition = pointToPosition((int) e.getX(), (int) e.getY()) ; if (mDownTouchPosition >= 0) { mDownTouchView = getChildAt(mDownTouchPosition - mFirstPosition) ; mDownTouchView.setPressed(true); } // Reset the multiple-scroll tracking state mIsFirstScroll = true; // Must return true to get matching events for this down event. return true; } /** * Called when a touch event's action is MotionEvent.ACTION_UP. */ void onUp() { if (mFlingRunnable.mScroller.isFinished()) { scrollIntoSlots(); } dispatchUnpress(); } /** * Called when a touch event's action is MotionEvent.ACTION_CANCEL. */ void onCancel() { onUp(); } /** * {@inheritDoc} */ public void onLongPress(MotionEvent e) {

if (mDownTouchPosition < 0) { return; } performHapticFeedback(HapticFeedbackConstants.LONG_PRESS); long id = getItemIdAtPosition(mDownTouchPosition); dispatchLongPress(mDownTouchView, mDownTouchPosition, id); } // Unused methods from GestureDetector.OnGestureListener below /** * {@inheritDoc} */ public void onShowPress(MotionEvent e) { } // Unused methods from GestureDetector.OnGestureListener above private void dispatchPress(View child) { if (child != null) { child.setPressed(true); } setPressed(true); } private void dispatchUnpress() { for (int i = getChildCount() - 1; i >= 0; i--) { getChildAt(i).setPressed(false); } setPressed(false); } @Override public void dispatchSetSelected(boolean selected) { /* * We don't want to pass the selected state given from its parent to its * children since this widget itself has a selected state to give to its * children. */ } @Override protected void dispatchSetPressed(boolean pressed) { // Show the pressed state on the selected child if (mSelectedChild != null) { mSelectedChild.setPressed(pressed); } } @Override protected ContextMenuInfo getContextMenuInfo() {

return mContextMenuInfo; } @Override public boolean showContextMenuForChild(View originalView) { final int longPressPosition = getPositionForView(originalView); if (longPressPosition < 0) { return false; } final long longPressId = mAdapter.getItemId(longPressPosition); return dispatchLongPress(originalView, longPressPosition, longPressI d); } @Override public boolean showContextMenu() { if (isPressed() && mSelectedPosition >= 0) { int index = mSelectedPosition - mFirstPosition; View v = getChildAt(index); return dispatchLongPress(v, mSelectedPosition, mSelectedRowId); } return false; } private boolean dispatchLongPress(View view, int position, long id) { boolean handled = false; if (mOnItemLongClickListener != null) { handled = mOnItemLongClickListener.onItemLongClick(this, mDownTo uchView, mDownTouchPosition, id); } if (!handled) { mContextMenuInfo = new AdapterContextMenuInfo(view, position, id ); handled = super.showContextMenuForChild(this); } if (handled) { performHapticFeedback(HapticFeedbackConstants.LONG_PRESS); } return handled; } @Override public boolean dispatchKeyEvent(KeyEvent event) { // Gallery steals all key events return event.dispatch(this, null, null); } /** * Handles left, right, and clicking * @see android.view.View#onKeyDown */

@Override public boolean onKeyDown(int keyCode, KeyEvent event) { switch (keyCode) { case KeyEvent.KEYCODE_DPAD_LEFT: if (movePrevious()) { playSoundEffect(SoundEffectConstants.NAVIGATION_LEFT); } return true; case KeyEvent.KEYCODE_DPAD_RIGHT: if (moveNext()) { playSoundEffect(SoundEffectConstants.NAVIGATION_RIGHT); } return true; case KeyEvent.KEYCODE_DPAD_CENTER: case KeyEvent.KEYCODE_ENTER: mReceivedInvokeKeyDown = true; // fallthrough to default handling } return super.onKeyDown(keyCode, event); } @Override public boolean onKeyUp(int keyCode, KeyEvent event) { switch (keyCode) { case KeyEvent.KEYCODE_DPAD_CENTER: case KeyEvent.KEYCODE_ENTER: { if (mReceivedInvokeKeyDown) { if (mItemCount > 0) { dispatchPress(mSelectedChild); postDelayed(new Runnable() { public void run() { dispatchUnpress(); } }, ViewConfiguration.getPressedStateDuration()); int selectedIndex = mSelectedPosition - mFirstPosition; performItemClick(getChildAt(selectedIndex), mSelectedPos ition, mAdapter .getItemId(mSelectedPosition)); } } // Clear the flag mReceivedInvokeKeyDown = false; return true; } } return super.onKeyUp(keyCode, event); } boolean movePrevious() { if (mItemCount > 0 && mSelectedPosition > 0) {

scrollToChild(mSelectedPosition - mFirstPosition - 1); return true; } else { return false; } } boolean moveNext() { if (mItemCount > 0 && mSelectedPosition < mItemCount - 1) { scrollToChild(mSelectedPosition - mFirstPosition + 1); return true; } else { return false; } } private boolean scrollToChild(int childPosition) { View child = getChildAt(childPosition); if (child != null) { int distance = getCenterOfGallery() - getCenterOfView(child); mFlingRunnable.startUsingDistance(distance); return true; } return false; } @Override void setSelectedPositionInt(int position) { super.setSelectedPositionInt(position); // Updates any metadata we keep about the selected item. updateSelectedItemMetadata(); } private void updateSelectedItemMetadata() { View oldSelectedChild = mSelectedChild; View child = mSelectedChild = getChildAt(mSelectedPosition - mFirstP osition); if (child == null) { return; } child.setSelected(true); child.setFocusable(true); if (hasFocus()) { child.requestFocus(); } // We unfocus the old child down here so the above hasFocus check // returns true if (oldSelectedChild != null) { // Make sure its drawable state doesn't contain 'selected' oldSelectedChild.setSelected(false);

// Make sure it is not focusable anymore, since otherwise arrow keys // can make this one be focused oldSelectedChild.setFocusable(false); } } /** * Describes how the child views are aligned. * @param gravity * * @attr ref android.R.styleable#Gallery_gravity */ public void setGravity(int gravity) { if (mGravity != gravity) { mGravity = gravity; requestLayout(); } } @Override protected int getChildDrawingOrder(int childCount, int i) { int selectedIndex = mSelectedPosition - mFirstPosition; // Just to be safe if (selectedIndex < 0) return i; if (i == childCount - 1) { // Draw the selected child last return selectedIndex; } else if (i >= selectedIndex) { // Move the children to the right of the selected child earlier one return i + 1; } else { // Keep the children to the left of the selected child the same return i; } } @Override protected void onFocusChanged(boolean gainFocus, int direction, Rect pre viouslyFocusedRect) { super.onFocusChanged(gainFocus, direction, previouslyFocusedRect); /* * The gallery shows focus by focusing the selected item. So, give * focus to our selected item instead. We steal keys from our * selected item elsewhere. */ if (gainFocus && mSelectedChild != null) { mSelectedChild.requestFocus(direction); } } /** * Responsible for fling behavior. Use {@link #startUsingVelocity(int)}

to * initiate a fling. Each frame of the fling is handled in {@link #run() }. * A FlingRunnable will keep re-posting itself until the fling is done. * */ private class FlingRunnable implements Runnable { /** * Tracks the decay of a fling scroll */ private Scroller mScroller; /** * X value reported by mScroller on the previous fling */ private int mLastFlingX; public FlingRunnable() { mScroller = new Scroller(getContext()); } private void startCommon() { // Remove any pending flings removeCallbacks(this); } public void startUsingVelocity(int initialVelocity) { if (initialVelocity == 0) return; startCommon(); int initialX = initialVelocity < 0 ? Integer.MAX_VALUE : 0; mLastFlingX = initialX; mScroller.fling(initialX, 0, initialVelocity, 0, 0, Integer.MAX_VALUE, 0, Integer.MAX_VALUE); post(this); } public void startUsingDistance(int distance) { if (distance == 0) return; startCommon(); mLastFlingX = 0; mScroller.startScroll(0, 0, -distance, 0, mAnimationDuration); post(this); } public void stop(boolean scrollIntoSlots) { removeCallbacks(this); endFling(scrollIntoSlots); } private void endFling(boolean scrollIntoSlots) { /* * Force the scroller's status to finished (without setting its * position to the end) */ mScroller.forceFinished(true);

if (scrollIntoSlots) scrollIntoSlots(); } public void run() { if (mItemCount == 0) { endFling(true); return; } mShouldStopFling = false; final Scroller scroller = mScroller; boolean more = scroller.computeScrollOffset(); final int x = scroller.getCurrX(); // Flip sign to convert finger direction to list items direction // (e.g. finger moving down means list is moving towards the top ) int delta = mLastFlingX - x; // Pretend that each frame of a fling scroll is a touch scroll if (delta > 0) { // Moving towards the left. Use first view as mDownTouchPosi tion mDownTouchPosition = mFirstPosition; // Don't fling more than 1 screen delta = Math.min(getWidth() - getPaddingLeft() - getPaddingR ight() - 1, delta); } else { // Moving towards the right. Use last view as mDownTouchPosi tion int offsetToLast = getChildCount() - 1; mDownTouchPosition = mFirstPosition + offsetToLast; // Don't fling more than 1 screen delta = Math.max(-(getWidth() - getPaddingRight() - getPaddi ngLeft() - 1), delta); } trackMotionScroll(delta); if (more && !mShouldStopFling) { mLastFlingX = x; post(this); } else { endFling(true); } } } /** * Gallery extends LayoutParams to provide a place to hold current * Transformation information along with previous position/transformatio n * info. * */

public static class LayoutParams extends ViewGroup.LayoutParams { public LayoutParams(Context c, AttributeSet attrs) { super(c, attrs); } public LayoutParams(int w, int h) { super(w, h); } public LayoutParams(ViewGroup.LayoutParams source) { super(source); } } } You'll also need to copy `simple_spinner_item` and `simple_spinner_dropdown_item ` from the SDK layout folder into yours. Then to reference it layout XML: <com.blah.EcoGallery xmlns:android="http://schemas.android.com/apk/res/android" xmlns:blah="http://schemas.android.com/apk/res/com.blah" android:layout_width="fill_parent" android:layout_height="fill_parent" blah:spacing="0px" blah:unselectedAlpha="0.5" blah:animationDuration="200" blah:gravity="center_horizontal" />