• 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.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      */
ArrayAdapter(@onNull Context context, @LayoutRes int resource)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      */
ArrayAdapter(@onNull Context context, @LayoutRes int resource, @IdRes int textViewResourceId)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      */
ArrayAdapter(@onNull Context context, @LayoutRes int resource, @NonNull T[] objects)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      */
ArrayAdapter(@onNull Context context, @LayoutRes int resource, @IdRes int textViewResourceId, @NonNull T[] objects)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      */
ArrayAdapter(@onNull Context context, @LayoutRes int resource, @NonNull List<T> objects)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      */
ArrayAdapter(@onNull Context context, @LayoutRes int resource, @IdRes int textViewResourceId, @NonNull List<T> objects)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 
ArrayAdapter(@onNull Context context, @LayoutRes int resource, @IdRes int textViewResourceId, @NonNull List<T> objects, boolean objsFromResources)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      */
add(@ullable T object)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      */
addAll(@onNull Collection<? extends T> collection)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      */
addAll(T .... items)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      */
insert(@ullable T object, int index)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      */
remove(@ullable T object)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      */
clear()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      */
sort(@onNull Comparator<? super T> comparator)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
notifyDataSetChanged()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      */
setNotifyOnChange(boolean notifyOnChange)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      */
getContext()374     public @NonNull Context getContext() {
375         return mContext;
376     }
377 
378     @Override
getCount()379     public int getCount() {
380         return mObjects.size();
381     }
382 
383     @Override
getItem(int position)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      */
getPosition(@ullable T item)395     public int getPosition(@Nullable T item) {
396         return mObjects.indexOf(item);
397     }
398 
399     @Override
getItemId(int position)400     public long getItemId(int position) {
401         return position;
402     }
403 
404     @Override
getView(int position, @Nullable View convertView, @NonNull ViewGroup parent)405     public @NonNull View getView(int position, @Nullable View convertView,
406             @NonNull ViewGroup parent) {
407         return createViewFromResource(mInflater, position, convertView, parent, mResource);
408     }
409 
createViewFromResource(@onNull LayoutInflater inflater, int position, @Nullable View convertView, @NonNull ViewGroup parent, int resource)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      */
setDropDownViewResource(@ayoutRes int resource)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
setDropDownViewTheme(@ullable Resources.Theme theme)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
getDropDownViewTheme()485     public @Nullable Resources.Theme getDropDownViewTheme() {
486         return mDropDownInflater == null ? null : mDropDownInflater.getContext().getTheme();
487     }
488 
489     @Override
getDropDownView(int position, @Nullable View convertView, @NonNull ViewGroup parent)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      */
createFromResource(@onNull Context context, @ArrayRes int textArrayResId, @LayoutRes int textViewResId)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
getFilter()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
getAutofillOptions()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
performFiltering(CharSequence prefix)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
publishResults(CharSequence constraint, FilterResults results)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