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