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. This constructor will result in the underlying data collection being
    156      * immutable, so methods such as {@link #clear()} will throw an exception.
    157      *
    158      * @param context The current context.
    159      * @param resource The resource ID for a layout file containing a TextView to use when
    160      *                 instantiating views.
    161      * @param objects The objects to represent in the ListView.
    162      */
    163     public ArrayAdapter(@NonNull Context context, @LayoutRes int resource, @NonNull T[] objects) {
    164         this(context, resource, 0, Arrays.asList(objects));
    165     }
    166 
    167     /**
    168      * Constructor. This constructor will result in the underlying data collection being
    169      * immutable, so methods such as {@link #clear()} will throw an exception.
    170      *
    171      * @param context The current context.
    172      * @param resource The resource ID for a layout file containing a layout to use when
    173      *                 instantiating views.
    174      * @param textViewResourceId The id of the TextView within the layout resource to be populated
    175      * @param objects The objects to represent in the ListView.
    176      */
    177     public ArrayAdapter(@NonNull Context context, @LayoutRes int resource,
    178             @IdRes int textViewResourceId, @NonNull T[] objects) {
    179         this(context, resource, textViewResourceId, Arrays.asList(objects));
    180     }
    181 
    182     /**
    183      * Constructor
    184      *
    185      * @param context The current context.
    186      * @param resource The resource ID for a layout file containing a TextView to use when
    187      *                 instantiating views.
    188      * @param objects The objects to represent in the ListView.
    189      */
    190     public ArrayAdapter(@NonNull Context context, @LayoutRes int resource,
    191             @NonNull List<T> objects) {
    192         this(context, resource, 0, objects);
    193     }
    194 
    195     /**
    196      * Constructor
    197      *
    198      * @param context The current context.
    199      * @param resource The resource ID for a layout file containing a layout to use when
    200      *                 instantiating views.
    201      * @param textViewResourceId The id of the TextView within the layout resource to be populated
    202      * @param objects The objects to represent in the ListView.
    203      */
    204     public ArrayAdapter(@NonNull Context context, @LayoutRes int resource,
    205             @IdRes int textViewResourceId, @NonNull List<T> objects) {
    206         this(context, resource, textViewResourceId, objects, false);
    207     }
    208 
    209     private ArrayAdapter(@NonNull Context context, @LayoutRes int resource,
    210             @IdRes int textViewResourceId, @NonNull List<T> objects, boolean objsFromResources) {
    211         mContext = context;
    212         mInflater = LayoutInflater.from(context);
    213         mResource = mDropDownResource = resource;
    214         mObjects = objects;
    215         mObjectsFromResources = objsFromResources;
    216         mFieldId = textViewResourceId;
    217     }
    218 
    219     /**
    220      * Adds the specified object at the end of the array.
    221      *
    222      * @param object The object to add at the end of the array.
    223      * @throws UnsupportedOperationException if the underlying data collection is immutable
    224      */
    225     public void add(@Nullable T object) {
    226         synchronized (mLock) {
    227             if (mOriginalValues != null) {
    228                 mOriginalValues.add(object);
    229             } else {
    230                 mObjects.add(object);
    231             }
    232             mObjectsFromResources = false;
    233         }
    234         if (mNotifyOnChange) notifyDataSetChanged();
    235     }
    236 
    237     /**
    238      * Adds the specified Collection at the end of the array.
    239      *
    240      * @param collection The Collection to add at the end of the array.
    241      * @throws UnsupportedOperationException if the <tt>addAll</tt> operation
    242      *         is not supported by this list
    243      * @throws ClassCastException if the class of an element of the specified
    244      *         collection prevents it from being added to this list
    245      * @throws NullPointerException if the specified collection contains one
    246      *         or more null elements and this list does not permit null
    247      *         elements, or if the specified collection is null
    248      * @throws IllegalArgumentException if some property of an element of the
    249      *         specified collection prevents it from being added to this list
    250      */
    251     public void addAll(@NonNull Collection<? extends T> collection) {
    252         synchronized (mLock) {
    253             if (mOriginalValues != null) {
    254                 mOriginalValues.addAll(collection);
    255             } else {
    256                 mObjects.addAll(collection);
    257             }
    258             mObjectsFromResources = false;
    259         }
    260         if (mNotifyOnChange) notifyDataSetChanged();
    261     }
    262 
    263     /**
    264      * Adds the specified items at the end of the array.
    265      *
    266      * @param items The items to add at the end of the array.
    267      * @throws UnsupportedOperationException if the underlying data collection is immutable
    268      */
    269     public void addAll(T ... items) {
    270         synchronized (mLock) {
    271             if (mOriginalValues != null) {
    272                 Collections.addAll(mOriginalValues, items);
    273             } else {
    274                 Collections.addAll(mObjects, items);
    275             }
    276             mObjectsFromResources = false;
    277         }
    278         if (mNotifyOnChange) notifyDataSetChanged();
    279     }
    280 
    281     /**
    282      * Inserts the specified object at the specified index in the array.
    283      *
    284      * @param object The object to insert into the array.
    285      * @param index The index at which the object must be inserted.
    286      * @throws UnsupportedOperationException if the underlying data collection is immutable
    287      */
    288     public void insert(@Nullable T object, int index) {
    289         synchronized (mLock) {
    290             if (mOriginalValues != null) {
    291                 mOriginalValues.add(index, object);
    292             } else {
    293                 mObjects.add(index, object);
    294             }
    295             mObjectsFromResources = false;
    296         }
    297         if (mNotifyOnChange) notifyDataSetChanged();
    298     }
    299 
    300     /**
    301      * Removes the specified object from the array.
    302      *
    303      * @param object The object to remove.
    304      * @throws UnsupportedOperationException if the underlying data collection is immutable
    305      */
    306     public void remove(@Nullable T object) {
    307         synchronized (mLock) {
    308             if (mOriginalValues != null) {
    309                 mOriginalValues.remove(object);
    310             } else {
    311                 mObjects.remove(object);
    312             }
    313             mObjectsFromResources = false;
    314         }
    315         if (mNotifyOnChange) notifyDataSetChanged();
    316     }
    317 
    318     /**
    319      * Remove all elements from the list.
    320      *
    321      * @throws UnsupportedOperationException if the underlying data collection is immutable
    322      */
    323     public void clear() {
    324         synchronized (mLock) {
    325             if (mOriginalValues != null) {
    326                 mOriginalValues.clear();
    327             } else {
    328                 mObjects.clear();
    329             }
    330             mObjectsFromResources = false;
    331         }
    332         if (mNotifyOnChange) notifyDataSetChanged();
    333     }
    334 
    335     /**
    336      * Sorts the content of this adapter using the specified comparator.
    337      *
    338      * @param comparator The comparator used to sort the objects contained
    339      *        in this adapter.
    340      */
    341     public void sort(@NonNull Comparator<? super T> comparator) {
    342         synchronized (mLock) {
    343             if (mOriginalValues != null) {
    344                 Collections.sort(mOriginalValues, comparator);
    345             } else {
    346                 Collections.sort(mObjects, comparator);
    347             }
    348         }
    349         if (mNotifyOnChange) notifyDataSetChanged();
    350     }
    351 
    352     @Override
    353     public void notifyDataSetChanged() {
    354         super.notifyDataSetChanged();
    355         mNotifyOnChange = true;
    356     }
    357 
    358     /**
    359      * Control whether methods that change the list ({@link #add}, {@link #addAll(Collection)},
    360      * {@link #addAll(Object[])}, {@link #insert}, {@link #remove}, {@link #clear},
    361      * {@link #sort(Comparator)}) automatically call {@link #notifyDataSetChanged}.  If set to
    362      * false, caller must manually call notifyDataSetChanged() to have the changes
    363      * reflected in the attached view.
    364      *
    365      * The default is true, and calling notifyDataSetChanged()
    366      * resets the flag to true.
    367      *
    368      * @param notifyOnChange if true, modifications to the list will
    369      *                       automatically call {@link
    370      *                       #notifyDataSetChanged}
    371      */
    372     public void setNotifyOnChange(boolean notifyOnChange) {
    373         mNotifyOnChange = notifyOnChange;
    374     }
    375 
    376     /**
    377      * Returns the context associated with this array adapter. The context is used
    378      * to create views from the resource passed to the constructor.
    379      *
    380      * @return The Context associated with this adapter.
    381      */
    382     public @NonNull Context getContext() {
    383         return mContext;
    384     }
    385 
    386     @Override
    387     public int getCount() {
    388         return mObjects.size();
    389     }
    390 
    391     @Override
    392     public @Nullable T getItem(int position) {
    393         return mObjects.get(position);
    394     }
    395 
    396     /**
    397      * Returns the position of the specified item in the array.
    398      *
    399      * @param item The item to retrieve the position of.
    400      *
    401      * @return The position of the specified item.
    402      */
    403     public int getPosition(@Nullable T item) {
    404         return mObjects.indexOf(item);
    405     }
    406 
    407     @Override
    408     public long getItemId(int position) {
    409         return position;
    410     }
    411 
    412     @Override
    413     public @NonNull View getView(int position, @Nullable View convertView,
    414             @NonNull ViewGroup parent) {
    415         return createViewFromResource(mInflater, position, convertView, parent, mResource);
    416     }
    417 
    418     private @NonNull View createViewFromResource(@NonNull LayoutInflater inflater, int position,
    419             @Nullable View convertView, @NonNull ViewGroup parent, int resource) {
    420         final View view;
    421         final TextView text;
    422 
    423         if (convertView == null) {
    424             view = inflater.inflate(resource, parent, false);
    425         } else {
    426             view = convertView;
    427         }
    428 
    429         try {
    430             if (mFieldId == 0) {
    431                 //  If no custom field is assigned, assume the whole resource is a TextView
    432                 text = (TextView) view;
    433             } else {
    434                 //  Otherwise, find the TextView field within the layout
    435                 text = view.findViewById(mFieldId);
    436 
    437                 if (text == null) {
    438                     throw new RuntimeException("Failed to find view with ID "
    439                             + mContext.getResources().getResourceName(mFieldId)
    440                             + " in item layout");
    441                 }
    442             }
    443         } catch (ClassCastException e) {
    444             Log.e("ArrayAdapter", "You must supply a resource ID for a TextView");
    445             throw new IllegalStateException(
    446                     "ArrayAdapter requires the resource ID to be a TextView", e);
    447         }
    448 
    449         final T item = getItem(position);
    450         if (item instanceof CharSequence) {
    451             text.setText((CharSequence) item);
    452         } else {
    453             text.setText(item.toString());
    454         }
    455 
    456         return view;
    457     }
    458 
    459     /**
    460      * <p>Sets the layout resource to create the drop down views.</p>
    461      *
    462      * @param resource the layout resource defining the drop down views
    463      * @see #getDropDownView(int, android.view.View, android.view.ViewGroup)
    464      */
    465     public void setDropDownViewResource(@LayoutRes int resource) {
    466         this.mDropDownResource = resource;
    467     }
    468 
    469     /**
    470      * Sets the {@link Resources.Theme} against which drop-down views are
    471      * inflated.
    472      * <p>
    473      * By default, drop-down views are inflated against the theme of the
    474      * {@link Context} passed to the adapter's constructor.
    475      *
    476      * @param theme the theme against which to inflate drop-down views or
    477      *              {@code null} to use the theme from the adapter's context
    478      * @see #getDropDownView(int, View, ViewGroup)
    479      */
    480     @Override
    481     public void setDropDownViewTheme(@Nullable Resources.Theme theme) {
    482         if (theme == null) {
    483             mDropDownInflater = null;
    484         } else if (theme == mInflater.getContext().getTheme()) {
    485             mDropDownInflater = mInflater;
    486         } else {
    487             final Context context = new ContextThemeWrapper(mContext, theme);
    488             mDropDownInflater = LayoutInflater.from(context);
    489         }
    490     }
    491 
    492     @Override
    493     public @Nullable Resources.Theme getDropDownViewTheme() {
    494         return mDropDownInflater == null ? null : mDropDownInflater.getContext().getTheme();
    495     }
    496 
    497     @Override
    498     public View getDropDownView(int position, @Nullable View convertView,
    499             @NonNull ViewGroup parent) {
    500         final LayoutInflater inflater = mDropDownInflater == null ? mInflater : mDropDownInflater;
    501         return createViewFromResource(inflater, position, convertView, parent, mDropDownResource);
    502     }
    503 
    504     /**
    505      * Creates a new ArrayAdapter from external resources. The content of the array is
    506      * obtained through {@link android.content.res.Resources#getTextArray(int)}.
    507      *
    508      * @param context The application's environment.
    509      * @param textArrayResId The identifier of the array to use as the data source.
    510      * @param textViewResId The identifier of the layout used to create views.
    511      *
    512      * @return An ArrayAdapter<CharSequence>.
    513      */
    514     public static @NonNull ArrayAdapter<CharSequence> createFromResource(@NonNull Context context,
    515             @ArrayRes int textArrayResId, @LayoutRes int textViewResId) {
    516         final CharSequence[] strings = context.getResources().getTextArray(textArrayResId);
    517         return new ArrayAdapter<>(context, textViewResId, 0, Arrays.asList(strings), true);
    518     }
    519 
    520     @Override
    521     public @NonNull Filter getFilter() {
    522         if (mFilter == null) {
    523             mFilter = new ArrayFilter();
    524         }
    525         return mFilter;
    526     }
    527 
    528     /**
    529      * {@inheritDoc}
    530      *
    531      * @return values from the string array used by {@link #createFromResource(Context, int, int)},
    532      * or {@code null} if object was created otherwsie or if contents were dynamically changed after
    533      * creation.
    534      */
    535     @Override
    536     public CharSequence[] getAutofillOptions() {
    537         // First check if app developer explicitly set them.
    538         final CharSequence[] explicitOptions = super.getAutofillOptions();
    539         if (explicitOptions != null) {
    540             return explicitOptions;
    541         }
    542 
    543         // Otherwise, only return options that came from static resources.
    544         if (!mObjectsFromResources || mObjects == null || mObjects.isEmpty()) {
    545             return null;
    546         }
    547         final int size = mObjects.size();
    548         final CharSequence[] options = new CharSequence[size];
    549         mObjects.toArray(options);
    550         return options;
    551     }
    552 
    553     /**
    554      * <p>An array filter constrains the content of the array adapter with
    555      * a prefix. Each item that does not start with the supplied prefix
    556      * is removed from the list.</p>
    557      */
    558     private class ArrayFilter extends Filter {
    559         @Override
    560         protected FilterResults performFiltering(CharSequence prefix) {
    561             final FilterResults results = new FilterResults();
    562 
    563             if (mOriginalValues == null) {
    564                 synchronized (mLock) {
    565                     mOriginalValues = new ArrayList<>(mObjects);
    566                 }
    567             }
    568 
    569             if (prefix == null || prefix.length() == 0) {
    570                 final ArrayList<T> list;
    571                 synchronized (mLock) {
    572                     list = new ArrayList<>(mOriginalValues);
    573                 }
    574                 results.values = list;
    575                 results.count = list.size();
    576             } else {
    577                 final String prefixString = prefix.toString().toLowerCase();
    578 
    579                 final ArrayList<T> values;
    580                 synchronized (mLock) {
    581                     values = new ArrayList<>(mOriginalValues);
    582                 }
    583 
    584                 final int count = values.size();
    585                 final ArrayList<T> newValues = new ArrayList<>();
    586 
    587                 for (int i = 0; i < count; i++) {
    588                     final T value = values.get(i);
    589                     final String valueText = value.toString().toLowerCase();
    590 
    591                     // First match against the whole, non-splitted value
    592                     if (valueText.startsWith(prefixString)) {
    593                         newValues.add(value);
    594                     } else {
    595                         final String[] words = valueText.split(" ");
    596                         for (String word : words) {
    597                             if (word.startsWith(prefixString)) {
    598                                 newValues.add(value);
    599                                 break;
    600                             }
    601                         }
    602                     }
    603                 }
    604 
    605                 results.values = newValues;
    606                 results.count = newValues.size();
    607             }
    608 
    609             return results;
    610         }
    611 
    612         @Override
    613         protected void publishResults(CharSequence constraint, FilterResults results) {
    614             //noinspection unchecked
    615             mObjects = (List<T>) results.values;
    616             if (results.count > 0) {
    617                 notifyDataSetChanged();
    618             } else {
    619                 notifyDataSetInvalidated();
    620             }
    621         }
    622     }
    623 }
    624