Home | History | Annotate | Download | only in widget
      1 /*
      2  * Copyright (C) 2006 The Android Open Source Project
      3  *
      4  * Licensed under the Apache License, Version 2.0 (the "License");
      5  * you may not use this file except in compliance with the License.
      6  * You may obtain a copy of the License at
      7  *
      8  *      http://www.apache.org/licenses/LICENSE-2.0
      9  *
     10  * Unless required by applicable law or agreed to in writing, software
     11  * distributed under the License is distributed on an "AS IS" BASIS,
     12  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
     13  * See the License for the specific language governing permissions and
     14  * limitations under the License.
     15  */
     16 
     17 package android.widget;
     18 
     19 import android.annotation.ArrayRes;
     20 import android.annotation.IdRes;
     21 import android.annotation.LayoutRes;
     22 import android.annotation.NonNull;
     23 import android.annotation.Nullable;
     24 import android.annotation.UnsupportedAppUsage;
     25 import android.content.Context;
     26 import android.content.res.Resources;
     27 import android.util.Log;
     28 import android.view.ContextThemeWrapper;
     29 import android.view.LayoutInflater;
     30 import android.view.View;
     31 import android.view.ViewGroup;
     32 
     33 import java.util.ArrayList;
     34 import java.util.Arrays;
     35 import java.util.Collection;
     36 import java.util.Collections;
     37 import java.util.Comparator;
     38 import java.util.List;
     39 
     40 /**
     41  * You can use this adapter to provide views for an {@link AdapterView},
     42  * Returns a view for each object in a collection of data objects you
     43  * provide, and can be used with list-based user interface widgets such as
     44  * {@link ListView} or {@link Spinner}.
     45  * <p>
     46  * By default, the array adapter creates a view by calling {@link Object#toString()} on each
     47  * data object in the collection you provide, and places the result in a TextView.
     48  * You may also customize what type of view is used for the data object in the collection.
     49  * To customize what type of view is used for the data object,
     50  * override {@link #getView(int, View, ViewGroup)}
     51  * and inflate a view resource.
     52  * For a code example, see
     53  * the <a href="https://developer.android.com/samples/CustomChoiceList/index.html">
     54  * CustomChoiceList</a> sample.
     55  * </p>
     56  * <p>
     57  * For an example of using an array adapter with a ListView, see the
     58  * <a href="{@docRoot}guide/topics/ui/declaring-layout.html#AdapterViews">
     59  * Adapter Views</a> guide.
     60  * </p>
     61  * <p>
     62  * For an example of using an array adapter with a Spinner, see the
     63  * <a href="{@docRoot}guide/topics/ui/controls/spinner.html">Spinners</a> guide.
     64  * </p>
     65  * <p class="note"><strong>Note:</strong>
     66  * If you are considering using array adapter with a ListView, consider using
     67  * {@link android.support.v7.widget.RecyclerView} instead.
     68  * RecyclerView offers similar features with better performance and more flexibility than
     69  * ListView provides.
     70  * See the
     71  * <a href="https://developer.android.com/guide/topics/ui/layout/recyclerview.html">
     72  * Recycler View</a> guide.</p>
     73  */
     74 public class ArrayAdapter<T> extends BaseAdapter implements Filterable, ThemedSpinnerAdapter {
     75     /**
     76      * Lock used to modify the content of {@link #mObjects}. Any write operation
     77      * performed on the array should be synchronized on this lock. This lock is also
     78      * used by the filter (see {@link #getFilter()} to make a synchronized copy of
     79      * the original array of data.
     80      */
     81     @UnsupportedAppUsage
     82     private final Object mLock = new Object();
     83 
     84     private final LayoutInflater mInflater;
     85 
     86     private final Context mContext;
     87 
     88     /**
     89      * The resource indicating what views to inflate to display the content of this
     90      * array adapter.
     91      */
     92     private final int mResource;
     93 
     94     /**
     95      * The resource indicating what views to inflate to display the content of this
     96      * array adapter in a drop down widget.
     97      */
     98     private int mDropDownResource;
     99 
    100     /**
    101      * Contains the list of objects that represent the data of this ArrayAdapter.
    102      * The content of this list is referred to as "the array" in the documentation.
    103      */
    104     @UnsupportedAppUsage
    105     private List<T> mObjects;
    106 
    107     /**
    108      * Indicates whether the contents of {@link #mObjects} came from static resources.
    109      */
    110     private boolean mObjectsFromResources;
    111 
    112     /**
    113      * If the inflated resource is not a TextView, {@code mFieldId} is used to find
    114      * a TextView inside the inflated views hierarchy. This field must contain the
    115      * identifier that matches the one defined in the resource file.
    116      */
    117     private int mFieldId = 0;
    118 
    119     /**
    120      * Indicates whether or not {@link #notifyDataSetChanged()} must be called whenever
    121      * {@link #mObjects} is modified.
    122      */
    123     private boolean mNotifyOnChange = true;
    124 
    125     // A copy of the original mObjects array, initialized from and then used instead as soon as
    126     // the mFilter ArrayFilter is used. mObjects will then only contain the filtered values.
    127     @UnsupportedAppUsage
    128     private ArrayList<T> mOriginalValues;
    129     private ArrayFilter mFilter;
    130 
    131     /** Layout inflater used for {@link #getDropDownView(int, View, ViewGroup)}. */
    132     private LayoutInflater mDropDownInflater;
    133 
    134     /**
    135      * Constructor
    136      *
    137      * @param context The current context.
    138      * @param resource The resource ID for a layout file containing a TextView to use when
    139      *                 instantiating views.
    140      */
    141     public ArrayAdapter(@NonNull Context context, @LayoutRes int resource) {
    142         this(context, resource, 0, new ArrayList<>());
    143     }
    144 
    145     /**
    146      * Constructor
    147      *
    148      * @param context The current context.
    149      * @param resource The resource ID for a layout file containing a layout to use when
    150      *                 instantiating views.
    151      * @param textViewResourceId The id of the TextView within the layout resource to be populated
    152      */
    153     public ArrayAdapter(@NonNull Context context, @LayoutRes int resource,
    154             @IdRes int textViewResourceId) {
    155         this(context, resource, textViewResourceId, new ArrayList<>());
    156     }
    157 
    158     /**
    159      * Constructor. This constructor will result in the underlying data collection being
    160      * immutable, so methods such as {@link #clear()} will throw an exception.
    161      *
    162      * @param context The current context.
    163      * @param resource The resource ID for a layout file containing a TextView to use when
    164      *                 instantiating views.
    165      * @param objects The objects to represent in the ListView.
    166      */
    167     public ArrayAdapter(@NonNull Context context, @LayoutRes int resource, @NonNull T[] objects) {
    168         this(context, resource, 0, Arrays.asList(objects));
    169     }
    170 
    171     /**
    172      * Constructor. This constructor will result in the underlying data collection being
    173      * immutable, so methods such as {@link #clear()} will throw an exception.
    174      *
    175      * @param context The current context.
    176      * @param resource The resource ID for a layout file containing a layout to use when
    177      *                 instantiating views.
    178      * @param textViewResourceId The id of the TextView within the layout resource to be populated
    179      * @param objects The objects to represent in the ListView.
    180      */
    181     public ArrayAdapter(@NonNull Context context, @LayoutRes int resource,
    182             @IdRes int textViewResourceId, @NonNull T[] objects) {
    183         this(context, resource, textViewResourceId, Arrays.asList(objects));
    184     }
    185 
    186     /**
    187      * Constructor
    188      *
    189      * @param context The current context.
    190      * @param resource The resource ID for a layout file containing a TextView to use when
    191      *                 instantiating views.
    192      * @param objects The objects to represent in the ListView.
    193      */
    194     public ArrayAdapter(@NonNull Context context, @LayoutRes int resource,
    195             @NonNull List<T> objects) {
    196         this(context, resource, 0, objects);
    197     }
    198 
    199     /**
    200      * Constructor
    201      *
    202      * @param context The current context.
    203      * @param resource The resource ID for a layout file containing a layout to use when
    204      *                 instantiating views.
    205      * @param textViewResourceId The id of the TextView within the layout resource to be populated
    206      * @param objects The objects to represent in the ListView.
    207      */
    208     public ArrayAdapter(@NonNull Context context, @LayoutRes int resource,
    209             @IdRes int textViewResourceId, @NonNull List<T> objects) {
    210         this(context, resource, textViewResourceId, objects, false);
    211     }
    212 
    213     private ArrayAdapter(@NonNull Context context, @LayoutRes int resource,
    214             @IdRes int textViewResourceId, @NonNull List<T> objects, boolean objsFromResources) {
    215         mContext = context;
    216         mInflater = LayoutInflater.from(context);
    217         mResource = mDropDownResource = resource;
    218         mObjects = objects;
    219         mObjectsFromResources = objsFromResources;
    220         mFieldId = textViewResourceId;
    221     }
    222 
    223     /**
    224      * Adds the specified object at the end of the array.
    225      *
    226      * @param object The object to add at the end of the array.
    227      * @throws UnsupportedOperationException if the underlying data collection is immutable
    228      */
    229     public void add(@Nullable T object) {
    230         synchronized (mLock) {
    231             if (mOriginalValues != null) {
    232                 mOriginalValues.add(object);
    233             } else {
    234                 mObjects.add(object);
    235             }
    236             mObjectsFromResources = false;
    237         }
    238         if (mNotifyOnChange) notifyDataSetChanged();
    239     }
    240 
    241     /**
    242      * Adds the specified Collection at the end of the array.
    243      *
    244      * @param collection The Collection to add at the end of the array.
    245      * @throws UnsupportedOperationException if the <tt>addAll</tt> operation
    246      *         is not supported by this list
    247      * @throws ClassCastException if the class of an element of the specified
    248      *         collection prevents it from being added to this list
    249      * @throws NullPointerException if the specified collection contains one
    250      *         or more null elements and this list does not permit null
    251      *         elements, or if the specified collection is null
    252      * @throws IllegalArgumentException if some property of an element of the
    253      *         specified collection prevents it from being added to this list
    254      */
    255     public void addAll(@NonNull Collection<? extends T> collection) {
    256         synchronized (mLock) {
    257             if (mOriginalValues != null) {
    258                 mOriginalValues.addAll(collection);
    259             } else {
    260                 mObjects.addAll(collection);
    261             }
    262             mObjectsFromResources = false;
    263         }
    264         if (mNotifyOnChange) notifyDataSetChanged();
    265     }
    266 
    267     /**
    268      * Adds the specified items at the end of the array.
    269      *
    270      * @param items The items to add at the end of the array.
    271      * @throws UnsupportedOperationException if the underlying data collection is immutable
    272      */
    273     public void addAll(T ... items) {
    274         synchronized (mLock) {
    275             if (mOriginalValues != null) {
    276                 Collections.addAll(mOriginalValues, items);
    277             } else {
    278                 Collections.addAll(mObjects, items);
    279             }
    280             mObjectsFromResources = false;
    281         }
    282         if (mNotifyOnChange) notifyDataSetChanged();
    283     }
    284 
    285     /**
    286      * Inserts the specified object at the specified index in the array.
    287      *
    288      * @param object The object to insert into the array.
    289      * @param index The index at which the object must be inserted.
    290      * @throws UnsupportedOperationException if the underlying data collection is immutable
    291      */
    292     public void insert(@Nullable T object, int index) {
    293         synchronized (mLock) {
    294             if (mOriginalValues != null) {
    295                 mOriginalValues.add(index, object);
    296             } else {
    297                 mObjects.add(index, object);
    298             }
    299             mObjectsFromResources = false;
    300         }
    301         if (mNotifyOnChange) notifyDataSetChanged();
    302     }
    303 
    304     /**
    305      * Removes the specified object from the array.
    306      *
    307      * @param object The object to remove.
    308      * @throws UnsupportedOperationException if the underlying data collection is immutable
    309      */
    310     public void remove(@Nullable T object) {
    311         synchronized (mLock) {
    312             if (mOriginalValues != null) {
    313                 mOriginalValues.remove(object);
    314             } else {
    315                 mObjects.remove(object);
    316             }
    317             mObjectsFromResources = false;
    318         }
    319         if (mNotifyOnChange) notifyDataSetChanged();
    320     }
    321 
    322     /**
    323      * Remove all elements from the list.
    324      *
    325      * @throws UnsupportedOperationException if the underlying data collection is immutable
    326      */
    327     public void clear() {
    328         synchronized (mLock) {
    329             if (mOriginalValues != null) {
    330                 mOriginalValues.clear();
    331             } else {
    332                 mObjects.clear();
    333             }
    334             mObjectsFromResources = false;
    335         }
    336         if (mNotifyOnChange) notifyDataSetChanged();
    337     }
    338 
    339     /**
    340      * Sorts the content of this adapter using the specified comparator.
    341      *
    342      * @param comparator The comparator used to sort the objects contained
    343      *        in this adapter.
    344      */
    345     public void sort(@NonNull Comparator<? super T> comparator) {
    346         synchronized (mLock) {
    347             if (mOriginalValues != null) {
    348                 Collections.sort(mOriginalValues, comparator);
    349             } else {
    350                 Collections.sort(mObjects, comparator);
    351             }
    352         }
    353         if (mNotifyOnChange) notifyDataSetChanged();
    354     }
    355 
    356     @Override
    357     public void notifyDataSetChanged() {
    358         super.notifyDataSetChanged();
    359         mNotifyOnChange = true;
    360     }
    361 
    362     /**
    363      * Control whether methods that change the list ({@link #add}, {@link #addAll(Collection)},
    364      * {@link #addAll(Object[])}, {@link #insert}, {@link #remove}, {@link #clear},
    365      * {@link #sort(Comparator)}) automatically call {@link #notifyDataSetChanged}.  If set to
    366      * false, caller must manually call notifyDataSetChanged() to have the changes
    367      * reflected in the attached view.
    368      *
    369      * The default is true, and calling notifyDataSetChanged()
    370      * resets the flag to true.
    371      *
    372      * @param notifyOnChange if true, modifications to the list will
    373      *                       automatically call {@link
    374      *                       #notifyDataSetChanged}
    375      */
    376     public void setNotifyOnChange(boolean notifyOnChange) {
    377         mNotifyOnChange = notifyOnChange;
    378     }
    379 
    380     /**
    381      * Returns the context associated with this array adapter. The context is used
    382      * to create views from the resource passed to the constructor.
    383      *
    384      * @return The Context associated with this adapter.
    385      */
    386     public @NonNull Context getContext() {
    387         return mContext;
    388     }
    389 
    390     @Override
    391     public int getCount() {
    392         return mObjects.size();
    393     }
    394 
    395     @Override
    396     public @Nullable T getItem(int position) {
    397         return mObjects.get(position);
    398     }
    399 
    400     /**
    401      * Returns the position of the specified item in the array.
    402      *
    403      * @param item The item to retrieve the position of.
    404      *
    405      * @return The position of the specified item.
    406      */
    407     public int getPosition(@Nullable T item) {
    408         return mObjects.indexOf(item);
    409     }
    410 
    411     @Override
    412     public long getItemId(int position) {
    413         return position;
    414     }
    415 
    416     @Override
    417     public @NonNull View getView(int position, @Nullable View convertView,
    418             @NonNull ViewGroup parent) {
    419         return createViewFromResource(mInflater, position, convertView, parent, mResource);
    420     }
    421 
    422     private @NonNull View createViewFromResource(@NonNull LayoutInflater inflater, int position,
    423             @Nullable View convertView, @NonNull ViewGroup parent, int resource) {
    424         final View view;
    425         final TextView text;
    426 
    427         if (convertView == null) {
    428             view = inflater.inflate(resource, parent, false);
    429         } else {
    430             view = convertView;
    431         }
    432 
    433         try {
    434             if (mFieldId == 0) {
    435                 //  If no custom field is assigned, assume the whole resource is a TextView
    436                 text = (TextView) view;
    437             } else {
    438                 //  Otherwise, find the TextView field within the layout
    439                 text = view.findViewById(mFieldId);
    440 
    441                 if (text == null) {
    442                     throw new RuntimeException("Failed to find view with ID "
    443                             + mContext.getResources().getResourceName(mFieldId)
    444                             + " in item layout");
    445                 }
    446             }
    447         } catch (ClassCastException e) {
    448             Log.e("ArrayAdapter", "You must supply a resource ID for a TextView");
    449             throw new IllegalStateException(
    450                     "ArrayAdapter requires the resource ID to be a TextView", e);
    451         }
    452 
    453         final T item = getItem(position);
    454         if (item instanceof CharSequence) {
    455             text.setText((CharSequence) item);
    456         } else {
    457             text.setText(item.toString());
    458         }
    459 
    460         return view;
    461     }
    462 
    463     /**
    464      * <p>Sets the layout resource to create the drop down views.</p>
    465      *
    466      * @param resource the layout resource defining the drop down views
    467      * @see #getDropDownView(int, android.view.View, android.view.ViewGroup)
    468      */
    469     public void setDropDownViewResource(@LayoutRes int resource) {
    470         this.mDropDownResource = resource;
    471     }
    472 
    473     /**
    474      * Sets the {@link Resources.Theme} against which drop-down views are
    475      * inflated.
    476      * <p>
    477      * By default, drop-down views are inflated against the theme of the
    478      * {@link Context} passed to the adapter's constructor.
    479      *
    480      * @param theme the theme against which to inflate drop-down views or
    481      *              {@code null} to use the theme from the adapter's context
    482      * @see #getDropDownView(int, View, ViewGroup)
    483      */
    484     @Override
    485     public void setDropDownViewTheme(@Nullable Resources.Theme theme) {
    486         if (theme == null) {
    487             mDropDownInflater = null;
    488         } else if (theme == mInflater.getContext().getTheme()) {
    489             mDropDownInflater = mInflater;
    490         } else {
    491             final Context context = new ContextThemeWrapper(mContext, theme);
    492             mDropDownInflater = LayoutInflater.from(context);
    493         }
    494     }
    495 
    496     @Override
    497     public @Nullable Resources.Theme getDropDownViewTheme() {
    498         return mDropDownInflater == null ? null : mDropDownInflater.getContext().getTheme();
    499     }
    500 
    501     @Override
    502     public View getDropDownView(int position, @Nullable View convertView,
    503             @NonNull ViewGroup parent) {
    504         final LayoutInflater inflater = mDropDownInflater == null ? mInflater : mDropDownInflater;
    505         return createViewFromResource(inflater, position, convertView, parent, mDropDownResource);
    506     }
    507 
    508     /**
    509      * Creates a new ArrayAdapter from external resources. The content of the array is
    510      * obtained through {@link android.content.res.Resources#getTextArray(int)}.
    511      *
    512      * @param context The application's environment.
    513      * @param textArrayResId The identifier of the array to use as the data source.
    514      * @param textViewResId The identifier of the layout used to create views.
    515      *
    516      * @return An ArrayAdapter<CharSequence>.
    517      */
    518     public static @NonNull ArrayAdapter<CharSequence> createFromResource(@NonNull Context context,
    519             @ArrayRes int textArrayResId, @LayoutRes int textViewResId) {
    520         final CharSequence[] strings = context.getResources().getTextArray(textArrayResId);
    521         return new ArrayAdapter<>(context, textViewResId, 0, Arrays.asList(strings), true);
    522     }
    523 
    524     @Override
    525     public @NonNull Filter getFilter() {
    526         if (mFilter == null) {
    527             mFilter = new ArrayFilter();
    528         }
    529         return mFilter;
    530     }
    531 
    532     /**
    533      * {@inheritDoc}
    534      *
    535      * @return values from the string array used by {@link #createFromResource(Context, int, int)},
    536      * or {@code null} if object was created otherwsie or if contents were dynamically changed after
    537      * creation.
    538      */
    539     @Override
    540     public CharSequence[] getAutofillOptions() {
    541         // First check if app developer explicitly set them.
    542         final CharSequence[] explicitOptions = super.getAutofillOptions();
    543         if (explicitOptions != null) {
    544             return explicitOptions;
    545         }
    546 
    547         // Otherwise, only return options that came from static resources.
    548         if (!mObjectsFromResources || mObjects == null || mObjects.isEmpty()) {
    549             return null;
    550         }
    551         final int size = mObjects.size();
    552         final CharSequence[] options = new CharSequence[size];
    553         mObjects.toArray(options);
    554         return options;
    555     }
    556 
    557     /**
    558      * <p>An array filter constrains the content of the array adapter with
    559      * a prefix. Each item that does not start with the supplied prefix
    560      * is removed from the list.</p>
    561      */
    562     private class ArrayFilter extends Filter {
    563         @Override
    564         protected FilterResults performFiltering(CharSequence prefix) {
    565             final FilterResults results = new FilterResults();
    566 
    567             if (mOriginalValues == null) {
    568                 synchronized (mLock) {
    569                     mOriginalValues = new ArrayList<>(mObjects);
    570                 }
    571             }
    572 
    573             if (prefix == null || prefix.length() == 0) {
    574                 final ArrayList<T> list;
    575                 synchronized (mLock) {
    576                     list = new ArrayList<>(mOriginalValues);
    577                 }
    578                 results.values = list;
    579                 results.count = list.size();
    580             } else {
    581                 final String prefixString = prefix.toString().toLowerCase();
    582 
    583                 final ArrayList<T> values;
    584                 synchronized (mLock) {
    585                     values = new ArrayList<>(mOriginalValues);
    586                 }
    587 
    588                 final int count = values.size();
    589                 final ArrayList<T> newValues = new ArrayList<>();
    590 
    591                 for (int i = 0; i < count; i++) {
    592                     final T value = values.get(i);
    593                     final String valueText = value.toString().toLowerCase();
    594 
    595                     // First match against the whole, non-splitted value
    596                     if (valueText.startsWith(prefixString)) {
    597                         newValues.add(value);
    598                     } else {
    599                         final String[] words = valueText.split(" ");
    600                         for (String word : words) {
    601                             if (word.startsWith(prefixString)) {
    602                                 newValues.add(value);
    603                                 break;
    604                             }
    605                         }
    606                     }
    607                 }
    608 
    609                 results.values = newValues;
    610                 results.count = newValues.size();
    611             }
    612 
    613             return results;
    614         }
    615 
    616         @Override
    617         protected void publishResults(CharSequence constraint, FilterResults results) {
    618             //noinspection unchecked
    619             mObjects = (List<T>) results.values;
    620             if (results.count > 0) {
    621                 notifyDataSetChanged();
    622             } else {
    623                 notifyDataSetInvalidated();
    624             }
    625         }
    626     }
    627 }
    628