DropdownChipLayouter.java revision e5d17d57be3b89a6adb7d429a4cb3cb1fbb422ff
1package com.android.ex.chips; 2 3import android.content.Context; 4import android.content.res.Resources; 5import android.graphics.Bitmap; 6import android.graphics.BitmapFactory; 7import android.graphics.drawable.StateListDrawable; 8import android.net.Uri; 9import android.support.annotation.DrawableRes; 10import android.support.annotation.IdRes; 11import android.support.annotation.LayoutRes; 12import android.support.annotation.Nullable; 13import android.text.SpannableStringBuilder; 14import android.text.Spanned; 15import android.text.TextUtils; 16import android.text.style.ForegroundColorSpan; 17import android.text.util.Rfc822Tokenizer; 18import android.view.LayoutInflater; 19import android.view.View; 20import android.view.ViewGroup; 21import android.widget.ImageView; 22import android.widget.TextView; 23 24import com.android.ex.chips.Queries.Query; 25 26/** 27 * A class that inflates and binds the views in the dropdown list from 28 * RecipientEditTextView. 29 */ 30public class DropdownChipLayouter { 31 /** 32 * The type of adapter that is requesting a chip layout. 33 */ 34 public enum AdapterType { 35 BASE_RECIPIENT, 36 RECIPIENT_ALTERNATES, 37 SINGLE_RECIPIENT 38 } 39 40 public interface ChipDeleteListener { 41 void onChipDelete(); 42 } 43 44 private final LayoutInflater mInflater; 45 private final Context mContext; 46 private ChipDeleteListener mDeleteListener; 47 private Query mQuery; 48 49 public DropdownChipLayouter(LayoutInflater inflater, Context context) { 50 mInflater = inflater; 51 mContext = context; 52 } 53 54 public void setQuery(Query query) { 55 mQuery = query; 56 } 57 58 public void setDeleteListener(ChipDeleteListener listener) { 59 mDeleteListener = listener; 60 } 61 62 63 /** 64 * Layouts and binds recipient information to the view. If convertView is null, inflates a new 65 * view with getItemLaytout(). 66 * 67 * @param convertView The view to bind information to. 68 * @param parent The parent to bind the view to if we inflate a new view. 69 * @param entry The recipient entry to get information from. 70 * @param position The position in the list. 71 * @param type The adapter type that is requesting the bind. 72 * @param constraint The constraint typed in the auto complete view. 73 * 74 * @return A view ready to be shown in the drop down list. 75 */ 76 public View bindView(View convertView, ViewGroup parent, RecipientEntry entry, int position, 77 AdapterType type, String constraint) { 78 return bindView(convertView, parent, entry, position, type, constraint, null); 79 } 80 81 /** 82 * See {@link #bindView(View, ViewGroup, RecipientEntry, int, AdapterType, String)} 83 * @param deleteDrawable a {@link android.graphics.drawable.StateListDrawable} representing 84 * the delete icon. android.R.attr.state_activated should map to the delete icon, and the 85 * default state can map to a drawable of your choice (or null for no drawable). 86 */ 87 public View bindView(View convertView, ViewGroup parent, RecipientEntry entry, int position, 88 AdapterType type, String constraint, StateListDrawable deleteDrawable) { 89 // Default to show all the information 90 CharSequence[] styledResults = 91 getStyledResults(constraint, entry.getDisplayName(), entry.getDestination()); 92 CharSequence displayName = styledResults[0]; 93 CharSequence destination = styledResults[1]; 94 boolean showImage = true; 95 CharSequence destinationType = getDestinationType(entry); 96 97 final View itemView = reuseOrInflateView(convertView, parent, type); 98 99 final ViewHolder viewHolder = new ViewHolder(itemView); 100 101 // Hide some information depending on the entry type and adapter type 102 switch (type) { 103 case BASE_RECIPIENT: 104 if (TextUtils.isEmpty(displayName) || TextUtils.equals(displayName, destination)) { 105 displayName = destination; 106 107 // We only show the destination for secondary entries, so clear it only for the 108 // first level. 109 if (entry.isFirstLevel()) { 110 destination = null; 111 } 112 } 113 114 if (!entry.isFirstLevel()) { 115 displayName = null; 116 showImage = false; 117 } 118 119 // For BASE_RECIPIENT set all top dividers except for the first one to be GONE. 120 if (viewHolder.topDivider != null) { 121 viewHolder.topDivider.setVisibility(position == 0 ? View.VISIBLE : View.GONE); 122 } 123 break; 124 case RECIPIENT_ALTERNATES: 125 if (position != 0) { 126 displayName = null; 127 showImage = false; 128 } 129 break; 130 case SINGLE_RECIPIENT: 131 destination = Rfc822Tokenizer.tokenize(entry.getDestination())[0].getAddress(); 132 destinationType = null; 133 } 134 135 // Bind the information to the view 136 bindTextToView(displayName, viewHolder.displayNameView); 137 bindTextToView(destination, viewHolder.destinationView); 138 bindTextToView(destinationType, viewHolder.destinationTypeView); 139 bindIconToView(showImage, entry, viewHolder.imageView, type); 140 bindDrawableToDeleteView(deleteDrawable, entry.getDisplayName(), viewHolder.deleteView); 141 142 return itemView; 143 } 144 145 /** 146 * Returns a new view with {@link #getItemLayoutResId(AdapterType)}. 147 */ 148 public View newView(AdapterType type) { 149 return mInflater.inflate(getItemLayoutResId(type), null); 150 } 151 152 /** 153 * Returns the same view, or inflates a new one if the given view was null. 154 */ 155 protected View reuseOrInflateView(View convertView, ViewGroup parent, AdapterType type) { 156 int itemLayout = getItemLayoutResId(type); 157 switch (type) { 158 case BASE_RECIPIENT: 159 case RECIPIENT_ALTERNATES: 160 break; 161 case SINGLE_RECIPIENT: 162 itemLayout = getAlternateItemLayoutResId(type); 163 break; 164 } 165 return convertView != null ? convertView : mInflater.inflate(itemLayout, parent, false); 166 } 167 168 /** 169 * Binds the text to the given text view. If the text was null, hides the text view. 170 */ 171 protected void bindTextToView(CharSequence text, TextView view) { 172 if (view == null) { 173 return; 174 } 175 176 if (text != null) { 177 view.setText(text); 178 view.setVisibility(View.VISIBLE); 179 } else { 180 view.setVisibility(View.GONE); 181 } 182 } 183 184 /** 185 * Binds the avatar icon to the image view. If we don't want to show the image, hides the 186 * image view. 187 */ 188 protected void bindIconToView(boolean showImage, RecipientEntry entry, ImageView view, 189 AdapterType type) { 190 if (view == null) { 191 return; 192 } 193 194 if (showImage) { 195 switch (type) { 196 case BASE_RECIPIENT: 197 byte[] photoBytes = entry.getPhotoBytes(); 198 if (photoBytes != null && photoBytes.length > 0) { 199 final Bitmap photo = BitmapFactory.decodeByteArray(photoBytes, 0, 200 photoBytes.length); 201 view.setImageBitmap(photo); 202 } else { 203 view.setImageResource(getDefaultPhotoResId()); 204 } 205 break; 206 case RECIPIENT_ALTERNATES: 207 Uri thumbnailUri = entry.getPhotoThumbnailUri(); 208 if (thumbnailUri != null) { 209 // TODO: see if this needs to be done outside the main thread 210 // as it may be too slow to get immediately. 211 view.setImageURI(thumbnailUri); 212 } else { 213 view.setImageResource(getDefaultPhotoResId()); 214 } 215 break; 216 case SINGLE_RECIPIENT: 217 default: 218 break; 219 } 220 view.setVisibility(View.VISIBLE); 221 } else { 222 view.setVisibility(View.GONE); 223 } 224 } 225 226 protected void bindDrawableToDeleteView(final StateListDrawable drawable, String recipient, 227 ImageView view) { 228 if (view == null) { 229 return; 230 } 231 if (drawable == null) { 232 view.setVisibility(View.GONE); 233 } else { 234 final Resources res = mContext.getResources(); 235 view.setImageDrawable(drawable); 236 view.setContentDescription( 237 res.getString(R.string.dropdown_delete_button_desc, recipient)); 238 if (mDeleteListener != null) { 239 view.setOnClickListener(new View.OnClickListener() { 240 @Override 241 public void onClick(View view) { 242 if (drawable.getCurrent() != null) { 243 mDeleteListener.onChipDelete(); 244 } 245 } 246 }); 247 } 248 } 249 } 250 251 protected CharSequence getDestinationType(RecipientEntry entry) { 252 return mQuery.getTypeLabel(mContext.getResources(), entry.getDestinationType(), 253 entry.getDestinationLabel()).toString().toUpperCase(); 254 } 255 256 /** 257 * Returns a layout id for each item inside auto-complete list. 258 * 259 * Each View must contain two TextViews (for display name and destination) and one ImageView 260 * (for photo). Ids for those should be available via {@link #getDisplayNameResId()}, 261 * {@link #getDestinationResId()}, and {@link #getPhotoResId()}. 262 */ 263 protected @LayoutRes int getItemLayoutResId(AdapterType type) { 264 switch (type) { 265 case BASE_RECIPIENT: 266 return R.layout.chips_autocomplete_recipient_dropdown_item; 267 case RECIPIENT_ALTERNATES: 268 return R.layout.chips_recipient_dropdown_item; 269 default: 270 return R.layout.chips_recipient_dropdown_item; 271 } 272 } 273 274 /** 275 * Returns a layout id for each item inside alternate auto-complete list. 276 * 277 * Each View must contain two TextViews (for display name and destination) and one ImageView 278 * (for photo). Ids for those should be available via {@link #getDisplayNameResId()}, 279 * {@link #getDestinationResId()}, and {@link #getPhotoResId()}. 280 */ 281 protected @LayoutRes int getAlternateItemLayoutResId(AdapterType type) { 282 switch (type) { 283 case BASE_RECIPIENT: 284 return R.layout.chips_autocomplete_recipient_dropdown_item; 285 case RECIPIENT_ALTERNATES: 286 return R.layout.chips_recipient_dropdown_item; 287 default: 288 return R.layout.chips_recipient_dropdown_item; 289 } 290 } 291 292 /** 293 * Returns a resource ID representing an image which should be shown when ther's no relevant 294 * photo is available. 295 */ 296 protected @DrawableRes int getDefaultPhotoResId() { 297 return R.drawable.ic_contact_picture; 298 } 299 300 /** 301 * Returns an id for TextView in an item View for showing a display name. By default 302 * {@link android.R.id#title} is returned. 303 */ 304 protected @IdRes int getDisplayNameResId() { 305 return android.R.id.title; 306 } 307 308 /** 309 * Returns an id for TextView in an item View for showing a destination 310 * (an email address or a phone number). 311 * By default {@link android.R.id#text1} is returned. 312 */ 313 protected @IdRes int getDestinationResId() { 314 return android.R.id.text1; 315 } 316 317 /** 318 * Returns an id for TextView in an item View for showing the type of the destination. 319 * By default {@link android.R.id#text2} is returned. 320 */ 321 protected @IdRes int getDestinationTypeResId() { 322 return android.R.id.text2; 323 } 324 325 /** 326 * Returns an id for ImageView in an item View for showing photo image for a person. In default 327 * {@link android.R.id#icon} is returned. 328 */ 329 protected @IdRes int getPhotoResId() { 330 return android.R.id.icon; 331 } 332 333 /** 334 * Returns an id for ImageView in an item View for showing the delete button. In default 335 * {@link android.R.id#icon1} is returned. 336 */ 337 protected @IdRes int getDeleteResId() { return android.R.id.icon1; } 338 339 /** 340 * Given a constraint and results, tries to find the constraint in those results, one at a time. 341 * A foreground font color style will be applied to the section that matches the constraint. As 342 * soon as a match has been found, no further matches are attempted. 343 * 344 * @param constraint A string that we will attempt to find within the results. 345 * @param results Strings that may contain the constraint. The order given is the order used to 346 * search for the constraint. 347 * 348 * @return An array of CharSequences, the length determined by the length of results. Each 349 * CharSequence will either be a styled SpannableString or just the input String. 350 */ 351 protected CharSequence[] getStyledResults(@Nullable String constraint, String... results) { 352 if (isAllWhitespace(constraint)) { 353 return results; 354 } 355 356 CharSequence[] styledResults = new CharSequence[results.length]; 357 boolean foundMatch = false; 358 for (int i = 0; i < results.length; i++) { 359 String result = results[i]; 360 if (result == null) { 361 continue; 362 } 363 364 if (!foundMatch) { 365 int index = result.toLowerCase().indexOf(constraint.toLowerCase()); 366 if (index != -1) { 367 SpannableStringBuilder styled = SpannableStringBuilder.valueOf(result); 368 ForegroundColorSpan highlightSpan = 369 new ForegroundColorSpan(mContext.getResources().getColor( 370 R.color.chips_dropdown_text_highlighted)); 371 styled.setSpan(highlightSpan, 372 index, index + constraint.length(), Spanned.SPAN_EXCLUSIVE_EXCLUSIVE); 373 styledResults[i] = styled; 374 foundMatch = true; 375 continue; 376 } 377 } 378 styledResults[i] = result; 379 } 380 return styledResults; 381 } 382 383 private static boolean isAllWhitespace(@Nullable String string) { 384 if (TextUtils.isEmpty(string)) { 385 return true; 386 } 387 388 for (int i = 0; i < string.length(); ++i) { 389 if (!Character.isWhitespace(string.charAt(i))) { 390 return false; 391 } 392 } 393 394 return true; 395 } 396 397 /** 398 * A holder class the view. Uses the getters in DropdownChipLayouter to find the id of the 399 * corresponding views. 400 */ 401 protected class ViewHolder { 402 public final TextView displayNameView; 403 public final TextView destinationView; 404 public final TextView destinationTypeView; 405 public final ImageView imageView; 406 public final ImageView deleteView; 407 public final View topDivider; 408 409 public ViewHolder(View view) { 410 displayNameView = (TextView) view.findViewById(getDisplayNameResId()); 411 destinationView = (TextView) view.findViewById(getDestinationResId()); 412 destinationTypeView = (TextView) view.findViewById(getDestinationTypeResId()); 413 imageView = (ImageView) view.findViewById(getPhotoResId()); 414 deleteView = (ImageView) view.findViewById(getDeleteResId()); 415 topDivider = view.findViewById(R.id.chip_autocomplete_top_divider); 416 } 417 } 418} 419