DropdownChipLayouter.java revision 174917b0eae33cf51af7fab947c49738028ed018
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 adapterType, 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 int entryType = entry.getEntryType(); 128 final View itemView = 129 reuseOrInflateView(convertView, parent, adapterType, entryType); 130 131 final ViewHolder viewHolder = new ViewHolder(itemView); 132 133 // Hide some information depending on the adapter type. 134 switch (adapterType) { 135 case BASE_RECIPIENT: 136 if (TextUtils.isEmpty(displayName) || TextUtils.equals(displayName, destination)) { 137 displayName = destination; 138 139 // We only show the destination for secondary entries, so clear it only for the 140 // first level. 141 if (entry.isFirstLevel()) { 142 destination = null; 143 } 144 } 145 146 if (!entry.isFirstLevel()) { 147 displayName = null; 148 showImage = false; 149 } 150 151 // For BASE_RECIPIENT set all top dividers except for the first one to be GONE. 152 if (viewHolder.topDivider != null) { 153 viewHolder.topDivider.setVisibility(position == 0 ? View.VISIBLE : View.GONE); 154 MarginLayoutParamsCompat.setMarginStart( 155 (MarginLayoutParams) viewHolder.topDivider.getLayoutParams(), 156 mAutocompleteDividerMarginStart); 157 } 158 if ((viewHolder.bottomDivider != null) 159 && (entryType == RecipientEntry.ENTRY_TYPE_PERSON)) { 160 MarginLayoutParamsCompat.setMarginStart( 161 (MarginLayoutParams) viewHolder.bottomDivider.getLayoutParams(), 162 mAutocompleteDividerMarginStart); 163 } 164 break; 165 case RECIPIENT_ALTERNATES: 166 if (position != 0) { 167 displayName = null; 168 showImage = false; 169 } 170 break; 171 case SINGLE_RECIPIENT: 172 destination = Rfc822Tokenizer.tokenize(entry.getDestination())[0].getAddress(); 173 destinationType = null; 174 } 175 176 // Bind the information to the view 177 bindTextToView(displayName, viewHolder.displayNameView); 178 bindTextToView(destination, viewHolder.destinationView); 179 bindTextToView(destinationType, viewHolder.destinationTypeView); 180 bindIconToView(showImage, entry, viewHolder.imageView, adapterType); 181 bindDrawableToDeleteView(deleteDrawable, entry.getDisplayName(), viewHolder.deleteView); 182 bindIndicatorToView( 183 entry.getIndicatorIconId(), entry.getIndicatorText(), viewHolder.indicatorView); 184 bindPermissionRequestDismissView(viewHolder.permissionRequestDismissView); 185 186 return itemView; 187 } 188 189 /** 190 * Returns a new view with {@link #getItemLayoutResId(AdapterType)}. 191 */ 192 public View newView(AdapterType type) { 193 return mInflater.inflate(getItemLayoutResId(type), null); 194 } 195 196 /** 197 * Returns the same view, or inflates a new one if the given view was null. 198 */ 199 protected View reuseOrInflateView( 200 View convertView, ViewGroup parent, AdapterType adapterType, int entryType) { 201 @LayoutRes int itemLayout = getItemLayoutResId(adapterType, entryType); 202 switch (adapterType) { 203 case BASE_RECIPIENT: 204 case RECIPIENT_ALTERNATES: 205 break; 206 case SINGLE_RECIPIENT: 207 itemLayout = getAlternateItemLayoutResId(adapterType); 208 break; 209 } 210 return convertView != null ? convertView : mInflater.inflate(itemLayout, parent, false); 211 } 212 213 /** 214 * Binds the text to the given text view. If the text was null, hides the text view. 215 */ 216 protected void bindTextToView(CharSequence text, TextView view) { 217 if (view == null) { 218 return; 219 } 220 221 if (text != null) { 222 view.setText(text); 223 view.setVisibility(View.VISIBLE); 224 } else { 225 view.setVisibility(View.GONE); 226 } 227 } 228 229 /** 230 * Binds the avatar icon to the image view. If we don't want to show the image, hides the 231 * image view. 232 */ 233 protected void bindIconToView(boolean showImage, RecipientEntry entry, ImageView view, 234 AdapterType type) { 235 if (view == null) { 236 return; 237 } 238 239 if (showImage) { 240 switch (type) { 241 case BASE_RECIPIENT: 242 byte[] photoBytes = entry.getPhotoBytes(); 243 if (photoBytes != null && photoBytes.length > 0) { 244 final Bitmap photo = BitmapFactory.decodeByteArray(photoBytes, 0, 245 photoBytes.length); 246 view.setImageBitmap(photo); 247 } else { 248 view.setImageResource(getDefaultPhotoResId()); 249 } 250 break; 251 case RECIPIENT_ALTERNATES: 252 Uri thumbnailUri = entry.getPhotoThumbnailUri(); 253 if (thumbnailUri != null) { 254 // TODO: see if this needs to be done outside the main thread 255 // as it may be too slow to get immediately. 256 view.setImageURI(thumbnailUri); 257 } else { 258 view.setImageResource(getDefaultPhotoResId()); 259 } 260 break; 261 case SINGLE_RECIPIENT: 262 default: 263 break; 264 } 265 view.setVisibility(View.VISIBLE); 266 } else { 267 view.setVisibility(View.GONE); 268 } 269 } 270 271 protected void bindDrawableToDeleteView(final StateListDrawable drawable, String recipient, 272 ImageView view) { 273 if (view == null) { 274 return; 275 } 276 if (drawable == null) { 277 view.setVisibility(View.GONE); 278 } else { 279 final Resources res = mContext.getResources(); 280 view.setImageDrawable(drawable); 281 view.setContentDescription( 282 res.getString(R.string.dropdown_delete_button_desc, recipient)); 283 if (mDeleteListener != null) { 284 view.setOnClickListener(new View.OnClickListener() { 285 @Override 286 public void onClick(View view) { 287 if (drawable.getCurrent() != null) { 288 mDeleteListener.onChipDelete(); 289 } 290 } 291 }); 292 } 293 } 294 } 295 296 protected void bindIndicatorToView( 297 @DrawableRes int indicatorIconId, String indicatorText, TextView view) { 298 if (view != null) { 299 if (indicatorText != null || indicatorIconId != 0) { 300 view.setText(indicatorText); 301 view.setVisibility(View.VISIBLE); 302 final Drawable indicatorIcon; 303 if (indicatorIconId != 0) { 304 indicatorIcon = mContext.getDrawable(indicatorIconId).mutate(); 305 indicatorIcon.setColorFilter(Color.WHITE, PorterDuff.Mode.SRC_IN); 306 } else { 307 indicatorIcon = null; 308 } 309 view.setCompoundDrawablesRelativeWithIntrinsicBounds( 310 indicatorIcon, null, null, null); 311 } else { 312 view.setVisibility(View.GONE); 313 } 314 } 315 } 316 317 protected void bindPermissionRequestDismissView(ImageView view) { 318 if (view == null) { 319 return; 320 } 321 view.setOnClickListener(new OnClickListener() { 322 @Override 323 public void onClick(View v) { 324 if (mPermissionRequestDismissedListener != null) { 325 mPermissionRequestDismissedListener.onPermissionRequestDismissed(); 326 } 327 } 328 }); 329 } 330 331 protected void setViewVisibility(View view, int visibility) { 332 if (view != null) { 333 view.setVisibility(visibility); 334 } 335 } 336 337 protected CharSequence getDestinationType(RecipientEntry entry) { 338 return mQuery.getTypeLabel(mContext.getResources(), entry.getDestinationType(), 339 entry.getDestinationLabel()).toString().toUpperCase(); 340 } 341 342 /** 343 * Returns a layout id for each item inside auto-complete list. 344 * 345 * Each View must contain two TextViews (for display name and destination) and one ImageView 346 * (for photo). Ids for those should be available via {@link #getDisplayNameResId()}, 347 * {@link #getDestinationResId()}, and {@link #getPhotoResId()}. 348 */ 349 protected @LayoutRes int getItemLayoutResId( 350 AdapterType adapterType, @Nullable Integer entryType) { 351 if (entryType == RecipientEntry.ENTRY_TYPE_PERMISSION_REQUEST) { 352 return R.layout.chips_autocomplete_permission_dropdown_item; 353 } 354 switch (adapterType) { 355 case BASE_RECIPIENT: 356 return R.layout.chips_autocomplete_recipient_dropdown_item; 357 case RECIPIENT_ALTERNATES: 358 return R.layout.chips_recipient_dropdown_item; 359 default: 360 return R.layout.chips_recipient_dropdown_item; 361 } 362 } 363 364 protected @LayoutRes int getItemLayoutResId(AdapterType adapterType) { 365 return getItemLayoutResId(adapterType, null); 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 TextView displayNameView; 520 public final TextView destinationView; 521 public final TextView destinationTypeView; 522 public final TextView indicatorView; 523 public final ImageView imageView; 524 public final ImageView deleteView; 525 public final View topDivider; 526 public final View bottomDivider; 527 528 public final ImageView permissionRequestDismissView; 529 530 public ViewHolder(View view) { 531 displayNameView = (TextView) view.findViewById(getDisplayNameResId()); 532 destinationView = (TextView) view.findViewById(getDestinationResId()); 533 destinationTypeView = (TextView) view.findViewById(getDestinationTypeResId()); 534 imageView = (ImageView) view.findViewById(getPhotoResId()); 535 deleteView = (ImageView) view.findViewById(getDeleteResId()); 536 topDivider = view.findViewById(R.id.chip_autocomplete_top_divider); 537 bottomDivider = view.findViewById(R.id.chip_autocomplete_bottom_divider); 538 indicatorView = (TextView) view.findViewById(R.id.chip_indicator_text); 539 540 permissionRequestDismissView = 541 (ImageView) view.findViewById(getPermissionRequestDismissResId()); 542 } 543 } 544} 545