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