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