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