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