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 */ 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. 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 */ 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 */ 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 */ 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 */ 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 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 */ 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 */ 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 */ 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 */ 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 */ 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 */ 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 */ 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 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 */ 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 */ 382 public @NonNull Context getContext() { 383 return mContext; 384 } 385 386 @Override 387 public int getCount() { 388 return mObjects.size(); 389 } 390 391 @Override 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 */ 403 public int getPosition(@Nullable T item) { 404 return mObjects.indexOf(item); 405 } 406 407 @Override 408 public long getItemId(int position) { 409 return position; 410 } 411 412 @Override 413 public @NonNull View getView(int position, @Nullable View convertView, 414 @NonNull ViewGroup parent) { 415 return createViewFromResource(mInflater, position, convertView, parent, mResource); 416 } 417 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 */ 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 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 493 public @Nullable Resources.Theme getDropDownViewTheme() { 494 return mDropDownInflater == null ? null : mDropDownInflater.getContext().getTheme(); 495 } 496 497 @Override 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 */ 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 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 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 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 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