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.compat.annotation.UnsupportedAppUsage; 25 import android.content.Context; 26 import android.content.res.Resources; 27 import android.util.Log; 28 import android.view.ContextThemeWrapper; 29 import android.view.LayoutInflater; 30 import android.view.View; 31 import android.view.ViewGroup; 32 33 import java.util.ArrayList; 34 import java.util.Arrays; 35 import java.util.Collection; 36 import java.util.Collections; 37 import java.util.Comparator; 38 import java.util.List; 39 40 /** 41 * You can use this adapter to provide views for an {@link AdapterView}, 42 * Returns a view for each object in a collection of data objects you 43 * provide, and can be used with list-based user interface widgets such as 44 * {@link ListView} or {@link Spinner}. 45 * <p> 46 * By default, the array adapter creates a view by calling {@link Object#toString()} on each 47 * data object in the collection you provide, and places the result in a TextView. 48 * You may also customize what type of view is used for the data object in the collection. 49 * To customize what type of view is used for the data object, 50 * override {@link #getView(int, View, ViewGroup)} 51 * and inflate a view resource. 52 * </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 */ 71 public 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 */ ArrayAdapter(@onNull Context context, @LayoutRes int resource)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 */ ArrayAdapter(@onNull Context context, @LayoutRes int resource, @IdRes int textViewResourceId)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 */ ArrayAdapter(@onNull Context context, @LayoutRes int resource, @NonNull T[] objects)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 */ ArrayAdapter(@onNull Context context, @LayoutRes int resource, @IdRes int textViewResourceId, @NonNull T[] objects)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 */ ArrayAdapter(@onNull Context context, @LayoutRes int resource, @NonNull List<T> objects)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 */ ArrayAdapter(@onNull Context context, @LayoutRes int resource, @IdRes int textViewResourceId, @NonNull List<T> objects)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 ArrayAdapter(@onNull Context context, @LayoutRes int resource, @IdRes int textViewResourceId, @NonNull List<T> objects, boolean objsFromResources)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 */ add(@ullable T object)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 */ addAll(@onNull Collection<? extends T> collection)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 */ addAll(T .... items)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 */ insert(@ullable T object, int index)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 */ remove(@ullable T object)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 */ clear()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 */ sort(@onNull Comparator<? super T> comparator)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 notifyDataSetChanged()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 */ setNotifyOnChange(boolean notifyOnChange)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 */ getContext()383 public @NonNull Context getContext() { 384 return mContext; 385 } 386 387 @Override getCount()388 public int getCount() { 389 return mObjects.size(); 390 } 391 392 @Override getItem(int position)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 */ getPosition(@ullable T item)404 public int getPosition(@Nullable T item) { 405 return mObjects.indexOf(item); 406 } 407 408 @Override getItemId(int position)409 public long getItemId(int position) { 410 return position; 411 } 412 413 @Override getView(int position, @Nullable View convertView, @NonNull ViewGroup parent)414 public @NonNull View getView(int position, @Nullable View convertView, 415 @NonNull ViewGroup parent) { 416 return createViewFromResource(mInflater, position, convertView, parent, mResource); 417 } 418 createViewFromResource(@onNull LayoutInflater inflater, int position, @Nullable View convertView, @NonNull ViewGroup parent, int resource)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 */ setDropDownViewResource(@ayoutRes int resource)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 setDropDownViewTheme(@ullable Resources.Theme theme)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 getDropDownViewTheme()494 public @Nullable Resources.Theme getDropDownViewTheme() { 495 return mDropDownInflater == null ? null : mDropDownInflater.getContext().getTheme(); 496 } 497 498 @Override getDropDownView(int position, @Nullable View convertView, @NonNull ViewGroup parent)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 */ createFromResource(@onNull Context context, @ArrayRes int textArrayResId, @LayoutRes int textViewResId)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 getFilter()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 getAutofillOptions()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 performFiltering(CharSequence prefix)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 publishResults(CharSequence constraint, FilterResults results)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 } 625