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