package com.android.ex.chips; import android.content.Context; import android.content.res.Resources; import android.graphics.Bitmap; import android.graphics.BitmapFactory; import android.graphics.drawable.StateListDrawable; import android.net.Uri; import android.support.annotation.DrawableRes; import android.support.annotation.IdRes; import android.support.annotation.LayoutRes; import android.support.annotation.Nullable; import android.support.v4.view.MarginLayoutParamsCompat; import android.text.SpannableStringBuilder; import android.text.Spanned; import android.text.TextUtils; import android.text.style.ForegroundColorSpan; import android.text.util.Rfc822Tokenizer; import android.view.LayoutInflater; import android.view.View; import android.view.ViewGroup; import android.view.ViewGroup.MarginLayoutParams; import android.widget.ImageView; import android.widget.TextView; import com.android.ex.chips.Queries.Query; /** * A class that inflates and binds the views in the dropdown list from * RecipientEditTextView. */ public class DropdownChipLayouter { /** * The type of adapter that is requesting a chip layout. */ public enum AdapterType { BASE_RECIPIENT, RECIPIENT_ALTERNATES, SINGLE_RECIPIENT } public interface ChipDeleteListener { void onChipDelete(); } private final LayoutInflater mInflater; private final Context mContext; private ChipDeleteListener mDeleteListener; private Query mQuery; private int mAutocompleteDividerMarginStart; public DropdownChipLayouter(LayoutInflater inflater, Context context) { mInflater = inflater; mContext = context; mAutocompleteDividerMarginStart = context.getResources().getDimensionPixelOffset(R.dimen.chip_wrapper_start_padding); } public void setQuery(Query query) { mQuery = query; } public void setDeleteListener(ChipDeleteListener listener) { mDeleteListener = listener; } public void setAutocompleteDividerMarginStart(int autocompleteDividerMarginStart) { mAutocompleteDividerMarginStart = autocompleteDividerMarginStart; } /** * Layouts and binds recipient information to the view. If convertView is null, inflates a new * view with getItemLaytout(). * * @param convertView The view to bind information to. * @param parent The parent to bind the view to if we inflate a new view. * @param entry The recipient entry to get information from. * @param position The position in the list. * @param type The adapter type that is requesting the bind. * @param constraint The constraint typed in the auto complete view. * * @return A view ready to be shown in the drop down list. */ public View bindView(View convertView, ViewGroup parent, RecipientEntry entry, int position, AdapterType type, String constraint) { return bindView(convertView, parent, entry, position, type, constraint, null); } /** * See {@link #bindView(View, ViewGroup, RecipientEntry, int, AdapterType, String)} * @param deleteDrawable a {@link android.graphics.drawable.StateListDrawable} representing * the delete icon. android.R.attr.state_activated should map to the delete icon, and the * default state can map to a drawable of your choice (or null for no drawable). */ public View bindView(View convertView, ViewGroup parent, RecipientEntry entry, int position, AdapterType type, String constraint, StateListDrawable deleteDrawable) { // Default to show all the information CharSequence[] styledResults = getStyledResults(constraint, entry.getDisplayName(), entry.getDestination()); CharSequence displayName = styledResults[0]; CharSequence destination = styledResults[1]; boolean showImage = true; CharSequence destinationType = getDestinationType(entry); final View itemView = reuseOrInflateView(convertView, parent, type); final ViewHolder viewHolder = new ViewHolder(itemView); // Hide some information depending on the entry type and adapter type switch (type) { case BASE_RECIPIENT: if (TextUtils.isEmpty(displayName) || TextUtils.equals(displayName, destination)) { displayName = destination; // We only show the destination for secondary entries, so clear it only for the // first level. if (entry.isFirstLevel()) { destination = null; } } if (!entry.isFirstLevel()) { displayName = null; showImage = false; } // For BASE_RECIPIENT set all top dividers except for the first one to be GONE. if (viewHolder.topDivider != null) { viewHolder.topDivider.setVisibility(position == 0 ? View.VISIBLE : View.GONE); MarginLayoutParamsCompat.setMarginStart( (MarginLayoutParams) viewHolder.topDivider.getLayoutParams(), mAutocompleteDividerMarginStart); } if (viewHolder.bottomDivider != null) { MarginLayoutParamsCompat.setMarginStart( (MarginLayoutParams) viewHolder.bottomDivider.getLayoutParams(), mAutocompleteDividerMarginStart); } break; case RECIPIENT_ALTERNATES: if (position != 0) { displayName = null; showImage = false; } break; case SINGLE_RECIPIENT: destination = Rfc822Tokenizer.tokenize(entry.getDestination())[0].getAddress(); destinationType = null; } // Bind the information to the view bindTextToView(displayName, viewHolder.displayNameView); bindTextToView(destination, viewHolder.destinationView); bindTextToView(destinationType, viewHolder.destinationTypeView); bindIconToView(showImage, entry, viewHolder.imageView, type); bindDrawableToDeleteView(deleteDrawable, entry.getDisplayName(), viewHolder.deleteView); return itemView; } /** * Returns a new view with {@link #getItemLayoutResId(AdapterType)}. */ public View newView(AdapterType type) { return mInflater.inflate(getItemLayoutResId(type), null); } /** * Returns the same view, or inflates a new one if the given view was null. */ protected View reuseOrInflateView(View convertView, ViewGroup parent, AdapterType type) { int itemLayout = getItemLayoutResId(type); switch (type) { case BASE_RECIPIENT: case RECIPIENT_ALTERNATES: break; case SINGLE_RECIPIENT: itemLayout = getAlternateItemLayoutResId(type); break; } return convertView != null ? convertView : mInflater.inflate(itemLayout, parent, false); } /** * Binds the text to the given text view. If the text was null, hides the text view. */ protected void bindTextToView(CharSequence text, TextView view) { if (view == null) { return; } if (text != null) { view.setText(text); view.setVisibility(View.VISIBLE); } else { view.setVisibility(View.GONE); } } /** * Binds the avatar icon to the image view. If we don't want to show the image, hides the * image view. */ protected void bindIconToView(boolean showImage, RecipientEntry entry, ImageView view, AdapterType type) { if (view == null) { return; } if (showImage) { switch (type) { case BASE_RECIPIENT: byte[] photoBytes = entry.getPhotoBytes(); if (photoBytes != null && photoBytes.length > 0) { final Bitmap photo = BitmapFactory.decodeByteArray(photoBytes, 0, photoBytes.length); view.setImageBitmap(photo); } else { view.setImageResource(getDefaultPhotoResId()); } break; case RECIPIENT_ALTERNATES: Uri thumbnailUri = entry.getPhotoThumbnailUri(); if (thumbnailUri != null) { // TODO: see if this needs to be done outside the main thread // as it may be too slow to get immediately. view.setImageURI(thumbnailUri); } else { view.setImageResource(getDefaultPhotoResId()); } break; case SINGLE_RECIPIENT: default: break; } view.setVisibility(View.VISIBLE); } else { view.setVisibility(View.GONE); } } protected void bindDrawableToDeleteView(final StateListDrawable drawable, String recipient, ImageView view) { if (view == null) { return; } if (drawable == null) { view.setVisibility(View.GONE); } else { final Resources res = mContext.getResources(); view.setImageDrawable(drawable); view.setContentDescription( res.getString(R.string.dropdown_delete_button_desc, recipient)); if (mDeleteListener != null) { view.setOnClickListener(new View.OnClickListener() { @Override public void onClick(View view) { if (drawable.getCurrent() != null) { mDeleteListener.onChipDelete(); } } }); } } } protected CharSequence getDestinationType(RecipientEntry entry) { return mQuery.getTypeLabel(mContext.getResources(), entry.getDestinationType(), entry.getDestinationLabel()).toString().toUpperCase(); } /** * Returns a layout id for each item inside auto-complete list. * * Each View must contain two TextViews (for display name and destination) and one ImageView * (for photo). Ids for those should be available via {@link #getDisplayNameResId()}, * {@link #getDestinationResId()}, and {@link #getPhotoResId()}. */ protected @LayoutRes int getItemLayoutResId(AdapterType type) { switch (type) { case BASE_RECIPIENT: return R.layout.chips_autocomplete_recipient_dropdown_item; case RECIPIENT_ALTERNATES: return R.layout.chips_recipient_dropdown_item; default: return R.layout.chips_recipient_dropdown_item; } } /** * Returns a layout id for each item inside alternate auto-complete list. * * Each View must contain two TextViews (for display name and destination) and one ImageView * (for photo). Ids for those should be available via {@link #getDisplayNameResId()}, * {@link #getDestinationResId()}, and {@link #getPhotoResId()}. */ protected @LayoutRes int getAlternateItemLayoutResId(AdapterType type) { switch (type) { case BASE_RECIPIENT: return R.layout.chips_autocomplete_recipient_dropdown_item; case RECIPIENT_ALTERNATES: return R.layout.chips_recipient_dropdown_item; default: return R.layout.chips_recipient_dropdown_item; } } /** * Returns a resource ID representing an image which should be shown when ther's no relevant * photo is available. */ protected @DrawableRes int getDefaultPhotoResId() { return R.drawable.ic_contact_picture; } /** * Returns an id for TextView in an item View for showing a display name. By default * {@link android.R.id#title} is returned. */ protected @IdRes int getDisplayNameResId() { return android.R.id.title; } /** * Returns an id for TextView in an item View for showing a destination * (an email address or a phone number). * By default {@link android.R.id#text1} is returned. */ protected @IdRes int getDestinationResId() { return android.R.id.text1; } /** * Returns an id for TextView in an item View for showing the type of the destination. * By default {@link android.R.id#text2} is returned. */ protected @IdRes int getDestinationTypeResId() { return android.R.id.text2; } /** * Returns an id for ImageView in an item View for showing photo image for a person. In default * {@link android.R.id#icon} is returned. */ protected @IdRes int getPhotoResId() { return android.R.id.icon; } /** * Returns an id for ImageView in an item View for showing the delete button. In default * {@link android.R.id#icon1} is returned. */ protected @IdRes int getDeleteResId() { return android.R.id.icon1; } /** * Given a constraint and results, tries to find the constraint in those results, one at a time. * A foreground font color style will be applied to the section that matches the constraint. As * soon as a match has been found, no further matches are attempted. * * @param constraint A string that we will attempt to find within the results. * @param results Strings that may contain the constraint. The order given is the order used to * search for the constraint. * * @return An array of CharSequences, the length determined by the length of results. Each * CharSequence will either be a styled SpannableString or just the input String. */ protected CharSequence[] getStyledResults(@Nullable String constraint, String... results) { if (isAllWhitespace(constraint)) { return results; } CharSequence[] styledResults = new CharSequence[results.length]; boolean foundMatch = false; for (int i = 0; i < results.length; i++) { String result = results[i]; if (result == null) { continue; } if (!foundMatch) { int index = result.toLowerCase().indexOf(constraint.toLowerCase()); if (index != -1) { SpannableStringBuilder styled = SpannableStringBuilder.valueOf(result); ForegroundColorSpan highlightSpan = new ForegroundColorSpan(mContext.getResources().getColor( R.color.chips_dropdown_text_highlighted)); styled.setSpan(highlightSpan, index, index + constraint.length(), Spanned.SPAN_EXCLUSIVE_EXCLUSIVE); styledResults[i] = styled; foundMatch = true; continue; } } styledResults[i] = result; } return styledResults; } private static boolean isAllWhitespace(@Nullable String string) { if (TextUtils.isEmpty(string)) { return true; } for (int i = 0; i < string.length(); ++i) { if (!Character.isWhitespace(string.charAt(i))) { return false; } } return true; } /** * A holder class the view. Uses the getters in DropdownChipLayouter to find the id of the * corresponding views. */ protected class ViewHolder { public final TextView displayNameView; public final TextView destinationView; public final TextView destinationTypeView; public final ImageView imageView; public final ImageView deleteView; public final View topDivider; public final View bottomDivider; public ViewHolder(View view) { displayNameView = (TextView) view.findViewById(getDisplayNameResId()); destinationView = (TextView) view.findViewById(getDestinationResId()); destinationTypeView = (TextView) view.findViewById(getDestinationTypeResId()); imageView = (ImageView) view.findViewById(getPhotoResId()); deleteView = (ImageView) view.findViewById(getDeleteResId()); topDivider = view.findViewById(R.id.chip_autocomplete_top_divider); bottomDivider = view.findViewById(R.id.chip_autocomplete_bottom_divider); } } }