package com.aviary.android.feather.widget; import java.util.ArrayList; import java.util.Arrays; import java.util.Collection; import java.util.Collections; import java.util.Comparator; import java.util.List; import android.content.Context; import android.util.Log; import android.view.LayoutInflater; import android.view.View; import android.view.ViewGroup; import android.widget.ArrayAdapter; import android.widget.Filter; import android.widget.Filterable; import android.widget.TextView; public class ArrayAdapterExtended<T> extends BaseAdapterExtended implements Filterable { /** * Contains the list of objects that represent the data of this ArrayAdapter. The content of this list is referred to as * "the array" in the documentation. */ private List<T> mObjects; /** * Lock used to modify the content of {@link #mObjects}. Any write operation performed on the array should be synchronized on * this lock. This lock is also used by the filter (see {@link #getFilter()} to make a synchronized copy of the original array of * data. */ private final Object mLock = new Object(); /** * The resource indicating what views to inflate to display the content of this array adapter. */ private int mResource; /** * The resource indicating what views to inflate to display the content of this array adapter in a drop down widget. */ private int mDropDownResource; /** * If the inflated resource is not a TextView, {@link #mFieldId} is used to find a TextView inside the inflated views hierarchy. * This field must contain the identifier that matches the one defined in the resource file. */ private int mFieldId = 0; /** * Indicates whether or not {@link #notifyDataSetChanged()} must be called whenever {@link #mObjects} is modified. */ private boolean mNotifyOnChange = true; private Context mContext; // A copy of the original mObjects array, initialized from and then used instead as soon as // the mFilter ArrayFilter is used. mObjects will then only contain the filtered values. private ArrayList<T> mOriginalValues; private ArrayFilter mFilter; private LayoutInflater mInflater; /** * Constructor * * @param context * The current context. * @param textViewResourceId * The resource ID for a layout file containing a TextView to use when instantiating views. */ public ArrayAdapterExtended( Context context, int textViewResourceId ) { init( context, textViewResourceId, 0, new ArrayList<T>() ); } /** * Constructor * * @param context * The current context. * @param resource * The resource ID for a layout file containing a layout to use when instantiating views. * @param textViewResourceId * The id of the TextView within the layout resource to be populated */ public ArrayAdapterExtended( Context context, int resource, int textViewResourceId ) { init( context, resource, textViewResourceId, new ArrayList<T>() ); } /** * Constructor * * @param context * The current context. * @param textViewResourceId * The resource ID for a layout file containing a TextView to use when instantiating views. * @param objects * The objects to represent in the ListView. */ public ArrayAdapterExtended( Context context, int textViewResourceId, T[] objects ) { init( context, textViewResourceId, 0, Arrays.asList( objects ) ); } /** * Constructor * * @param context * The current context. * @param resource * The resource ID for a layout file containing a layout to use when instantiating views. * @param textViewResourceId * The id of the TextView within the layout resource to be populated * @param objects * The objects to represent in the ListView. */ public ArrayAdapterExtended( Context context, int resource, int textViewResourceId, T[] objects ) { init( context, resource, textViewResourceId, Arrays.asList( objects ) ); } /** * Constructor * * @param context * The current context. * @param textViewResourceId * The resource ID for a layout file containing a TextView to use when instantiating views. * @param objects * The objects to represent in the ListView. */ public ArrayAdapterExtended( Context context, int textViewResourceId, List<T> objects ) { init( context, textViewResourceId, 0, objects ); } /** * Constructor * * @param context * The current context. * @param resource * The resource ID for a layout file containing a layout to use when instantiating views. * @param textViewResourceId * The id of the TextView within the layout resource to be populated * @param objects * The objects to represent in the ListView. */ public ArrayAdapterExtended( Context context, int resource, int textViewResourceId, List<T> objects ) { init( context, resource, textViewResourceId, objects ); } /** * Adds the specified object at the end of the array. * * @param object * The object to add at the end of the array. */ public void add( T object ) { synchronized ( mLock ) { if ( mOriginalValues != null ) { mOriginalValues.add( object ); } else { mObjects.add( object ); } } if ( mNotifyOnChange ) notifyDataSetAdded(); } /** * Adds the specified Collection at the end of the array. * * @param collection * The Collection to add at the end of the array. */ public void addAll( Collection<? extends T> collection ) { synchronized ( mLock ) { if ( mOriginalValues != null ) { mOriginalValues.addAll( collection ); } else { mObjects.addAll( collection ); } } if ( mNotifyOnChange ) notifyDataSetAdded(); } /** * Adds the specified items at the end of the array. * * @param items * The items to add at the end of the array. */ public void addAll( T... items ) { synchronized ( mLock ) { if ( mOriginalValues != null ) { Collections.addAll( mOriginalValues, items ); } else { Collections.addAll( mObjects, items ); } } if ( mNotifyOnChange ) notifyDataSetAdded(); } /** * Inserts the specified object at the specified index in the array. * * @param object * The object to insert into the array. * @param index * The index at which the object must be inserted. */ public void insert( T object, int index ) { synchronized ( mLock ) { if ( mOriginalValues != null ) { mOriginalValues.add( index, object ); } else { mObjects.add( index, object ); } } if ( mNotifyOnChange ) notifyDataSetChanged(); } /** * Removes the specified object from the array. * * @param object * The object to remove. */ public void remove( T object ) { synchronized ( mLock ) { if ( mOriginalValues != null ) { mOriginalValues.remove( object ); } else { mObjects.remove( object ); } } if ( mNotifyOnChange ) notifyDataSetRemoved(); } /** * Remove all elements from the list. */ public void clear() { synchronized ( mLock ) { if ( mOriginalValues != null ) { mOriginalValues.clear(); } else { mObjects.clear(); } } if ( mNotifyOnChange ) notifyDataSetChanged(); } /** * Sorts the content of this adapter using the specified comparator. * * @param comparator * The comparator used to sort the objects contained in this adapter. */ public void sort( Comparator<? super T> comparator ) { synchronized ( mLock ) { if ( mOriginalValues != null ) { Collections.sort( mOriginalValues, comparator ); } else { Collections.sort( mObjects, comparator ); } } if ( mNotifyOnChange ) notifyDataSetChanged(); } /** * {@inheritDoc} */ @Override public void notifyDataSetChanged() { super.notifyDataSetChanged(); mNotifyOnChange = true; } /** * Control whether methods that change the list ({@link #add}, {@link #insert}, {@link #remove}, {@link #clear}) automatically * call {@link #notifyDataSetChanged}. If set to false, caller must manually call notifyDataSetChanged() to have the changes * reflected in the attached view. * * The default is true, and calling notifyDataSetChanged() resets the flag to true. * * @param notifyOnChange * if true, modifications to the list will automatically call {@link #notifyDataSetChanged} */ public void setNotifyOnChange( boolean notifyOnChange ) { mNotifyOnChange = notifyOnChange; } private void init( Context context, int resource, int textViewResourceId, List<T> objects ) { mContext = context; mInflater = (LayoutInflater) context.getSystemService( Context.LAYOUT_INFLATER_SERVICE ); mResource = mDropDownResource = resource; mObjects = objects; mFieldId = textViewResourceId; } /** * Returns the context associated with this array adapter. The context is used to create views from the resource passed to the * constructor. * * @return The Context associated with this adapter. */ public Context getContext() { return mContext; } /** * {@inheritDoc} */ public int getCount() { return mObjects.size(); } /** * {@inheritDoc} */ public T getItem( int position ) { return mObjects.get( position ); } /** * Returns the position of the specified item in the array. * * @param item * The item to retrieve the position of. * * @return The position of the specified item. */ public int getPosition( T item ) { return mObjects.indexOf( item ); } /** * {@inheritDoc} */ public long getItemId( int position ) { return position; } /** * {@inheritDoc} */ public View getView( int position, View convertView, ViewGroup parent ) { return createViewFromResource( position, convertView, parent, mResource ); } private View createViewFromResource( int position, View convertView, ViewGroup parent, int resource ) { View view; TextView text; if ( convertView == null ) { view = mInflater.inflate( resource, parent, false ); } else { view = convertView; } try { if ( mFieldId == 0 ) { // If no custom field is assigned, assume the whole resource is a TextView text = (TextView) view; } else { // Otherwise, find the TextView field within the layout text = (TextView) view.findViewById( mFieldId ); } } catch ( ClassCastException e ) { Log.e( "ArrayAdapter", "You must supply a resource ID for a TextView" ); throw new IllegalStateException( "ArrayAdapter requires the resource ID to be a TextView", e ); } T item = getItem( position ); if ( item instanceof CharSequence ) { text.setText( (CharSequence) item ); } else { text.setText( item.toString() ); } return view; } /** * <p> * Sets the layout resource to create the drop down views. * </p> * * @param resource * the layout resource defining the drop down views * @see #getDropDownView(int, android.view.View, android.view.ViewGroup) */ public void setDropDownViewResource( int resource ) { this.mDropDownResource = resource; } /** * {@inheritDoc} */ @Override public View getDropDownView( int position, View convertView, ViewGroup parent ) { return createViewFromResource( position, convertView, parent, mDropDownResource ); } /** * Creates a new ArrayAdapter from external resources. The content of the array is obtained through * {@link android.content.res.Resources#getTextArray(int)}. * * @param context * The application's environment. * @param textArrayResId * The identifier of the array to use as the data source. * @param textViewResId * The identifier of the layout used to create views. * * @return An ArrayAdapter<CharSequence>. */ public static ArrayAdapterExtended<CharSequence> createFromResource( Context context, int textArrayResId, int textViewResId ) { CharSequence[] strings = context.getResources().getTextArray( textArrayResId ); return new ArrayAdapterExtended<CharSequence>( context, textViewResId, strings ); } /** * {@inheritDoc} */ public Filter getFilter() { if ( mFilter == null ) { mFilter = new ArrayFilter(); } return mFilter; } /** * <p> * An array filter constrains the content of the array adapter with a prefix. Each item that does not start with the supplied * prefix is removed from the list. * </p> */ private class ArrayFilter extends Filter { @Override protected FilterResults performFiltering( CharSequence prefix ) { FilterResults results = new FilterResults(); if ( mOriginalValues == null ) { synchronized ( mLock ) { mOriginalValues = new ArrayList<T>( mObjects ); } } if ( prefix == null || prefix.length() == 0 ) { ArrayList<T> list; synchronized ( mLock ) { list = new ArrayList<T>( mOriginalValues ); } results.values = list; results.count = list.size(); } else { String prefixString = prefix.toString().toLowerCase(); ArrayList<T> values; synchronized ( mLock ) { values = new ArrayList<T>( mOriginalValues ); } final int count = values.size(); final ArrayList<T> newValues = new ArrayList<T>(); for ( int i = 0; i < count; i++ ) { final T value = values.get( i ); final String valueText = value.toString().toLowerCase(); // First match against the whole, non-splitted value if ( valueText.startsWith( prefixString ) ) { newValues.add( value ); } else { final String[] words = valueText.split( " " ); final int wordCount = words.length; // Start at index 0, in case valueText starts with space(s) for ( int k = 0; k < wordCount; k++ ) { if ( words[k].startsWith( prefixString ) ) { newValues.add( value ); break; } } } } results.values = newValues; results.count = newValues.size(); } return results; } @Override protected void publishResults( CharSequence constraint, FilterResults results ) { // noinspection unchecked mObjects = (List<T>) results.values; if ( results.count > 0 ) { notifyDataSetChanged(); } else { notifyDataSetInvalidated(); } } } }