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