DropdownChipLayouter.java revision 0d4d806606fa8210ad09430fab3a230a1c4e3681
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.Color; 8import android.graphics.PorterDuff; 9import android.graphics.drawable.Drawable; 10import android.graphics.drawable.StateListDrawable; 11import android.net.Uri; 12import android.support.annotation.DrawableRes; 13import android.support.annotation.IdRes; 14import android.support.annotation.LayoutRes; 15import android.support.annotation.Nullable; 16import android.support.v4.view.MarginLayoutParamsCompat; 17import android.text.SpannableStringBuilder; 18import android.text.Spanned; 19import android.text.TextUtils; 20import android.text.style.ForegroundColorSpan; 21import android.text.util.Rfc822Tokenizer; 22import android.view.LayoutInflater; 23import android.view.View; 24import android.view.View.OnClickListener; 25import android.view.ViewGroup; 26import android.view.ViewGroup.MarginLayoutParams; 27import android.widget.ImageView; 28import android.widget.TextView; 29 30import com.android.ex.chips.Queries.Query; 31 32/** 33 * A class that inflates and binds the views in the dropdown list from 34 * RecipientEditTextView. 35 */ 36public class DropdownChipLayouter { 37 /** 38 * The type of adapter that is requesting a chip layout. 39 */ 40 public enum AdapterType { 41 BASE_RECIPIENT, 42 RECIPIENT_ALTERNATES, 43 SINGLE_RECIPIENT 44 } 45 46 public interface ChipDeleteListener { 47 void onChipDelete(); 48 } 49 50 /** 51 * Listener that handles the dismisses of the entries of the 52 * {@link RecipientEntry#ENTRY_TYPE_PERMISSION_REQUEST} type. 53 */ 54 public interface PermissionRequestDismissedListener { 55 56 /** 57 * Callback that occurs when user dismisses the item that asks user to grant permissions to 58 * the app. 59 */ 60 void onPermissionRequestDismissed(); 61 } 62 63 private final LayoutInflater mInflater; 64 private final Context mContext; 65 private ChipDeleteListener mDeleteListener; 66 private PermissionRequestDismissedListener mPermissionRequestDismissedListener; 67 private Query mQuery; 68 private int mAutocompleteDividerMarginStart; 69 70 public DropdownChipLayouter(LayoutInflater inflater, Context context) { 71 mInflater = inflater; 72 mContext = context; 73 mAutocompleteDividerMarginStart = 74 context.getResources().getDimensionPixelOffset(R.dimen.chip_wrapper_start_padding); 75 } 76 77 public void setQuery(Query query) { 78 mQuery = query; 79 } 80 81 public void setDeleteListener(ChipDeleteListener listener) { 82 mDeleteListener = listener; 83 } 84 85 public void setPermissionRequestDismissedListener(PermissionRequestDismissedListener listener) { 86 mPermissionRequestDismissedListener = listener; 87 } 88 89 public void setAutocompleteDividerMarginStart(int autocompleteDividerMarginStart) { 90 mAutocompleteDividerMarginStart = autocompleteDividerMarginStart; 91 } 92 93 /** 94 * Layouts and binds recipient information to the view. If convertView is null, inflates a new 95 * view with getItemLaytout(). 96 * 97 * @param convertView The view to bind information to. 98 * @param parent The parent to bind the view to if we inflate a new view. 99 * @param entry The recipient entry to get information from. 100 * @param position The position in the list. 101 * @param type The adapter type that is requesting the bind. 102 * @param constraint The constraint typed in the auto complete view. 103 * 104 * @return A view ready to be shown in the drop down list. 105 */ 106 public View bindView(View convertView, ViewGroup parent, RecipientEntry entry, int position, 107 AdapterType type, String constraint) { 108 return bindView(convertView, parent, entry, position, type, constraint, null); 109 } 110 111 /** 112 * See {@link #bindView(View, ViewGroup, RecipientEntry, int, AdapterType, String)} 113 * @param deleteDrawable a {@link android.graphics.drawable.StateListDrawable} representing 114 * the delete icon. android.R.attr.state_activated should map to the delete icon, and the 115 * default state can map to a drawable of your choice (or null for no drawable). 116 */ 117 public View bindView(View convertView, ViewGroup parent, RecipientEntry entry, int position, 118 AdapterType type, String constraint, StateListDrawable deleteDrawable) { 119 // Default to show all the information 120 CharSequence[] styledResults = 121 getStyledResults(constraint, entry.getDisplayName(), entry.getDestination()); 122 CharSequence displayName = styledResults[0]; 123 CharSequence destination = styledResults[1]; 124 boolean showImage = true; 125 CharSequence destinationType = getDestinationType(entry); 126 127 final View itemView = reuseOrInflateView(convertView, parent, type); 128 129 final ViewHolder viewHolder = new ViewHolder(itemView); 130 131 // Hide some information depending on the adapter type. 132 switch (type) { 133 case BASE_RECIPIENT: 134 if (TextUtils.isEmpty(displayName) || TextUtils.equals(displayName, destination)) { 135 displayName = destination; 136 137 // We only show the destination for secondary entries, so clear it only for the 138 // first level. 139 if (entry.isFirstLevel()) { 140 destination = null; 141 } 142 } 143 144 if (!entry.isFirstLevel()) { 145 displayName = null; 146 showImage = false; 147 } 148 149 // For BASE_RECIPIENT set all top dividers except for the first one to be GONE. 150 if (viewHolder.topDivider != null) { 151 viewHolder.topDivider.setVisibility(position == 0 ? View.VISIBLE : View.GONE); 152 MarginLayoutParamsCompat.setMarginStart( 153 (MarginLayoutParams) viewHolder.topDivider.getLayoutParams(), 154 mAutocompleteDividerMarginStart); 155 } 156 if (viewHolder.bottomDivider != null) { 157 MarginLayoutParamsCompat.setMarginStart( 158 (MarginLayoutParams) viewHolder.bottomDivider.getLayoutParams(), 159 mAutocompleteDividerMarginStart); 160 } 161 break; 162 case RECIPIENT_ALTERNATES: 163 if (position != 0) { 164 displayName = null; 165 showImage = false; 166 } 167 break; 168 case SINGLE_RECIPIENT: 169 destination = Rfc822Tokenizer.tokenize(entry.getDestination())[0].getAddress(); 170 destinationType = null; 171 } 172 173 // Bind the information to the view 174 bindTextToView(displayName, viewHolder.displayNameView); 175 bindTextToView(destination, viewHolder.destinationView); 176 bindTextToView(destinationType, viewHolder.destinationTypeView); 177 bindIconToView(showImage, entry, viewHolder.imageView, type); 178 bindDrawableToDeleteView(deleteDrawable, entry.getDisplayName(), viewHolder.deleteView); 179 bindIndicatorToView( 180 entry.getIndicatorIconId(), entry.getIndicatorText(), viewHolder.indicatorView); 181 bindPermissionRequestDismissView(viewHolder.permissionRequestDismissView); 182 183 // Hide some view groups depending on the entry type 184 final int entryType = entry.getEntryType(); 185 if (entryType == RecipientEntry.ENTRY_TYPE_PERSON) { 186 setViewVisibility(viewHolder.personViewGroup, View.VISIBLE); 187 setViewVisibility(viewHolder.permissionViewGroup, View.GONE); 188 } else if (entryType == RecipientEntry.ENTRY_TYPE_PERMISSION_REQUEST) { 189 setViewVisibility(viewHolder.personViewGroup, View.GONE); 190 setViewVisibility(viewHolder.permissionViewGroup, View.VISIBLE); 191 } 192 193 return itemView; 194 } 195 196 /** 197 * Returns a new view with {@link #getItemLayoutResId(AdapterType)}. 198 */ 199 public View newView(AdapterType type) { 200 return mInflater.inflate(getItemLayoutResId(type), null); 201 } 202 203 /** 204 * Returns the same view, or inflates a new one if the given view was null. 205 */ 206 protected View reuseOrInflateView(View convertView, ViewGroup parent, AdapterType type) { 207 int itemLayout = getItemLayoutResId(type); 208 switch (type) { 209 case BASE_RECIPIENT: 210 case RECIPIENT_ALTERNATES: 211 break; 212 case SINGLE_RECIPIENT: 213 itemLayout = getAlternateItemLayoutResId(type); 214 break; 215 } 216 return convertView != null ? convertView : mInflater.inflate(itemLayout, parent, false); 217 } 218 219 /** 220 * Binds the text to the given text view. If the text was null, hides the text view. 221 */ 222 protected void bindTextToView(CharSequence text, TextView view) { 223 if (view == null) { 224 return; 225 } 226 227 if (text != null) { 228 view.setText(text); 229 view.setVisibility(View.VISIBLE); 230 } else { 231 view.setVisibility(View.GONE); 232 } 233 } 234 235 /** 236 * Binds the avatar icon to the image view. If we don't want to show the image, hides the 237 * image view. 238 */ 239 protected void bindIconToView(boolean showImage, RecipientEntry entry, ImageView view, 240 AdapterType type) { 241 if (view == null) { 242 return; 243 } 244 245 if (showImage) { 246 switch (type) { 247 case BASE_RECIPIENT: 248 byte[] photoBytes = entry.getPhotoBytes(); 249 if (photoBytes != null && photoBytes.length > 0) { 250 final Bitmap photo = BitmapFactory.decodeByteArray(photoBytes, 0, 251 photoBytes.length); 252 view.setImageBitmap(photo); 253 } else { 254 view.setImageResource(getDefaultPhotoResId()); 255 } 256 break; 257 case RECIPIENT_ALTERNATES: 258 Uri thumbnailUri = entry.getPhotoThumbnailUri(); 259 if (thumbnailUri != null) { 260 // TODO: see if this needs to be done outside the main thread 261 // as it may be too slow to get immediately. 262 view.setImageURI(thumbnailUri); 263 } else { 264 view.setImageResource(getDefaultPhotoResId()); 265 } 266 break; 267 case SINGLE_RECIPIENT: 268 default: 269 break; 270 } 271 view.setVisibility(View.VISIBLE); 272 } else { 273 view.setVisibility(View.GONE); 274 } 275 } 276 277 protected void bindDrawableToDeleteView(final StateListDrawable drawable, String recipient, 278 ImageView view) { 279 if (view == null) { 280 return; 281 } 282 if (drawable == null) { 283 view.setVisibility(View.GONE); 284 } else { 285 final Resources res = mContext.getResources(); 286 view.setImageDrawable(drawable); 287 view.setContentDescription( 288 res.getString(R.string.dropdown_delete_button_desc, recipient)); 289 if (mDeleteListener != null) { 290 view.setOnClickListener(new View.OnClickListener() { 291 @Override 292 public void onClick(View view) { 293 if (drawable.getCurrent() != null) { 294 mDeleteListener.onChipDelete(); 295 } 296 } 297 }); 298 } 299 } 300 } 301 302 protected void bindIndicatorToView( 303 @DrawableRes int indicatorIconId, String indicatorText, TextView view) { 304 if (view != null) { 305 if (indicatorText != null || indicatorIconId != 0) { 306 view.setText(indicatorText); 307 view.setVisibility(View.VISIBLE); 308 final Drawable indicatorIcon; 309 if (indicatorIconId != 0) { 310 indicatorIcon = mContext.getDrawable(indicatorIconId).mutate(); 311 indicatorIcon.setColorFilter(Color.WHITE, PorterDuff.Mode.SRC_IN); 312 } else { 313 indicatorIcon = null; 314 } 315 view.setCompoundDrawablesRelativeWithIntrinsicBounds( 316 indicatorIcon, null, null, null); 317 } else { 318 view.setVisibility(View.GONE); 319 } 320 } 321 } 322 323 protected void bindPermissionRequestDismissView(ImageView view) { 324 if (view == null) { 325 return; 326 } 327 view.setOnClickListener(new OnClickListener() { 328 @Override 329 public void onClick(View v) { 330 if (mPermissionRequestDismissedListener != null) { 331 mPermissionRequestDismissedListener.onPermissionRequestDismissed(); 332 } 333 } 334 }); 335 } 336 337 protected void setViewVisibility(View view, int visibility) { 338 if (view != null) { 339 view.setVisibility(visibility); 340 } 341 } 342 343 protected CharSequence getDestinationType(RecipientEntry entry) { 344 return mQuery.getTypeLabel(mContext.getResources(), entry.getDestinationType(), 345 entry.getDestinationLabel()).toString().toUpperCase(); 346 } 347 348 /** 349 * Returns a layout id for each item inside auto-complete list. 350 * 351 * Each View must contain two TextViews (for display name and destination) and one ImageView 352 * (for photo). Ids for those should be available via {@link #getDisplayNameResId()}, 353 * {@link #getDestinationResId()}, and {@link #getPhotoResId()}. 354 */ 355 protected @LayoutRes int getItemLayoutResId(AdapterType type) { 356 switch (type) { 357 case BASE_RECIPIENT: 358 return R.layout.chips_autocomplete_recipient_dropdown_item; 359 case RECIPIENT_ALTERNATES: 360 return R.layout.chips_recipient_dropdown_item; 361 default: 362 return R.layout.chips_recipient_dropdown_item; 363 } 364 } 365 366 /** 367 * Returns a layout id for each item inside alternate auto-complete list. 368 * 369 * Each View must contain two TextViews (for display name and destination) and one ImageView 370 * (for photo). Ids for those should be available via {@link #getDisplayNameResId()}, 371 * {@link #getDestinationResId()}, and {@link #getPhotoResId()}. 372 */ 373 protected @LayoutRes int getAlternateItemLayoutResId(AdapterType type) { 374 switch (type) { 375 case BASE_RECIPIENT: 376 return R.layout.chips_autocomplete_recipient_dropdown_item; 377 case RECIPIENT_ALTERNATES: 378 return R.layout.chips_recipient_dropdown_item; 379 default: 380 return R.layout.chips_recipient_dropdown_item; 381 } 382 } 383 384 /** 385 * Returns a resource ID representing an image which should be shown when ther's no relevant 386 * photo is available. 387 */ 388 protected @DrawableRes int getDefaultPhotoResId() { 389 return R.drawable.ic_contact_picture; 390 } 391 392 /** 393 * Returns an id for the ViewGroup in an item View that contains the person ui elements. 394 */ 395 protected @IdRes int getPersonGroupResId() { 396 return R.id.chip_person_wrapper; 397 } 398 399 /** 400 * Returns an id for TextView in an item View for showing a display name. By default 401 * {@link android.R.id#title} is returned. 402 */ 403 protected @IdRes int getDisplayNameResId() { 404 return android.R.id.title; 405 } 406 407 /** 408 * Returns an id for TextView in an item View for showing a destination 409 * (an email address or a phone number). 410 * By default {@link android.R.id#text1} is returned. 411 */ 412 protected @IdRes int getDestinationResId() { 413 return android.R.id.text1; 414 } 415 416 /** 417 * Returns an id for TextView in an item View for showing the type of the destination. 418 * By default {@link android.R.id#text2} is returned. 419 */ 420 protected @IdRes int getDestinationTypeResId() { 421 return android.R.id.text2; 422 } 423 424 /** 425 * Returns an id for ImageView in an item View for showing photo image for a person. In default 426 * {@link android.R.id#icon} is returned. 427 */ 428 protected @IdRes int getPhotoResId() { 429 return android.R.id.icon; 430 } 431 432 /** 433 * Returns an id for ImageView in an item View for showing the delete button. In default 434 * {@link android.R.id#icon1} is returned. 435 */ 436 protected @IdRes int getDeleteResId() { return android.R.id.icon1; } 437 438 /** 439 * Returns an id for the ViewGroup in an item View that contains the request permission ui 440 * elements. 441 */ 442 protected @IdRes int getPermissionGroupResId() { 443 return R.id.chip_permission_wrapper; 444 } 445 446 /** 447 * Returns an id for ImageView in an item View for dismissing the permission request. In default 448 * {@link android.R.id#icon2} is returned. 449 */ 450 protected @IdRes int getPermissionRequestDismissResId() { 451 return android.R.id.icon2; 452 } 453 454 /** 455 * Given a constraint and results, tries to find the constraint in those results, one at a time. 456 * A foreground font color style will be applied to the section that matches the constraint. As 457 * soon as a match has been found, no further matches are attempted. 458 * 459 * @param constraint A string that we will attempt to find within the results. 460 * @param results Strings that may contain the constraint. The order given is the order used to 461 * search for the constraint. 462 * 463 * @return An array of CharSequences, the length determined by the length of results. Each 464 * CharSequence will either be a styled SpannableString or just the input String. 465 */ 466 protected CharSequence[] getStyledResults(@Nullable String constraint, String... results) { 467 if (isAllWhitespace(constraint)) { 468 return results; 469 } 470 471 CharSequence[] styledResults = new CharSequence[results.length]; 472 boolean foundMatch = false; 473 for (int i = 0; i < results.length; i++) { 474 String result = results[i]; 475 if (result == null) { 476 continue; 477 } 478 479 if (!foundMatch) { 480 int index = result.toLowerCase().indexOf(constraint.toLowerCase()); 481 if (index != -1) { 482 SpannableStringBuilder styled = SpannableStringBuilder.valueOf(result); 483 ForegroundColorSpan highlightSpan = 484 new ForegroundColorSpan(mContext.getResources().getColor( 485 R.color.chips_dropdown_text_highlighted)); 486 styled.setSpan(highlightSpan, 487 index, index + constraint.length(), Spanned.SPAN_EXCLUSIVE_EXCLUSIVE); 488 styledResults[i] = styled; 489 foundMatch = true; 490 continue; 491 } 492 } 493 styledResults[i] = result; 494 } 495 return styledResults; 496 } 497 498 private static boolean isAllWhitespace(@Nullable String string) { 499 if (TextUtils.isEmpty(string)) { 500 return true; 501 } 502 503 for (int i = 0; i < string.length(); ++i) { 504 if (!Character.isWhitespace(string.charAt(i))) { 505 return false; 506 } 507 } 508 509 return true; 510 } 511 512 /** 513 * A holder class the view. Uses the getters in DropdownChipLayouter to find the id of the 514 * corresponding views. 515 */ 516 protected class ViewHolder { 517 public final ViewGroup personViewGroup; 518 public final TextView displayNameView; 519 public final TextView destinationView; 520 public final TextView destinationTypeView; 521 public final TextView indicatorView; 522 public final ImageView imageView; 523 public final ImageView deleteView; 524 public final View topDivider; 525 public final View bottomDivider; 526 527 public final ViewGroup permissionViewGroup; 528 public final ImageView permissionRequestDismissView; 529 530 public ViewHolder(View view) { 531 personViewGroup = (ViewGroup) view.findViewById(getPersonGroupResId()); 532 displayNameView = (TextView) view.findViewById(getDisplayNameResId()); 533 destinationView = (TextView) view.findViewById(getDestinationResId()); 534 destinationTypeView = (TextView) view.findViewById(getDestinationTypeResId()); 535 imageView = (ImageView) view.findViewById(getPhotoResId()); 536 deleteView = (ImageView) view.findViewById(getDeleteResId()); 537 topDivider = view.findViewById(R.id.chip_autocomplete_top_divider); 538 bottomDivider = view.findViewById(R.id.chip_autocomplete_bottom_divider); 539 indicatorView = (TextView) view.findViewById(R.id.chip_indicator_text); 540 541 permissionViewGroup = (ViewGroup) view.findViewById(getPermissionGroupResId()); 542 permissionRequestDismissView = 543 (ImageView) view.findViewById(getPermissionRequestDismissResId()); 544 } 545 } 546} 547