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