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