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