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