DropdownChipLayouter.java revision e78ad406f0b2ee0d031b71b33e6094a6961f7c79
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 setViewVisibility(viewHolder.permissionBottomDivider, View.GONE); 189 } else if (entryType == RecipientEntry.ENTRY_TYPE_PERMISSION_REQUEST) { 190 setViewVisibility(viewHolder.personViewGroup, View.GONE); 191 setViewVisibility(viewHolder.permissionViewGroup, View.VISIBLE); 192 setViewVisibility(viewHolder.permissionBottomDivider, View.VISIBLE); 193 } 194 195 return itemView; 196 } 197 198 /** 199 * Returns a new view with {@link #getItemLayoutResId(AdapterType)}. 200 */ 201 public View newView(AdapterType type) { 202 return mInflater.inflate(getItemLayoutResId(type), null); 203 } 204 205 /** 206 * Returns the same view, or inflates a new one if the given view was null. 207 */ 208 protected View reuseOrInflateView(View convertView, ViewGroup parent, AdapterType type) { 209 int itemLayout = getItemLayoutResId(type); 210 switch (type) { 211 case BASE_RECIPIENT: 212 case RECIPIENT_ALTERNATES: 213 break; 214 case SINGLE_RECIPIENT: 215 itemLayout = getAlternateItemLayoutResId(type); 216 break; 217 } 218 return convertView != null ? convertView : mInflater.inflate(itemLayout, parent, false); 219 } 220 221 /** 222 * Binds the text to the given text view. If the text was null, hides the text view. 223 */ 224 protected void bindTextToView(CharSequence text, TextView view) { 225 if (view == null) { 226 return; 227 } 228 229 if (text != null) { 230 view.setText(text); 231 view.setVisibility(View.VISIBLE); 232 } else { 233 view.setVisibility(View.GONE); 234 } 235 } 236 237 /** 238 * Binds the avatar icon to the image view. If we don't want to show the image, hides the 239 * image view. 240 */ 241 protected void bindIconToView(boolean showImage, RecipientEntry entry, ImageView view, 242 AdapterType type) { 243 if (view == null) { 244 return; 245 } 246 247 if (showImage) { 248 switch (type) { 249 case BASE_RECIPIENT: 250 byte[] photoBytes = entry.getPhotoBytes(); 251 if (photoBytes != null && photoBytes.length > 0) { 252 final Bitmap photo = BitmapFactory.decodeByteArray(photoBytes, 0, 253 photoBytes.length); 254 view.setImageBitmap(photo); 255 } else { 256 view.setImageResource(getDefaultPhotoResId()); 257 } 258 break; 259 case RECIPIENT_ALTERNATES: 260 Uri thumbnailUri = entry.getPhotoThumbnailUri(); 261 if (thumbnailUri != null) { 262 // TODO: see if this needs to be done outside the main thread 263 // as it may be too slow to get immediately. 264 view.setImageURI(thumbnailUri); 265 } else { 266 view.setImageResource(getDefaultPhotoResId()); 267 } 268 break; 269 case SINGLE_RECIPIENT: 270 default: 271 break; 272 } 273 view.setVisibility(View.VISIBLE); 274 } else { 275 view.setVisibility(View.GONE); 276 } 277 } 278 279 protected void bindDrawableToDeleteView(final StateListDrawable drawable, String recipient, 280 ImageView view) { 281 if (view == null) { 282 return; 283 } 284 if (drawable == null) { 285 view.setVisibility(View.GONE); 286 } else { 287 final Resources res = mContext.getResources(); 288 view.setImageDrawable(drawable); 289 view.setContentDescription( 290 res.getString(R.string.dropdown_delete_button_desc, recipient)); 291 if (mDeleteListener != null) { 292 view.setOnClickListener(new View.OnClickListener() { 293 @Override 294 public void onClick(View view) { 295 if (drawable.getCurrent() != null) { 296 mDeleteListener.onChipDelete(); 297 } 298 } 299 }); 300 } 301 } 302 } 303 304 protected void bindIndicatorToView( 305 @DrawableRes int indicatorIconId, String indicatorText, TextView view) { 306 if (view != null) { 307 if (indicatorText != null || indicatorIconId != 0) { 308 view.setText(indicatorText); 309 view.setVisibility(View.VISIBLE); 310 final Drawable indicatorIcon; 311 if (indicatorIconId != 0) { 312 indicatorIcon = mContext.getDrawable(indicatorIconId).mutate(); 313 indicatorIcon.setColorFilter(Color.WHITE, PorterDuff.Mode.SRC_IN); 314 } else { 315 indicatorIcon = null; 316 } 317 view.setCompoundDrawablesRelativeWithIntrinsicBounds( 318 indicatorIcon, null, null, null); 319 } else { 320 view.setVisibility(View.GONE); 321 } 322 } 323 } 324 325 protected void bindPermissionRequestDismissView(ImageView view) { 326 if (view == null) { 327 return; 328 } 329 view.setOnClickListener(new OnClickListener() { 330 @Override 331 public void onClick(View v) { 332 if (mPermissionRequestDismissedListener != null) { 333 mPermissionRequestDismissedListener.onPermissionRequestDismissed(); 334 } 335 } 336 }); 337 } 338 339 protected void setViewVisibility(View view, int visibility) { 340 if (view != null) { 341 view.setVisibility(visibility); 342 } 343 } 344 345 protected CharSequence getDestinationType(RecipientEntry entry) { 346 return mQuery.getTypeLabel(mContext.getResources(), entry.getDestinationType(), 347 entry.getDestinationLabel()).toString().toUpperCase(); 348 } 349 350 /** 351 * Returns a layout id for each item inside auto-complete list. 352 * 353 * Each View must contain two TextViews (for display name and destination) and one ImageView 354 * (for photo). Ids for those should be available via {@link #getDisplayNameResId()}, 355 * {@link #getDestinationResId()}, and {@link #getPhotoResId()}. 356 */ 357 protected @LayoutRes int getItemLayoutResId(AdapterType type) { 358 switch (type) { 359 case BASE_RECIPIENT: 360 return R.layout.chips_autocomplete_recipient_dropdown_item; 361 case RECIPIENT_ALTERNATES: 362 return R.layout.chips_recipient_dropdown_item; 363 default: 364 return R.layout.chips_recipient_dropdown_item; 365 } 366 } 367 368 /** 369 * Returns a layout id for each item inside alternate auto-complete list. 370 * 371 * Each View must contain two TextViews (for display name and destination) and one ImageView 372 * (for photo). Ids for those should be available via {@link #getDisplayNameResId()}, 373 * {@link #getDestinationResId()}, and {@link #getPhotoResId()}. 374 */ 375 protected @LayoutRes int getAlternateItemLayoutResId(AdapterType type) { 376 switch (type) { 377 case BASE_RECIPIENT: 378 return R.layout.chips_autocomplete_recipient_dropdown_item; 379 case RECIPIENT_ALTERNATES: 380 return R.layout.chips_recipient_dropdown_item; 381 default: 382 return R.layout.chips_recipient_dropdown_item; 383 } 384 } 385 386 /** 387 * Returns a resource ID representing an image which should be shown when ther's no relevant 388 * photo is available. 389 */ 390 protected @DrawableRes int getDefaultPhotoResId() { 391 return R.drawable.ic_contact_picture; 392 } 393 394 /** 395 * Returns an id for the ViewGroup in an item View that contains the person ui elements. 396 */ 397 protected @IdRes int getPersonGroupResId() { 398 return R.id.chip_person_wrapper; 399 } 400 401 /** 402 * Returns an id for TextView in an item View for showing a display name. By default 403 * {@link android.R.id#title} is returned. 404 */ 405 protected @IdRes int getDisplayNameResId() { 406 return android.R.id.title; 407 } 408 409 /** 410 * Returns an id for TextView in an item View for showing a destination 411 * (an email address or a phone number). 412 * By default {@link android.R.id#text1} is returned. 413 */ 414 protected @IdRes int getDestinationResId() { 415 return android.R.id.text1; 416 } 417 418 /** 419 * Returns an id for TextView in an item View for showing the type of the destination. 420 * By default {@link android.R.id#text2} is returned. 421 */ 422 protected @IdRes int getDestinationTypeResId() { 423 return android.R.id.text2; 424 } 425 426 /** 427 * Returns an id for ImageView in an item View for showing photo image for a person. In default 428 * {@link android.R.id#icon} is returned. 429 */ 430 protected @IdRes int getPhotoResId() { 431 return android.R.id.icon; 432 } 433 434 /** 435 * Returns an id for ImageView in an item View for showing the delete button. In default 436 * {@link android.R.id#icon1} is returned. 437 */ 438 protected @IdRes int getDeleteResId() { return android.R.id.icon1; } 439 440 /** 441 * Returns an id for the ViewGroup in an item View that contains the request permission ui 442 * elements. 443 */ 444 protected @IdRes int getPermissionGroupResId() { 445 return R.id.chip_permission_wrapper; 446 } 447 448 /** 449 * Returns an id for ImageView in an item View for dismissing the permission request. In default 450 * {@link android.R.id#icon2} is returned. 451 */ 452 protected @IdRes int getPermissionRequestDismissResId() { 453 return android.R.id.icon2; 454 } 455 456 /** 457 * Given a constraint and results, tries to find the constraint in those results, one at a time. 458 * A foreground font color style will be applied to the section that matches the constraint. As 459 * soon as a match has been found, no further matches are attempted. 460 * 461 * @param constraint A string that we will attempt to find within the results. 462 * @param results Strings that may contain the constraint. The order given is the order used to 463 * search for the constraint. 464 * 465 * @return An array of CharSequences, the length determined by the length of results. Each 466 * CharSequence will either be a styled SpannableString or just the input String. 467 */ 468 protected CharSequence[] getStyledResults(@Nullable String constraint, String... results) { 469 if (isAllWhitespace(constraint)) { 470 return results; 471 } 472 473 CharSequence[] styledResults = new CharSequence[results.length]; 474 boolean foundMatch = false; 475 for (int i = 0; i < results.length; i++) { 476 String result = results[i]; 477 if (result == null) { 478 continue; 479 } 480 481 if (!foundMatch) { 482 int index = result.toLowerCase().indexOf(constraint.toLowerCase()); 483 if (index != -1) { 484 SpannableStringBuilder styled = SpannableStringBuilder.valueOf(result); 485 ForegroundColorSpan highlightSpan = 486 new ForegroundColorSpan(mContext.getResources().getColor( 487 R.color.chips_dropdown_text_highlighted)); 488 styled.setSpan(highlightSpan, 489 index, index + constraint.length(), Spanned.SPAN_EXCLUSIVE_EXCLUSIVE); 490 styledResults[i] = styled; 491 foundMatch = true; 492 continue; 493 } 494 } 495 styledResults[i] = result; 496 } 497 return styledResults; 498 } 499 500 private static boolean isAllWhitespace(@Nullable String string) { 501 if (TextUtils.isEmpty(string)) { 502 return true; 503 } 504 505 for (int i = 0; i < string.length(); ++i) { 506 if (!Character.isWhitespace(string.charAt(i))) { 507 return false; 508 } 509 } 510 511 return true; 512 } 513 514 /** 515 * A holder class the view. Uses the getters in DropdownChipLayouter to find the id of the 516 * corresponding views. 517 */ 518 protected class ViewHolder { 519 public final ViewGroup personViewGroup; 520 public final TextView displayNameView; 521 public final TextView destinationView; 522 public final TextView destinationTypeView; 523 public final TextView indicatorView; 524 public final ImageView imageView; 525 public final ImageView deleteView; 526 public final View topDivider; 527 public final View bottomDivider; 528 public final View permissionBottomDivider; 529 530 public final ViewGroup permissionViewGroup; 531 public final ImageView permissionRequestDismissView; 532 533 public ViewHolder(View view) { 534 personViewGroup = (ViewGroup) view.findViewById(getPersonGroupResId()); 535 displayNameView = (TextView) view.findViewById(getDisplayNameResId()); 536 destinationView = (TextView) view.findViewById(getDestinationResId()); 537 destinationTypeView = (TextView) view.findViewById(getDestinationTypeResId()); 538 imageView = (ImageView) view.findViewById(getPhotoResId()); 539 deleteView = (ImageView) view.findViewById(getDeleteResId()); 540 topDivider = view.findViewById(R.id.chip_autocomplete_top_divider); 541 542 bottomDivider = view.findViewById(R.id.chip_autocomplete_bottom_divider); 543 permissionBottomDivider = view.findViewById(R.id.chip_permission_bottom_divider); 544 545 indicatorView = (TextView) view.findViewById(R.id.chip_indicator_text); 546 547 permissionViewGroup = (ViewGroup) view.findViewById(getPermissionGroupResId()); 548 permissionRequestDismissView = 549 (ImageView) view.findViewById(getPermissionRequestDismissResId()); 550 } 551 } 552} 553