• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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      */
ArrayAdapter(@onNull Context context, @LayoutRes int resource)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      */
ArrayAdapter(@onNull Context context, @LayoutRes int resource, @IdRes int textViewResourceId)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      */
ArrayAdapter(@onNull Context context, @LayoutRes int resource, @NonNull T[] objects)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      */
ArrayAdapter(@onNull Context context, @LayoutRes int resource, @IdRes int textViewResourceId, @NonNull T[] objects)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      */
ArrayAdapter(@onNull Context context, @LayoutRes int resource, @NonNull List<T> objects)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      */
ArrayAdapter(@onNull Context context, @LayoutRes int resource, @IdRes int textViewResourceId, @NonNull List<T> objects)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 
ArrayAdapter(@onNull Context context, @LayoutRes int resource, @IdRes int textViewResourceId, @NonNull List<T> objects, boolean objsFromResources)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      */
add(@ullable T object)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      */
addAll(@onNull Collection<? extends T> collection)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      */
addAll(T .... items)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      */
insert(@ullable T object, int index)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      */
remove(@ullable T object)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      */
clear()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      */
sort(@onNull Comparator<? super T> comparator)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
notifyDataSetChanged()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      */
setNotifyOnChange(boolean notifyOnChange)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      */
getContext()386     public @NonNull Context getContext() {
387         return mContext;
388     }
389 
390     @Override
getCount()391     public int getCount() {
392         return mObjects.size();
393     }
394 
395     @Override
getItem(int position)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      */
getPosition(@ullable T item)407     public int getPosition(@Nullable T item) {
408         return mObjects.indexOf(item);
409     }
410 
411     @Override
getItemId(int position)412     public long getItemId(int position) {
413         return position;
414     }
415 
416     @Override
getView(int position, @Nullable View convertView, @NonNull ViewGroup parent)417     public @NonNull View getView(int position, @Nullable View convertView,
418             @NonNull ViewGroup parent) {
419         return createViewFromResource(mInflater, position, convertView, parent, mResource);
420     }
421 
createViewFromResource(@onNull LayoutInflater inflater, int position, @Nullable View convertView, @NonNull ViewGroup parent, int resource)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      */
setDropDownViewResource(@ayoutRes int resource)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
setDropDownViewTheme(@ullable Resources.Theme theme)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
getDropDownViewTheme()497     public @Nullable Resources.Theme getDropDownViewTheme() {
498         return mDropDownInflater == null ? null : mDropDownInflater.getContext().getTheme();
499     }
500 
501     @Override
getDropDownView(int position, @Nullable View convertView, @NonNull ViewGroup parent)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      */
createFromResource(@onNull Context context, @ArrayRes int textArrayResId, @LayoutRes int textViewResId)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
getFilter()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
getAutofillOptions()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
performFiltering(CharSequence prefix)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
publishResults(CharSequence constraint, FilterResults results)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