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