Kevin Lin | b10d1c6 | 2014-01-24 12:45:00 -0800 | [diff] [blame] | 1 | package com.android.ex.chips; |
| 2 | |
| 3 | import android.content.Context; |
| 4 | import android.graphics.Bitmap; |
| 5 | import android.graphics.BitmapFactory; |
Jin Cao | 4db8ccc | 2014-07-30 10:11:07 -0700 | [diff] [blame] | 6 | import android.graphics.drawable.StateListDrawable; |
Kevin Lin | b10d1c6 | 2014-01-24 12:45:00 -0800 | [diff] [blame] | 7 | import android.net.Uri; |
Jin Cao | 4db8ccc | 2014-07-30 10:11:07 -0700 | [diff] [blame] | 8 | import android.support.annotation.DrawableRes; |
| 9 | import android.support.annotation.IdRes; |
Jin Cao | 4ddcdae | 2014-07-28 19:03:56 -0700 | [diff] [blame] | 10 | import android.support.annotation.LayoutRes; |
Kevin Lin | b10d1c6 | 2014-01-24 12:45:00 -0800 | [diff] [blame] | 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 | |
Jin Cao | 4db8ccc | 2014-07-30 10:11:07 -0700 | [diff] [blame] | 35 | public interface ChipDeleteListener { |
| 36 | void onChipDelete(); |
| 37 | } |
| 38 | |
Kevin Lin | b10d1c6 | 2014-01-24 12:45:00 -0800 | [diff] [blame] | 39 | private final LayoutInflater mInflater; |
| 40 | private final Context mContext; |
Jin Cao | 4db8ccc | 2014-07-30 10:11:07 -0700 | [diff] [blame] | 41 | private ChipDeleteListener mDeleteListener; |
Kevin Lin | b10d1c6 | 2014-01-24 12:45:00 -0800 | [diff] [blame] | 42 | private Query mQuery; |
| 43 | |
| 44 | public DropdownChipLayouter(LayoutInflater inflater, Context context) { |
| 45 | mInflater = inflater; |
| 46 | mContext = context; |
| 47 | } |
| 48 | |
| 49 | public void setQuery(Query query) { |
| 50 | mQuery = query; |
| 51 | } |
| 52 | |
Jin Cao | 4db8ccc | 2014-07-30 10:11:07 -0700 | [diff] [blame] | 53 | public void setDeleteListener(ChipDeleteListener listener) { |
| 54 | mDeleteListener = listener; |
| 55 | } |
| 56 | |
Kevin Lin | b10d1c6 | 2014-01-24 12:45:00 -0800 | [diff] [blame] | 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 | */ |
| 71 | public View bindView(View convertView, ViewGroup parent, RecipientEntry entry, int position, |
| 72 | AdapterType type, String constraint) { |
Jin Cao | 4db8ccc | 2014-07-30 10:11:07 -0700 | [diff] [blame] | 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 | */ |
| 80 | public View bindView(View convertView, ViewGroup parent, RecipientEntry entry, int position, |
| 81 | AdapterType type, String constraint, StateListDrawable deleteDrawable) { |
Kevin Lin | b10d1c6 | 2014-01-24 12:45:00 -0800 | [diff] [blame] | 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 | } |
Jin Cao | b58c9a6 | 2014-08-06 14:29:14 -0700 | [diff] [blame] | 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 | } |
Kevin Lin | b10d1c6 | 2014-01-24 12:45:00 -0800 | [diff] [blame] | 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); |
Jin Cao | 4db8ccc | 2014-07-30 10:11:07 -0700 | [diff] [blame] | 131 | bindDrawableToDeleteView(deleteDrawable, viewHolder.deleteView); |
Kevin Lin | b10d1c6 | 2014-01-24 12:45:00 -0800 | [diff] [blame] | 132 | |
| 133 | return itemView; |
| 134 | } |
| 135 | |
| 136 | /** |
Jin Cao | 4ddcdae | 2014-07-28 19:03:56 -0700 | [diff] [blame] | 137 | * Returns a new view with {@link #getItemLayoutResId(AdapterType)}. |
Kevin Lin | b10d1c6 | 2014-01-24 12:45:00 -0800 | [diff] [blame] | 138 | */ |
Jin Cao | 4ddcdae | 2014-07-28 19:03:56 -0700 | [diff] [blame] | 139 | public View newView(AdapterType type) { |
| 140 | return mInflater.inflate(getItemLayoutResId(type), null); |
Kevin Lin | b10d1c6 | 2014-01-24 12:45:00 -0800 | [diff] [blame] | 141 | } |
| 142 | |
| 143 | /** |
| 144 | * Returns the same view, or inflates a new one if the given view was null. |
| 145 | */ |
| 146 | protected View reuseOrInflateView(View convertView, ViewGroup parent, AdapterType type) { |
Jin Cao | 4ddcdae | 2014-07-28 19:03:56 -0700 | [diff] [blame] | 147 | int itemLayout = getItemLayoutResId(type); |
Kevin Lin | b10d1c6 | 2014-01-24 12:45:00 -0800 | [diff] [blame] | 148 | switch (type) { |
| 149 | case BASE_RECIPIENT: |
| 150 | case RECIPIENT_ALTERNATES: |
| 151 | break; |
| 152 | case SINGLE_RECIPIENT: |
Jin Cao | 4ddcdae | 2014-07-28 19:03:56 -0700 | [diff] [blame] | 153 | itemLayout = getAlternateItemLayoutResId(type); |
Kevin Lin | b10d1c6 | 2014-01-24 12:45:00 -0800 | [diff] [blame] | 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 | */ |
| 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 | */ |
| 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. |
Scott Kennedy | 7a4e677 | 2013-11-21 14:31:33 -0800 | [diff] [blame] | 202 | view.setImageURI(thumbnailUri); |
Kevin Lin | b10d1c6 | 2014-01-24 12:45:00 -0800 | [diff] [blame] | 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 | |
Jin Cao | 4db8ccc | 2014-07-30 10:11:07 -0700 | [diff] [blame] | 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 | |
Kevin Lin | b10d1c6 | 2014-01-24 12:45:00 -0800 | [diff] [blame] | 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 | */ |
Jin Cao | 4ddcdae | 2014-07-28 19:03:56 -0700 | [diff] [blame] | 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 | } |
Kevin Lin | b10d1c6 | 2014-01-24 12:45:00 -0800 | [diff] [blame] | 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 | */ |
Jin Cao | 4ddcdae | 2014-07-28 19:03:56 -0700 | [diff] [blame] | 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 | } |
Kevin Lin | b10d1c6 | 2014-01-24 12:45:00 -0800 | [diff] [blame] | 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 | */ |
Jin Cao | 4db8ccc | 2014-07-30 10:11:07 -0700 | [diff] [blame] | 283 | protected @DrawableRes int getDefaultPhotoResId() { |
Kevin Lin | b10d1c6 | 2014-01-24 12:45:00 -0800 | [diff] [blame] | 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 | */ |
Jin Cao | 4db8ccc | 2014-07-30 10:11:07 -0700 | [diff] [blame] | 291 | protected @IdRes int getDisplayNameResId() { |
Kevin Lin | b10d1c6 | 2014-01-24 12:45:00 -0800 | [diff] [blame] | 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 | */ |
Jin Cao | 4db8ccc | 2014-07-30 10:11:07 -0700 | [diff] [blame] | 300 | protected @IdRes int getDestinationResId() { |
Kevin Lin | b10d1c6 | 2014-01-24 12:45:00 -0800 | [diff] [blame] | 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 | */ |
Jin Cao | 4db8ccc | 2014-07-30 10:11:07 -0700 | [diff] [blame] | 308 | protected @IdRes int getDestinationTypeResId() { |
Kevin Lin | b10d1c6 | 2014-01-24 12:45:00 -0800 | [diff] [blame] | 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 | */ |
Jin Cao | 4db8ccc | 2014-07-30 10:11:07 -0700 | [diff] [blame] | 316 | protected @IdRes int getPhotoResId() { |
Kevin Lin | b10d1c6 | 2014-01-24 12:45:00 -0800 | [diff] [blame] | 317 | return android.R.id.icon; |
| 318 | } |
| 319 | |
| 320 | /** |
Jin Cao | 4db8ccc | 2014-07-30 10:11:07 -0700 | [diff] [blame] | 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 | */ |
| 324 | protected @IdRes int getDeleteResId() { return android.R.id.icon1; } |
| 325 | |
| 326 | /** |
Kevin Lin | b10d1c6 | 2014-01-24 12:45:00 -0800 | [diff] [blame] | 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; |
Jin Cao | 4db8ccc | 2014-07-30 10:11:07 -0700 | [diff] [blame] | 335 | public final ImageView deleteView; |
Jin Cao | b58c9a6 | 2014-08-06 14:29:14 -0700 | [diff] [blame] | 336 | public final View topDivider; |
Kevin Lin | b10d1c6 | 2014-01-24 12:45:00 -0800 | [diff] [blame] | 337 | |
| 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()); |
Jin Cao | 4db8ccc | 2014-07-30 10:11:07 -0700 | [diff] [blame] | 343 | deleteView = (ImageView) view.findViewById(getDeleteResId()); |
Jin Cao | b58c9a6 | 2014-08-06 14:29:14 -0700 | [diff] [blame] | 344 | topDivider = view.findViewById(R.id.chip_autocomplete_top_divider); |
Kevin Lin | b10d1c6 | 2014-01-24 12:45:00 -0800 | [diff] [blame] | 345 | } |
| 346 | } |
| 347 | } |