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
17package android.widget;
18
19import android.annotation.ArrayRes;
20import android.annotation.IdRes;
21import android.annotation.LayoutRes;
22import android.annotation.NonNull;
23import android.annotation.Nullable;
24import android.content.Context;
25import android.content.res.Resources;
26import android.util.Log;
27import android.view.ContextThemeWrapper;
28import android.view.LayoutInflater;
29import android.view.View;
30import android.view.ViewGroup;
31
32import java.util.ArrayList;
33import java.util.Arrays;
34import java.util.Collection;
35import java.util.Collections;
36import java.util.Comparator;
37import 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 */
73public class ArrayAdapter<T> extends BaseAdapter implements Filterable, ThemedSpinnerAdapter {
74    /**
75     * Lock used to modify the content of {@link #mObjects}. Any write operation
76     * performed on the array should be synchronized on this lock. This lock is also
77     * used by the filter (see {@link #getFilter()} to make a synchronized copy of
78     * the original array of data.
79     */
80    private final Object mLock = new Object();
81
82    private final LayoutInflater mInflater;
83
84    private final Context mContext;
85
86    /**
87     * The resource indicating what views to inflate to display the content of this
88     * array adapter.
89     */
90    private final int mResource;
91
92    /**
93     * The resource indicating what views to inflate to display the content of this
94     * array adapter in a drop down widget.
95     */
96    private int mDropDownResource;
97
98    /**
99     * Contains the list of objects that represent the data of this ArrayAdapter.
100     * The content of this list is referred to as "the array" in the documentation.
101     */
102    private List<T> mObjects;
103
104    /**
105     * Indicates whether the contents of {@link #mObjects} came from static resources.
106     */
107    private boolean mObjectsFromResources;
108
109    /**
110     * If the inflated resource is not a TextView, {@code mFieldId} is used to find
111     * a TextView inside the inflated views hierarchy. This field must contain the
112     * identifier that matches the one defined in the resource file.
113     */
114    private int mFieldId = 0;
115
116    /**
117     * Indicates whether or not {@link #notifyDataSetChanged()} must be called whenever
118     * {@link #mObjects} is modified.
119     */
120    private boolean mNotifyOnChange = true;
121
122    // A copy of the original mObjects array, initialized from and then used instead as soon as
123    // the mFilter ArrayFilter is used. mObjects will then only contain the filtered values.
124    private ArrayList<T> mOriginalValues;
125    private ArrayFilter mFilter;
126
127    /** Layout inflater used for {@link #getDropDownView(int, View, ViewGroup)}. */
128    private LayoutInflater mDropDownInflater;
129
130    /**
131     * Constructor
132     *
133     * @param context The current context.
134     * @param resource The resource ID for a layout file containing a TextView to use when
135     *                 instantiating views.
136     */
137    public ArrayAdapter(@NonNull Context context, @LayoutRes int resource) {
138        this(context, resource, 0, new ArrayList<>());
139    }
140
141    /**
142     * Constructor
143     *
144     * @param context The current context.
145     * @param resource The resource ID for a layout file containing a layout to use when
146     *                 instantiating views.
147     * @param textViewResourceId The id of the TextView within the layout resource to be populated
148     */
149    public ArrayAdapter(@NonNull Context context, @LayoutRes int resource,
150            @IdRes int textViewResourceId) {
151        this(context, resource, textViewResourceId, new ArrayList<>());
152    }
153
154    /**
155     * Constructor
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     */
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     */
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     */
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     */
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
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     */
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     */
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     */
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     */
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     */
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     */
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     */
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
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     */
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     */
374    public @NonNull Context getContext() {
375        return mContext;
376    }
377
378    @Override
379    public int getCount() {
380        return mObjects.size();
381    }
382
383    @Override
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     */
395    public int getPosition(@Nullable T item) {
396        return mObjects.indexOf(item);
397    }
398
399    @Override
400    public long getItemId(int position) {
401        return position;
402    }
403
404    @Override
405    public @NonNull View getView(int position, @Nullable View convertView,
406            @NonNull ViewGroup parent) {
407        return createViewFromResource(mInflater, position, convertView, parent, mResource);
408    }
409
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     */
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
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
485    public @Nullable Resources.Theme getDropDownViewTheme() {
486        return mDropDownInflater == null ? null : mDropDownInflater.getContext().getTheme();
487    }
488
489    @Override
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     */
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
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
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
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
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