SimpleAdapter.java revision b9ead4a91599ca63e947f74f83b67a58bda64a82
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.content.Context; 20import android.content.res.Resources; 21import android.view.ContextThemeWrapper; 22import android.view.View; 23import android.view.ViewGroup; 24import android.view.LayoutInflater; 25import android.net.Uri; 26 27import java.util.ArrayList; 28import java.util.List; 29import java.util.Map; 30 31/** 32 * An easy adapter to map static data to views defined in an XML file. You can specify the data 33 * backing the list as an ArrayList of Maps. Each entry in the ArrayList corresponds to one row 34 * in the list. The Maps contain the data for each row. You also specify an XML file that 35 * defines the views used to display the row, and a mapping from keys in the Map to specific 36 * views. 37 * 38 * Binding data to views occurs in two phases. First, if a 39 * {@link android.widget.SimpleAdapter.ViewBinder} is available, 40 * {@link ViewBinder#setViewValue(android.view.View, Object, String)} 41 * is invoked. If the returned value is true, binding has occurred. 42 * If the returned value is false, the following views are then tried in order: 43 * <ul> 44 * <li> A view that implements Checkable (e.g. CheckBox). The expected bind value is a boolean. 45 * <li> TextView. The expected bind value is a string and {@link #setViewText(TextView, String)} 46 * is invoked. 47 * <li> ImageView. The expected bind value is a resource id or a string and 48 * {@link #setViewImage(ImageView, int)} or {@link #setViewImage(ImageView, String)} is invoked. 49 * </ul> 50 * If no appropriate binding can be found, an {@link IllegalStateException} is thrown. 51 */ 52public class SimpleAdapter extends BaseAdapter implements Filterable, Spinner.ThemedSpinnerAdapter { 53 private int[] mTo; 54 private String[] mFrom; 55 private ViewBinder mViewBinder; 56 57 private List<? extends Map<String, ?>> mData; 58 59 private int mResource; 60 private int mDropDownResource; 61 private LayoutInflater mInflater; 62 63 /** Layout inflater used for {@link #getDropDownView(int, View, ViewGroup)}. */ 64 private LayoutInflater mDropDownInflater; 65 66 private SimpleFilter mFilter; 67 private ArrayList<Map<String, ?>> mUnfilteredData; 68 69 /** 70 * Constructor 71 * 72 * @param context The context where the View associated with this SimpleAdapter is running 73 * @param data A List of Maps. Each entry in the List corresponds to one row in the list. The 74 * Maps contain the data for each row, and should include all the entries specified in 75 * "from" 76 * @param resource Resource identifier of a view layout that defines the views for this list 77 * item. The layout file should include at least those named views defined in "to" 78 * @param from A list of column names that will be added to the Map associated with each 79 * item. 80 * @param to The views that should display column in the "from" parameter. These should all be 81 * TextViews. The first N views in this list are given the values of the first N columns 82 * in the from parameter. 83 */ 84 public SimpleAdapter(Context context, List<? extends Map<String, ?>> data, 85 int resource, String[] from, int[] to) { 86 mData = data; 87 mResource = mDropDownResource = resource; 88 mFrom = from; 89 mTo = to; 90 mInflater = (LayoutInflater) context.getSystemService(Context.LAYOUT_INFLATER_SERVICE); 91 } 92 93 /** 94 * @see android.widget.Adapter#getCount() 95 */ 96 public int getCount() { 97 return mData.size(); 98 } 99 100 /** 101 * @see android.widget.Adapter#getItem(int) 102 */ 103 public Object getItem(int position) { 104 return mData.get(position); 105 } 106 107 /** 108 * @see android.widget.Adapter#getItemId(int) 109 */ 110 public long getItemId(int position) { 111 return position; 112 } 113 114 /** 115 * @see android.widget.Adapter#getView(int, View, ViewGroup) 116 */ 117 public View getView(int position, View convertView, ViewGroup parent) { 118 return createViewFromResource(mInflater, position, convertView, parent, mResource); 119 } 120 121 private View createViewFromResource(LayoutInflater inflater, int position, View convertView, 122 ViewGroup parent, int resource) { 123 View v; 124 if (convertView == null) { 125 v = inflater.inflate(resource, parent, false); 126 } else { 127 v = convertView; 128 } 129 130 bindView(position, v); 131 132 return v; 133 } 134 135 /** 136 * <p>Sets the layout resource to create the drop down views.</p> 137 * 138 * @param resource the layout resource defining the drop down views 139 * @see #getDropDownView(int, android.view.View, android.view.ViewGroup) 140 */ 141 public void setDropDownViewResource(int resource) { 142 mDropDownResource = resource; 143 } 144 145 /** 146 * Sets the {@link android.content.res.Resources.Theme} against which drop-down views are 147 * inflated. 148 * <p> 149 * By default, drop-down views are inflated against the theme of the 150 * {@link Context} passed to the adapter's constructor. 151 * 152 * @param theme the theme against which to inflate drop-down views or 153 * {@code null} to use the theme from the adapter's context 154 * @see #getDropDownView(int, View, ViewGroup) 155 */ 156 @Override 157 public void setDropDownViewTheme(Resources.Theme theme) { 158 if (theme == null) { 159 mDropDownInflater = null; 160 } else if (theme == mInflater.getContext().getTheme()) { 161 mDropDownInflater = mInflater; 162 } else { 163 final Context context = new ContextThemeWrapper(mInflater.getContext(), theme); 164 mDropDownInflater = LayoutInflater.from(context); 165 } 166 } 167 168 @Override 169 public Resources.Theme getDropDownViewTheme() { 170 return mDropDownInflater == null ? null : mDropDownInflater.getContext().getTheme(); 171 } 172 173 @Override 174 public View getDropDownView(int position, View convertView, ViewGroup parent) { 175 return createViewFromResource( 176 mDropDownInflater, position, convertView, parent, mDropDownResource); 177 } 178 179 private void bindView(int position, View view) { 180 final Map dataSet = mData.get(position); 181 if (dataSet == null) { 182 return; 183 } 184 185 final ViewBinder binder = mViewBinder; 186 final String[] from = mFrom; 187 final int[] to = mTo; 188 final int count = to.length; 189 190 for (int i = 0; i < count; i++) { 191 final View v = view.findViewById(to[i]); 192 if (v != null) { 193 final Object data = dataSet.get(from[i]); 194 String text = data == null ? "" : data.toString(); 195 if (text == null) { 196 text = ""; 197 } 198 199 boolean bound = false; 200 if (binder != null) { 201 bound = binder.setViewValue(v, data, text); 202 } 203 204 if (!bound) { 205 if (v instanceof Checkable) { 206 if (data instanceof Boolean) { 207 ((Checkable) v).setChecked((Boolean) data); 208 } else if (v instanceof TextView) { 209 // Note: keep the instanceof TextView check at the bottom of these 210 // ifs since a lot of views are TextViews (e.g. CheckBoxes). 211 setViewText((TextView) v, text); 212 } else { 213 throw new IllegalStateException(v.getClass().getName() + 214 " should be bound to a Boolean, not a " + 215 (data == null ? "<unknown type>" : data.getClass())); 216 } 217 } else if (v instanceof TextView) { 218 // Note: keep the instanceof TextView check at the bottom of these 219 // ifs since a lot of views are TextViews (e.g. CheckBoxes). 220 setViewText((TextView) v, text); 221 } else if (v instanceof ImageView) { 222 if (data instanceof Integer) { 223 setViewImage((ImageView) v, (Integer) data); 224 } else { 225 setViewImage((ImageView) v, text); 226 } 227 } else { 228 throw new IllegalStateException(v.getClass().getName() + " is not a " + 229 " view that can be bounds by this SimpleAdapter"); 230 } 231 } 232 } 233 } 234 } 235 236 /** 237 * Returns the {@link ViewBinder} used to bind data to views. 238 * 239 * @return a ViewBinder or null if the binder does not exist 240 * 241 * @see #setViewBinder(android.widget.SimpleAdapter.ViewBinder) 242 */ 243 public ViewBinder getViewBinder() { 244 return mViewBinder; 245 } 246 247 /** 248 * Sets the binder used to bind data to views. 249 * 250 * @param viewBinder the binder used to bind data to views, can be null to 251 * remove the existing binder 252 * 253 * @see #getViewBinder() 254 */ 255 public void setViewBinder(ViewBinder viewBinder) { 256 mViewBinder = viewBinder; 257 } 258 259 /** 260 * Called by bindView() to set the image for an ImageView but only if 261 * there is no existing ViewBinder or if the existing ViewBinder cannot 262 * handle binding to an ImageView. 263 * 264 * This method is called instead of {@link #setViewImage(ImageView, String)} 265 * if the supplied data is an int or Integer. 266 * 267 * @param v ImageView to receive an image 268 * @param value the value retrieved from the data set 269 * 270 * @see #setViewImage(ImageView, String) 271 */ 272 public void setViewImage(ImageView v, int value) { 273 v.setImageResource(value); 274 } 275 276 /** 277 * Called by bindView() to set the image for an ImageView but only if 278 * there is no existing ViewBinder or if the existing ViewBinder cannot 279 * handle binding to an ImageView. 280 * 281 * By default, the value will be treated as an image resource. If the 282 * value cannot be used as an image resource, the value is used as an 283 * image Uri. 284 * 285 * This method is called instead of {@link #setViewImage(ImageView, int)} 286 * if the supplied data is not an int or Integer. 287 * 288 * @param v ImageView to receive an image 289 * @param value the value retrieved from the data set 290 * 291 * @see #setViewImage(ImageView, int) 292 */ 293 public void setViewImage(ImageView v, String value) { 294 try { 295 v.setImageResource(Integer.parseInt(value)); 296 } catch (NumberFormatException nfe) { 297 v.setImageURI(Uri.parse(value)); 298 } 299 } 300 301 /** 302 * Called by bindView() to set the text for a TextView but only if 303 * there is no existing ViewBinder or if the existing ViewBinder cannot 304 * handle binding to a TextView. 305 * 306 * @param v TextView to receive text 307 * @param text the text to be set for the TextView 308 */ 309 public void setViewText(TextView v, String text) { 310 v.setText(text); 311 } 312 313 public Filter getFilter() { 314 if (mFilter == null) { 315 mFilter = new SimpleFilter(); 316 } 317 return mFilter; 318 } 319 320 /** 321 * This class can be used by external clients of SimpleAdapter to bind 322 * values to views. 323 * 324 * You should use this class to bind values to views that are not 325 * directly supported by SimpleAdapter or to change the way binding 326 * occurs for views supported by SimpleAdapter. 327 * 328 * @see SimpleAdapter#setViewImage(ImageView, int) 329 * @see SimpleAdapter#setViewImage(ImageView, String) 330 * @see SimpleAdapter#setViewText(TextView, String) 331 */ 332 public static interface ViewBinder { 333 /** 334 * Binds the specified data to the specified view. 335 * 336 * When binding is handled by this ViewBinder, this method must return true. 337 * If this method returns false, SimpleAdapter will attempts to handle 338 * the binding on its own. 339 * 340 * @param view the view to bind the data to 341 * @param data the data to bind to the view 342 * @param textRepresentation a safe String representation of the supplied data: 343 * it is either the result of data.toString() or an empty String but it 344 * is never null 345 * 346 * @return true if the data was bound to the view, false otherwise 347 */ 348 boolean setViewValue(View view, Object data, String textRepresentation); 349 } 350 351 /** 352 * <p>An array filters constrains the content of the array adapter with 353 * a prefix. Each item that does not start with the supplied prefix 354 * is removed from the list.</p> 355 */ 356 private class SimpleFilter extends Filter { 357 358 @Override 359 protected FilterResults performFiltering(CharSequence prefix) { 360 FilterResults results = new FilterResults(); 361 362 if (mUnfilteredData == null) { 363 mUnfilteredData = new ArrayList<Map<String, ?>>(mData); 364 } 365 366 if (prefix == null || prefix.length() == 0) { 367 ArrayList<Map<String, ?>> list = mUnfilteredData; 368 results.values = list; 369 results.count = list.size(); 370 } else { 371 String prefixString = prefix.toString().toLowerCase(); 372 373 ArrayList<Map<String, ?>> unfilteredValues = mUnfilteredData; 374 int count = unfilteredValues.size(); 375 376 ArrayList<Map<String, ?>> newValues = new ArrayList<Map<String, ?>>(count); 377 378 for (int i = 0; i < count; i++) { 379 Map<String, ?> h = unfilteredValues.get(i); 380 if (h != null) { 381 382 int len = mTo.length; 383 384 for (int j=0; j<len; j++) { 385 String str = (String)h.get(mFrom[j]); 386 387 String[] words = str.split(" "); 388 int wordCount = words.length; 389 390 for (int k = 0; k < wordCount; k++) { 391 String word = words[k]; 392 393 if (word.toLowerCase().startsWith(prefixString)) { 394 newValues.add(h); 395 break; 396 } 397 } 398 } 399 } 400 } 401 402 results.values = newValues; 403 results.count = newValues.size(); 404 } 405 406 return results; 407 } 408 409 @Override 410 protected void publishResults(CharSequence constraint, FilterResults results) { 411 //noinspection unchecked 412 mData = (List<Map<String, ?>>) results.values; 413 if (results.count > 0) { 414 notifyDataSetChanged(); 415 } else { 416 notifyDataSetInvalidated(); 417 } 418 } 419 } 420} 421