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