blob: 170008f1838083533d9ce0d643e6d1f31392143f [file] [log] [blame]
Kevin Linb10d1c62014-01-24 12:45:00 -08001package com.android.ex.chips;
2
3import android.content.Context;
Jin Cao6c3f99e2015-01-22 11:35:29 -08004import android.content.res.Resources;
Kevin Linb10d1c62014-01-24 12:45:00 -08005import android.graphics.Bitmap;
6import android.graphics.BitmapFactory;
Jin Cao4db8ccc2014-07-30 10:11:07 -07007import android.graphics.drawable.StateListDrawable;
Kevin Linb10d1c62014-01-24 12:45:00 -08008import android.net.Uri;
Jin Cao4db8ccc2014-07-30 10:11:07 -07009import android.support.annotation.DrawableRes;
10import android.support.annotation.IdRes;
Jin Cao4ddcdae2014-07-28 19:03:56 -070011import android.support.annotation.LayoutRes;
Scott Kennedye5d17d52015-02-10 15:52:30 -080012import android.support.annotation.Nullable;
Scott Kennedy6f86e3b2015-02-10 20:13:40 -080013import android.support.v4.view.MarginLayoutParamsCompat;
Scott Kennedye5d17d52015-02-10 15:52:30 -080014import android.text.SpannableStringBuilder;
15import android.text.Spanned;
Kevin Linb10d1c62014-01-24 12:45:00 -080016import android.text.TextUtils;
Scott Kennedye5d17d52015-02-10 15:52:30 -080017import android.text.style.ForegroundColorSpan;
Kevin Linb10d1c62014-01-24 12:45:00 -080018import android.text.util.Rfc822Tokenizer;
19import android.view.LayoutInflater;
20import android.view.View;
21import android.view.ViewGroup;
Scott Kennedy6f86e3b2015-02-10 20:13:40 -080022import android.view.ViewGroup.MarginLayoutParams;
Kevin Linb10d1c62014-01-24 12:45:00 -080023import 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
Jin Cao4db8ccc2014-07-30 10:11:07 -070042 public interface ChipDeleteListener {
43 void onChipDelete();
44 }
45
Kevin Linb10d1c62014-01-24 12:45:00 -080046 private final LayoutInflater mInflater;
47 private final Context mContext;
Jin Cao4db8ccc2014-07-30 10:11:07 -070048 private ChipDeleteListener mDeleteListener;
Kevin Linb10d1c62014-01-24 12:45:00 -080049 private Query mQuery;
Scott Kennedy6f86e3b2015-02-10 20:13:40 -080050 private int mAutocompleteDividerMarginStart;
Kevin Linb10d1c62014-01-24 12:45:00 -080051
52 public DropdownChipLayouter(LayoutInflater inflater, Context context) {
53 mInflater = inflater;
54 mContext = context;
Scott Kennedy6f86e3b2015-02-10 20:13:40 -080055 mAutocompleteDividerMarginStart =
56 context.getResources().getDimensionPixelOffset(R.dimen.chip_wrapper_start_padding);
Kevin Linb10d1c62014-01-24 12:45:00 -080057 }
58
59 public void setQuery(Query query) {
60 mQuery = query;
61 }
62
Jin Cao4db8ccc2014-07-30 10:11:07 -070063 public void setDeleteListener(ChipDeleteListener listener) {
64 mDeleteListener = listener;
65 }
66
Scott Kennedy6f86e3b2015-02-10 20:13:40 -080067 public void setAutocompleteDividerMarginStart(int autocompleteDividerMarginStart) {
68 mAutocompleteDividerMarginStart = autocompleteDividerMarginStart;
69 }
Kevin Linb10d1c62014-01-24 12:45:00 -080070
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) {
Jin Cao4db8ccc2014-07-30 10:11:07 -070086 return bindView(convertView, parent, entry, position, type, constraint, null);
87 }
88
89 /**
90 * See {@link #bindView(View, ViewGroup, RecipientEntry, int, AdapterType, String)}
Jin Cao6c3f99e2015-01-22 11:35:29 -080091 * @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).
Jin Cao4db8ccc2014-07-30 10:11:07 -070094 */
95 public View bindView(View convertView, ViewGroup parent, RecipientEntry entry, int position,
96 AdapterType type, String constraint, StateListDrawable deleteDrawable) {
Kevin Linb10d1c62014-01-24 12:45:00 -080097 // Default to show all the information
Scott Kennedye5d17d52015-02-10 15:52:30 -080098 CharSequence[] styledResults =
99 getStyledResults(constraint, entry.getDisplayName(), entry.getDestination());
100 CharSequence displayName = styledResults[0];
101 CharSequence destination = styledResults[1];
Kevin Linb10d1c62014-01-24 12:45:00 -0800102 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 }
Jin Caob58c9a62014-08-06 14:29:14 -0700126
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);
Scott Kennedy6f86e3b2015-02-10 20:13:40 -0800130 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);
Jin Caob58c9a62014-08-06 14:29:14 -0700138 }
Kevin Linb10d1c62014-01-24 12:45:00 -0800139 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);
Jin Cao6c3f99e2015-01-22 11:35:29 -0800156 bindDrawableToDeleteView(deleteDrawable, entry.getDisplayName(), viewHolder.deleteView);
Kevin Linb10d1c62014-01-24 12:45:00 -0800157
158 return itemView;
159 }
160
161 /**
Jin Cao4ddcdae2014-07-28 19:03:56 -0700162 * Returns a new view with {@link #getItemLayoutResId(AdapterType)}.
Kevin Linb10d1c62014-01-24 12:45:00 -0800163 */
Jin Cao4ddcdae2014-07-28 19:03:56 -0700164 public View newView(AdapterType type) {
165 return mInflater.inflate(getItemLayoutResId(type), null);
Kevin Linb10d1c62014-01-24 12:45:00 -0800166 }
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) {
Jin Cao4ddcdae2014-07-28 19:03:56 -0700172 int itemLayout = getItemLayoutResId(type);
Kevin Linb10d1c62014-01-24 12:45:00 -0800173 switch (type) {
174 case BASE_RECIPIENT:
175 case RECIPIENT_ALTERNATES:
176 break;
177 case SINGLE_RECIPIENT:
Jin Cao4ddcdae2014-07-28 19:03:56 -0700178 itemLayout = getAlternateItemLayoutResId(type);
Kevin Linb10d1c62014-01-24 12:45:00 -0800179 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.
Scott Kennedy7a4e6772013-11-21 14:31:33 -0800227 view.setImageURI(thumbnailUri);
Kevin Linb10d1c62014-01-24 12:45:00 -0800228 } 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
Jin Cao6c3f99e2015-01-22 11:35:29 -0800242 protected void bindDrawableToDeleteView(final StateListDrawable drawable, String recipient,
243 ImageView view) {
Jin Cao4db8ccc2014-07-30 10:11:07 -0700244 if (view == null) {
245 return;
246 }
247 if (drawable == null) {
248 view.setVisibility(View.GONE);
Jin Caoffc01112014-12-15 16:24:37 -0800249 } else {
Jin Cao6c3f99e2015-01-22 11:35:29 -0800250 final Resources res = mContext.getResources();
Jin Caoffc01112014-12-15 16:24:37 -0800251 view.setImageDrawable(drawable);
Jin Cao6c3f99e2015-01-22 11:35:29 -0800252 view.setContentDescription(
253 res.getString(R.string.dropdown_delete_button_desc, recipient));
Jin Caoffc01112014-12-15 16:24:37 -0800254 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 }
Jin Cao4db8ccc2014-07-30 10:11:07 -0700261 }
Jin Caoffc01112014-12-15 16:24:37 -0800262 });
263 }
Jin Cao4db8ccc2014-07-30 10:11:07 -0700264 }
265 }
266
Kevin Linb10d1c62014-01-24 12:45:00 -0800267 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 */
Jin Cao4ddcdae2014-07-28 19:03:56 -0700279 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 }
Kevin Linb10d1c62014-01-24 12:45:00 -0800288 }
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 */
Jin Cao4ddcdae2014-07-28 19:03:56 -0700297 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 }
Kevin Linb10d1c62014-01-24 12:45:00 -0800306 }
307
308 /**
309 * Returns a resource ID representing an image which should be shown when ther's no relevant
310 * photo is available.
311 */
Jin Cao4db8ccc2014-07-30 10:11:07 -0700312 protected @DrawableRes int getDefaultPhotoResId() {
Kevin Linb10d1c62014-01-24 12:45:00 -0800313 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 */
Jin Cao4db8ccc2014-07-30 10:11:07 -0700320 protected @IdRes int getDisplayNameResId() {
Kevin Linb10d1c62014-01-24 12:45:00 -0800321 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 */
Jin Cao4db8ccc2014-07-30 10:11:07 -0700329 protected @IdRes int getDestinationResId() {
Kevin Linb10d1c62014-01-24 12:45:00 -0800330 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 */
Jin Cao4db8ccc2014-07-30 10:11:07 -0700337 protected @IdRes int getDestinationTypeResId() {
Kevin Linb10d1c62014-01-24 12:45:00 -0800338 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 */
Jin Cao4db8ccc2014-07-30 10:11:07 -0700345 protected @IdRes int getPhotoResId() {
Kevin Linb10d1c62014-01-24 12:45:00 -0800346 return android.R.id.icon;
347 }
348
349 /**
Jin Cao4db8ccc2014-07-30 10:11:07 -0700350 * 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 /**
Scott Kennedye5d17d52015-02-10 15:52:30 -0800356 * 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 /**
Kevin Linb10d1c62014-01-24 12:45:00 -0800414 * 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;
Jin Cao4db8ccc2014-07-30 10:11:07 -0700422 public final ImageView deleteView;
Jin Caob58c9a62014-08-06 14:29:14 -0700423 public final View topDivider;
Scott Kennedy6f86e3b2015-02-10 20:13:40 -0800424 public final View bottomDivider;
Kevin Linb10d1c62014-01-24 12:45:00 -0800425
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());
Jin Cao4db8ccc2014-07-30 10:11:07 -0700431 deleteView = (ImageView) view.findViewById(getDeleteResId());
Jin Caob58c9a62014-08-06 14:29:14 -0700432 topDivider = view.findViewById(R.id.chip_autocomplete_top_divider);
Scott Kennedy6f86e3b2015-02-10 20:13:40 -0800433 bottomDivider = view.findViewById(R.id.chip_autocomplete_bottom_divider);
Kevin Linb10d1c62014-01-24 12:45:00 -0800434 }
435 }
436}