| 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); |
| } |
| } |
| } |