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 * A concrete BaseAdapter that is backed by an array of arbitrary 41 * objects. By default this class expects that the provided resource id references 42 * a single TextView. If you want to use a more complex layout, use the constructors that 43 * also takes a field id. That field id should reference a TextView in the larger layout 44 * resource. 45 * 46 * <p>However the TextView is referenced, it will be filled with the toString() of each object in 47 * the array. You can add lists or arrays of custom objects. Override the toString() method 48 * of your objects to determine what text will be displayed for the item in the list. 49 * 50 * <p>To use something other than TextViews for the array display, for instance, ImageViews, 51 * or to have some of data besides toString() results fill the views, 52 * override {@link #getView(int, View, ViewGroup)} to return the type of view you want. 53 */ 54public class ArrayAdapter<T> extends BaseAdapter implements Filterable, ThemedSpinnerAdapter { 55 /** 56 * Lock used to modify the content of {@link #mObjects}. Any write operation 57 * performed on the array should be synchronized on this lock. This lock is also 58 * used by the filter (see {@link #getFilter()} to make a synchronized copy of 59 * the original array of data. 60 */ 61 private final Object mLock = new Object(); 62 63 private final LayoutInflater mInflater; 64 65 private final Context mContext; 66 67 /** 68 * The resource indicating what views to inflate to display the content of this 69 * array adapter. 70 */ 71 private final int mResource; 72 73 /** 74 * The resource indicating what views to inflate to display the content of this 75 * array adapter in a drop down widget. 76 */ 77 private int mDropDownResource; 78 79 /** 80 * Contains the list of objects that represent the data of this ArrayAdapter. 81 * The content of this list is referred to as "the array" in the documentation. 82 */ 83 private List<T> mObjects; 84 85 /** 86 * If the inflated resource is not a TextView, {@code mFieldId} is used to find 87 * a TextView inside the inflated views hierarchy. This field must contain the 88 * identifier that matches the one defined in the resource file. 89 */ 90 private int mFieldId = 0; 91 92 /** 93 * Indicates whether or not {@link #notifyDataSetChanged()} must be called whenever 94 * {@link #mObjects} is modified. 95 */ 96 private boolean mNotifyOnChange = true; 97 98 // A copy of the original mObjects array, initialized from and then used instead as soon as 99 // the mFilter ArrayFilter is used. mObjects will then only contain the filtered values. 100 private ArrayList<T> mOriginalValues; 101 private ArrayFilter mFilter; 102 103 /** Layout inflater used for {@link #getDropDownView(int, View, ViewGroup)}. */ 104 private LayoutInflater mDropDownInflater; 105 106 /** 107 * Constructor 108 * 109 * @param context The current context. 110 * @param resource The resource ID for a layout file containing a TextView to use when 111 * instantiating views. 112 */ 113 public ArrayAdapter(@NonNull Context context, @LayoutRes int resource) { 114 this(context, resource, 0, new ArrayList<>()); 115 } 116 117 /** 118 * Constructor 119 * 120 * @param context The current context. 121 * @param resource The resource ID for a layout file containing a layout to use when 122 * instantiating views. 123 * @param textViewResourceId The id of the TextView within the layout resource to be populated 124 */ 125 public ArrayAdapter(@NonNull Context context, @LayoutRes int resource, 126 @IdRes int textViewResourceId) { 127 this(context, resource, textViewResourceId, new ArrayList<>()); 128 } 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 * @param objects The objects to represent in the ListView. 137 */ 138 public ArrayAdapter(@NonNull Context context, @LayoutRes int resource, @NonNull T[] objects) { 139 this(context, resource, 0, Arrays.asList(objects)); 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 * @param objects The objects to represent in the ListView. 150 */ 151 public ArrayAdapter(@NonNull Context context, @LayoutRes int resource, 152 @IdRes int textViewResourceId, @NonNull T[] objects) { 153 this(context, resource, textViewResourceId, Arrays.asList(objects)); 154 } 155 156 /** 157 * Constructor 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, 165 @NonNull List<T> objects) { 166 this(context, resource, 0, objects); 167 } 168 169 /** 170 * Constructor 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 List<T> objects) { 180 mContext = context; 181 mInflater = LayoutInflater.from(context); 182 mResource = mDropDownResource = resource; 183 mObjects = objects; 184 mFieldId = textViewResourceId; 185 } 186 187 /** 188 * Adds the specified object at the end of the array. 189 * 190 * @param object The object to add at the end of the array. 191 */ 192 public void add(@Nullable T object) { 193 synchronized (mLock) { 194 if (mOriginalValues != null) { 195 mOriginalValues.add(object); 196 } else { 197 mObjects.add(object); 198 } 199 } 200 if (mNotifyOnChange) notifyDataSetChanged(); 201 } 202 203 /** 204 * Adds the specified Collection at the end of the array. 205 * 206 * @param collection The Collection to add at the end of the array. 207 * @throws UnsupportedOperationException if the <tt>addAll</tt> operation 208 * is not supported by this list 209 * @throws ClassCastException if the class of an element of the specified 210 * collection prevents it from being added to this list 211 * @throws NullPointerException if the specified collection contains one 212 * or more null elements and this list does not permit null 213 * elements, or if the specified collection is null 214 * @throws IllegalArgumentException if some property of an element of the 215 * specified collection prevents it from being added to this list 216 */ 217 public void addAll(@NonNull Collection<? extends T> collection) { 218 synchronized (mLock) { 219 if (mOriginalValues != null) { 220 mOriginalValues.addAll(collection); 221 } else { 222 mObjects.addAll(collection); 223 } 224 } 225 if (mNotifyOnChange) notifyDataSetChanged(); 226 } 227 228 /** 229 * Adds the specified items at the end of the array. 230 * 231 * @param items The items to add at the end of the array. 232 */ 233 public void addAll(T ... items) { 234 synchronized (mLock) { 235 if (mOriginalValues != null) { 236 Collections.addAll(mOriginalValues, items); 237 } else { 238 Collections.addAll(mObjects, items); 239 } 240 } 241 if (mNotifyOnChange) notifyDataSetChanged(); 242 } 243 244 /** 245 * Inserts the specified object at the specified index in the array. 246 * 247 * @param object The object to insert into the array. 248 * @param index The index at which the object must be inserted. 249 */ 250 public void insert(@Nullable T object, int index) { 251 synchronized (mLock) { 252 if (mOriginalValues != null) { 253 mOriginalValues.add(index, object); 254 } else { 255 mObjects.add(index, object); 256 } 257 } 258 if (mNotifyOnChange) notifyDataSetChanged(); 259 } 260 261 /** 262 * Removes the specified object from the array. 263 * 264 * @param object The object to remove. 265 */ 266 public void remove(@Nullable T object) { 267 synchronized (mLock) { 268 if (mOriginalValues != null) { 269 mOriginalValues.remove(object); 270 } else { 271 mObjects.remove(object); 272 } 273 } 274 if (mNotifyOnChange) notifyDataSetChanged(); 275 } 276 277 /** 278 * Remove all elements from the list. 279 */ 280 public void clear() { 281 synchronized (mLock) { 282 if (mOriginalValues != null) { 283 mOriginalValues.clear(); 284 } else { 285 mObjects.clear(); 286 } 287 } 288 if (mNotifyOnChange) notifyDataSetChanged(); 289 } 290 291 /** 292 * Sorts the content of this adapter using the specified comparator. 293 * 294 * @param comparator The comparator used to sort the objects contained 295 * in this adapter. 296 */ 297 public void sort(@NonNull Comparator<? super T> comparator) { 298 synchronized (mLock) { 299 if (mOriginalValues != null) { 300 Collections.sort(mOriginalValues, comparator); 301 } else { 302 Collections.sort(mObjects, comparator); 303 } 304 } 305 if (mNotifyOnChange) notifyDataSetChanged(); 306 } 307 308 @Override 309 public void notifyDataSetChanged() { 310 super.notifyDataSetChanged(); 311 mNotifyOnChange = true; 312 } 313 314 /** 315 * Control whether methods that change the list ({@link #add}, 316 * {@link #insert}, {@link #remove}, {@link #clear}) automatically call 317 * {@link #notifyDataSetChanged}. If set to false, caller must 318 * manually call notifyDataSetChanged() to have the changes 319 * reflected in the attached view. 320 * 321 * The default is true, and calling notifyDataSetChanged() 322 * resets the flag to true. 323 * 324 * @param notifyOnChange if true, modifications to the list will 325 * automatically call {@link 326 * #notifyDataSetChanged} 327 */ 328 public void setNotifyOnChange(boolean notifyOnChange) { 329 mNotifyOnChange = notifyOnChange; 330 } 331 332 /** 333 * Returns the context associated with this array adapter. The context is used 334 * to create views from the resource passed to the constructor. 335 * 336 * @return The Context associated with this adapter. 337 */ 338 public @NonNull Context getContext() { 339 return mContext; 340 } 341 342 @Override 343 public int getCount() { 344 return mObjects.size(); 345 } 346 347 @Override 348 public @Nullable T getItem(int position) { 349 return mObjects.get(position); 350 } 351 352 /** 353 * Returns the position of the specified item in the array. 354 * 355 * @param item The item to retrieve the position of. 356 * 357 * @return The position of the specified item. 358 */ 359 public int getPosition(@Nullable T item) { 360 return mObjects.indexOf(item); 361 } 362 363 @Override 364 public long getItemId(int position) { 365 return position; 366 } 367 368 @Override 369 public @NonNull View getView(int position, @Nullable View convertView, 370 @NonNull ViewGroup parent) { 371 return createViewFromResource(mInflater, position, convertView, parent, mResource); 372 } 373 374 private @NonNull View createViewFromResource(@NonNull LayoutInflater inflater, int position, 375 @Nullable View convertView, @NonNull ViewGroup parent, int resource) { 376 final View view; 377 final TextView text; 378 379 if (convertView == null) { 380 view = inflater.inflate(resource, parent, false); 381 } else { 382 view = convertView; 383 } 384 385 try { 386 if (mFieldId == 0) { 387 // If no custom field is assigned, assume the whole resource is a TextView 388 text = (TextView) view; 389 } else { 390 // Otherwise, find the TextView field within the layout 391 text = (TextView) view.findViewById(mFieldId); 392 393 if (text == null) { 394 throw new RuntimeException("Failed to find view with ID " 395 + mContext.getResources().getResourceName(mFieldId) 396 + " in item layout"); 397 } 398 } 399 } catch (ClassCastException e) { 400 Log.e("ArrayAdapter", "You must supply a resource ID for a TextView"); 401 throw new IllegalStateException( 402 "ArrayAdapter requires the resource ID to be a TextView", e); 403 } 404 405 final T item = getItem(position); 406 if (item instanceof CharSequence) { 407 text.setText((CharSequence) item); 408 } else { 409 text.setText(item.toString()); 410 } 411 412 return view; 413 } 414 415 /** 416 * <p>Sets the layout resource to create the drop down views.</p> 417 * 418 * @param resource the layout resource defining the drop down views 419 * @see #getDropDownView(int, android.view.View, android.view.ViewGroup) 420 */ 421 public void setDropDownViewResource(@LayoutRes int resource) { 422 this.mDropDownResource = resource; 423 } 424 425 /** 426 * Sets the {@link Resources.Theme} against which drop-down views are 427 * inflated. 428 * <p> 429 * By default, drop-down views are inflated against the theme of the 430 * {@link Context} passed to the adapter's constructor. 431 * 432 * @param theme the theme against which to inflate drop-down views or 433 * {@code null} to use the theme from the adapter's context 434 * @see #getDropDownView(int, View, ViewGroup) 435 */ 436 @Override 437 public void setDropDownViewTheme(@Nullable Resources.Theme theme) { 438 if (theme == null) { 439 mDropDownInflater = null; 440 } else if (theme == mInflater.getContext().getTheme()) { 441 mDropDownInflater = mInflater; 442 } else { 443 final Context context = new ContextThemeWrapper(mContext, theme); 444 mDropDownInflater = LayoutInflater.from(context); 445 } 446 } 447 448 @Override 449 public @Nullable Resources.Theme getDropDownViewTheme() { 450 return mDropDownInflater == null ? null : mDropDownInflater.getContext().getTheme(); 451 } 452 453 @Override 454 public View getDropDownView(int position, @Nullable View convertView, 455 @NonNull ViewGroup parent) { 456 final LayoutInflater inflater = mDropDownInflater == null ? mInflater : mDropDownInflater; 457 return createViewFromResource(inflater, position, convertView, parent, mDropDownResource); 458 } 459 460 /** 461 * Creates a new ArrayAdapter from external resources. The content of the array is 462 * obtained through {@link android.content.res.Resources#getTextArray(int)}. 463 * 464 * @param context The application's environment. 465 * @param textArrayResId The identifier of the array to use as the data source. 466 * @param textViewResId The identifier of the layout used to create views. 467 * 468 * @return An ArrayAdapter<CharSequence>. 469 */ 470 public static @NonNull ArrayAdapter<CharSequence> createFromResource(@NonNull Context context, 471 @ArrayRes int textArrayResId, @LayoutRes int textViewResId) { 472 final CharSequence[] strings = context.getResources().getTextArray(textArrayResId); 473 return new ArrayAdapter<>(context, textViewResId, strings); 474 } 475 476 @Override 477 public @NonNull Filter getFilter() { 478 if (mFilter == null) { 479 mFilter = new ArrayFilter(); 480 } 481 return mFilter; 482 } 483 484 /** 485 * <p>An array filter constrains the content of the array adapter with 486 * a prefix. Each item that does not start with the supplied prefix 487 * is removed from the list.</p> 488 */ 489 private class ArrayFilter extends Filter { 490 @Override 491 protected FilterResults performFiltering(CharSequence prefix) { 492 final FilterResults results = new FilterResults(); 493 494 if (mOriginalValues == null) { 495 synchronized (mLock) { 496 mOriginalValues = new ArrayList<>(mObjects); 497 } 498 } 499 500 if (prefix == null || prefix.length() == 0) { 501 final ArrayList<T> list; 502 synchronized (mLock) { 503 list = new ArrayList<>(mOriginalValues); 504 } 505 results.values = list; 506 results.count = list.size(); 507 } else { 508 final String prefixString = prefix.toString().toLowerCase(); 509 510 final ArrayList<T> values; 511 synchronized (mLock) { 512 values = new ArrayList<>(mOriginalValues); 513 } 514 515 final int count = values.size(); 516 final ArrayList<T> newValues = new ArrayList<>(); 517 518 for (int i = 0; i < count; i++) { 519 final T value = values.get(i); 520 final String valueText = value.toString().toLowerCase(); 521 522 // First match against the whole, non-splitted value 523 if (valueText.startsWith(prefixString)) { 524 newValues.add(value); 525 } else { 526 final String[] words = valueText.split(" "); 527 for (String word : words) { 528 if (word.startsWith(prefixString)) { 529 newValues.add(value); 530 break; 531 } 532 } 533 } 534 } 535 536 results.values = newValues; 537 results.count = newValues.size(); 538 } 539 540 return results; 541 } 542 543 @Override 544 protected void publishResults(CharSequence constraint, FilterResults results) { 545 //noinspection unchecked 546 mObjects = (List<T>) results.values; 547 if (results.count > 0) { 548 notifyDataSetChanged(); 549 } else { 550 notifyDataSetInvalidated(); 551 } 552 } 553 } 554} 555