Home | History | Annotate | Download | only in chips
      1 package com.android.ex.chips;
      2 
      3 import android.content.Context;
      4 import android.content.res.Resources;
      5 import android.graphics.Bitmap;
      6 import android.graphics.BitmapFactory;
      7 import android.graphics.Color;
      8 import android.graphics.PorterDuff;
      9 import android.graphics.drawable.Drawable;
     10 import android.graphics.drawable.StateListDrawable;
     11 import android.net.Uri;
     12 import android.support.annotation.DrawableRes;
     13 import android.support.annotation.IdRes;
     14 import android.support.annotation.LayoutRes;
     15 import android.support.annotation.Nullable;
     16 import android.support.v4.view.MarginLayoutParamsCompat;
     17 import android.text.SpannableStringBuilder;
     18 import android.text.Spanned;
     19 import android.text.TextUtils;
     20 import android.text.style.ForegroundColorSpan;
     21 import android.text.util.Rfc822Tokenizer;
     22 import android.view.LayoutInflater;
     23 import android.view.View;
     24 import android.view.View.OnClickListener;
     25 import android.view.ViewGroup;
     26 import android.view.ViewGroup.MarginLayoutParams;
     27 import android.widget.ImageView;
     28 import android.widget.TextView;
     29 
     30 import com.android.ex.chips.Queries.Query;
     31 
     32 /**
     33  * A class that inflates and binds the views in the dropdown list from
     34  * RecipientEditTextView.
     35  */
     36 public class DropdownChipLayouter {
     37     /**
     38      * The type of adapter that is requesting a chip layout.
     39      */
     40     public enum AdapterType {
     41         BASE_RECIPIENT,
     42         RECIPIENT_ALTERNATES,
     43         SINGLE_RECIPIENT
     44     }
     45 
     46     public interface ChipDeleteListener {
     47         void onChipDelete();
     48     }
     49 
     50     /**
     51      * Listener that handles the dismisses of the entries of the
     52      * {@link RecipientEntry#ENTRY_TYPE_PERMISSION_REQUEST} type.
     53      */
     54     public interface PermissionRequestDismissedListener {
     55 
     56         /**
     57          * Callback that occurs when user dismisses the item that asks user to grant permissions to
     58          * the app.
     59          */
     60         void onPermissionRequestDismissed();
     61     }
     62 
     63     private final LayoutInflater mInflater;
     64     private final Context mContext;
     65     private ChipDeleteListener mDeleteListener;
     66     private PermissionRequestDismissedListener mPermissionRequestDismissedListener;
     67     private Query mQuery;
     68     private int mAutocompleteDividerMarginStart;
     69 
     70     public DropdownChipLayouter(LayoutInflater inflater, Context context) {
     71         mInflater = inflater;
     72         mContext = context;
     73         mAutocompleteDividerMarginStart =
     74                 context.getResources().getDimensionPixelOffset(R.dimen.chip_wrapper_start_padding);
     75     }
     76 
     77     public void setQuery(Query query) {
     78         mQuery = query;
     79     }
     80 
     81     public void setDeleteListener(ChipDeleteListener listener) {
     82         mDeleteListener = listener;
     83     }
     84 
     85     public void setPermissionRequestDismissedListener(PermissionRequestDismissedListener listener) {
     86         mPermissionRequestDismissedListener = listener;
     87     }
     88 
     89     public void setAutocompleteDividerMarginStart(int autocompleteDividerMarginStart) {
     90         mAutocompleteDividerMarginStart = autocompleteDividerMarginStart;
     91     }
     92 
     93     /**
     94      * Layouts and binds recipient information to the view. If convertView is null, inflates a new
     95      * view with getItemLaytout().
     96      *
     97      * @param convertView The view to bind information to.
     98      * @param parent The parent to bind the view to if we inflate a new view.
     99      * @param entry The recipient entry to get information from.
    100      * @param position The position in the list.
    101      * @param type The adapter type that is requesting the bind.
    102      * @param constraint The constraint typed in the auto complete view.
    103      *
    104      * @return A view ready to be shown in the drop down list.
    105      */
    106     public View bindView(View convertView, ViewGroup parent, RecipientEntry entry, int position,
    107         AdapterType type, String constraint) {
    108         return bindView(convertView, parent, entry, position, type, constraint, null);
    109     }
    110 
    111     /**
    112      * See {@link #bindView(View, ViewGroup, RecipientEntry, int, AdapterType, String)}
    113      * @param deleteDrawable a {@link android.graphics.drawable.StateListDrawable} representing
    114      *     the delete icon. android.R.attr.state_activated should map to the delete icon, and the
    115      *     default state can map to a drawable of your choice (or null for no drawable).
    116      */
    117     public View bindView(View convertView, ViewGroup parent, RecipientEntry entry, int position,
    118             AdapterType type, String constraint, StateListDrawable deleteDrawable) {
    119         // Default to show all the information
    120         CharSequence[] styledResults = getStyledResults(constraint, entry);
    121         CharSequence displayName = styledResults[0];
    122         CharSequence destination = styledResults[1];
    123         boolean showImage = true;
    124         CharSequence destinationType = getDestinationType(entry);
    125 
    126         final View itemView = reuseOrInflateView(convertView, parent, type);
    127 
    128         final ViewHolder viewHolder = new ViewHolder(itemView);
    129 
    130         // Hide some information depending on the adapter type.
    131         switch (type) {
    132             case BASE_RECIPIENT:
    133                 if (TextUtils.isEmpty(displayName) || TextUtils.equals(displayName, destination)) {
    134                     displayName = destination;
    135 
    136                     // We only show the destination for secondary entries, so clear it only for the
    137                     // first level.
    138                     if (entry.isFirstLevel()) {
    139                         destination = null;
    140                     }
    141                 }
    142 
    143                 if (!entry.isFirstLevel()) {
    144                     displayName = null;
    145                     showImage = false;
    146                 }
    147 
    148                 // For BASE_RECIPIENT set all top dividers except for the first one to be GONE.
    149                 if (viewHolder.topDivider != null) {
    150                     viewHolder.topDivider.setVisibility(position == 0 ? View.VISIBLE : View.GONE);
    151                     MarginLayoutParamsCompat.setMarginStart(
    152                             (MarginLayoutParams) viewHolder.topDivider.getLayoutParams(),
    153                             mAutocompleteDividerMarginStart);
    154                 }
    155                 if (viewHolder.bottomDivider != null) {
    156                     MarginLayoutParamsCompat.setMarginStart(
    157                             (MarginLayoutParams) viewHolder.bottomDivider.getLayoutParams(),
    158                             mAutocompleteDividerMarginStart);
    159                 }
    160                 break;
    161             case RECIPIENT_ALTERNATES:
    162                 if (position != 0) {
    163                     displayName = null;
    164                     showImage = false;
    165                 }
    166                 break;
    167             case SINGLE_RECIPIENT:
    168                 if (!PhoneUtil.isPhoneNumber(entry.getDestination())) {
    169                     destination = Rfc822Tokenizer.tokenize(entry.getDestination())[0].getAddress();
    170                 }
    171                 destinationType = null;
    172         }
    173 
    174         // Bind the information to the view
    175         bindTextToView(displayName, viewHolder.displayNameView);
    176         bindTextToView(destination, viewHolder.destinationView);
    177         bindTextToView(destinationType, viewHolder.destinationTypeView);
    178         bindIconToView(showImage, entry, viewHolder.imageView, type);
    179         bindDrawableToDeleteView(deleteDrawable, entry.getDisplayName(), viewHolder.deleteView);
    180         bindIndicatorToView(
    181                 entry.getIndicatorIconId(), entry.getIndicatorText(), viewHolder.indicatorView);
    182         bindPermissionRequestDismissView(viewHolder.permissionRequestDismissView);
    183 
    184         // Hide some view groups depending on the entry type
    185         final int entryType = entry.getEntryType();
    186         if (entryType == RecipientEntry.ENTRY_TYPE_PERSON) {
    187             setViewVisibility(viewHolder.personViewGroup, View.VISIBLE);
    188             setViewVisibility(viewHolder.permissionViewGroup, View.GONE);
    189             setViewVisibility(viewHolder.permissionBottomDivider, View.GONE);
    190         } else if (entryType == RecipientEntry.ENTRY_TYPE_PERMISSION_REQUEST) {
    191             setViewVisibility(viewHolder.personViewGroup, View.GONE);
    192             setViewVisibility(viewHolder.permissionViewGroup, View.VISIBLE);
    193             setViewVisibility(viewHolder.permissionBottomDivider, View.VISIBLE);
    194         }
    195 
    196         return itemView;
    197     }
    198 
    199     /**
    200      * Returns a new view with {@link #getItemLayoutResId(AdapterType)}.
    201      */
    202     public View newView(AdapterType type) {
    203         return mInflater.inflate(getItemLayoutResId(type), null);
    204     }
    205 
    206     /**
    207      * Returns the same view, or inflates a new one if the given view was null.
    208      */
    209     protected View reuseOrInflateView(View convertView, ViewGroup parent, AdapterType type) {
    210         int itemLayout = getItemLayoutResId(type);
    211         switch (type) {
    212             case BASE_RECIPIENT:
    213             case RECIPIENT_ALTERNATES:
    214                 break;
    215             case SINGLE_RECIPIENT:
    216                 itemLayout = getAlternateItemLayoutResId(type);
    217                 break;
    218         }
    219         return convertView != null ? convertView : mInflater.inflate(itemLayout, parent, false);
    220     }
    221 
    222     /**
    223      * Binds the text to the given text view. If the text was null, hides the text view.
    224      */
    225     protected void bindTextToView(CharSequence text, TextView view) {
    226         if (view == null) {
    227             return;
    228         }
    229 
    230         if (text != null) {
    231             view.setText(text);
    232             view.setVisibility(View.VISIBLE);
    233         } else {
    234             view.setVisibility(View.GONE);
    235         }
    236     }
    237 
    238     /**
    239      * Binds the avatar icon to the image view. If we don't want to show the image, hides the
    240      * image view.
    241      */
    242     protected void bindIconToView(boolean showImage, RecipientEntry entry, ImageView view,
    243         AdapterType type) {
    244         if (view == null) {
    245             return;
    246         }
    247 
    248         if (showImage) {
    249             switch (type) {
    250                 case BASE_RECIPIENT:
    251                     byte[] photoBytes = entry.getPhotoBytes();
    252                     if (photoBytes != null && photoBytes.length > 0) {
    253                         final Bitmap photo = BitmapFactory.decodeByteArray(photoBytes, 0,
    254                             photoBytes.length);
    255                         view.setImageBitmap(photo);
    256                     } else {
    257                         view.setImageResource(getDefaultPhotoResId());
    258                     }
    259                     break;
    260                 case RECIPIENT_ALTERNATES:
    261                     Uri thumbnailUri = entry.getPhotoThumbnailUri();
    262                     if (thumbnailUri != null) {
    263                         // TODO: see if this needs to be done outside the main thread
    264                         // as it may be too slow to get immediately.
    265                         view.setImageURI(thumbnailUri);
    266                     } else {
    267                         view.setImageResource(getDefaultPhotoResId());
    268                     }
    269                     break;
    270                 case SINGLE_RECIPIENT:
    271                 default:
    272                     break;
    273             }
    274             view.setVisibility(View.VISIBLE);
    275         } else {
    276             view.setVisibility(View.GONE);
    277         }
    278     }
    279 
    280     protected void bindDrawableToDeleteView(final StateListDrawable drawable, String recipient,
    281             ImageView view) {
    282         if (view == null) {
    283             return;
    284         }
    285         if (drawable == null) {
    286             view.setVisibility(View.GONE);
    287         } else {
    288             final Resources res = mContext.getResources();
    289             view.setImageDrawable(drawable);
    290             view.setContentDescription(
    291                     res.getString(R.string.dropdown_delete_button_desc, recipient));
    292             if (mDeleteListener != null) {
    293                 view.setOnClickListener(new View.OnClickListener() {
    294                     @Override
    295                     public void onClick(View view) {
    296                         if (drawable.getCurrent() != null) {
    297                             mDeleteListener.onChipDelete();
    298                         }
    299                     }
    300                 });
    301             }
    302         }
    303     }
    304 
    305     protected void bindIndicatorToView(
    306             @DrawableRes int indicatorIconId, String indicatorText, TextView view) {
    307         if (view != null) {
    308             if (indicatorText != null || indicatorIconId != 0) {
    309                 view.setText(indicatorText);
    310                 view.setVisibility(View.VISIBLE);
    311                 final Drawable indicatorIcon;
    312                 if (indicatorIconId != 0) {
    313                     indicatorIcon = mContext.getDrawable(indicatorIconId).mutate();
    314                     indicatorIcon.setColorFilter(Color.WHITE, PorterDuff.Mode.SRC_IN);
    315                 } else {
    316                     indicatorIcon = null;
    317                 }
    318                 view.setCompoundDrawablesRelativeWithIntrinsicBounds(
    319                         indicatorIcon, null, null, null);
    320             } else {
    321                 view.setVisibility(View.GONE);
    322             }
    323         }
    324     }
    325 
    326     protected void bindPermissionRequestDismissView(ImageView view) {
    327         if (view == null) {
    328             return;
    329         }
    330         view.setOnClickListener(new OnClickListener() {
    331             @Override
    332             public void onClick(View v) {
    333                 if (mPermissionRequestDismissedListener != null) {
    334                     mPermissionRequestDismissedListener.onPermissionRequestDismissed();
    335                 }
    336             }
    337         });
    338     }
    339 
    340     protected void setViewVisibility(View view, int visibility) {
    341         if (view != null) {
    342             view.setVisibility(visibility);
    343         }
    344     }
    345 
    346     protected CharSequence getDestinationType(RecipientEntry entry) {
    347         return mQuery.getTypeLabel(mContext.getResources(), entry.getDestinationType(),
    348             entry.getDestinationLabel()).toString().toUpperCase();
    349     }
    350 
    351     /**
    352      * Returns a layout id for each item inside auto-complete list.
    353      *
    354      * Each View must contain two TextViews (for display name and destination) and one ImageView
    355      * (for photo). Ids for those should be available via {@link #getDisplayNameResId()},
    356      * {@link #getDestinationResId()}, and {@link #getPhotoResId()}.
    357      */
    358     protected @LayoutRes int getItemLayoutResId(AdapterType type) {
    359         switch (type) {
    360             case BASE_RECIPIENT:
    361                 return R.layout.chips_autocomplete_recipient_dropdown_item;
    362             case RECIPIENT_ALTERNATES:
    363                 return R.layout.chips_recipient_dropdown_item;
    364             default:
    365                 return R.layout.chips_recipient_dropdown_item;
    366         }
    367     }
    368 
    369     /**
    370      * Returns a layout id for each item inside alternate auto-complete list.
    371      *
    372      * Each View must contain two TextViews (for display name and destination) and one ImageView
    373      * (for photo). Ids for those should be available via {@link #getDisplayNameResId()},
    374      * {@link #getDestinationResId()}, and {@link #getPhotoResId()}.
    375      */
    376     protected @LayoutRes int getAlternateItemLayoutResId(AdapterType type) {
    377         switch (type) {
    378             case BASE_RECIPIENT:
    379                 return R.layout.chips_autocomplete_recipient_dropdown_item;
    380             case RECIPIENT_ALTERNATES:
    381                 return R.layout.chips_recipient_dropdown_item;
    382             default:
    383                 return R.layout.chips_recipient_dropdown_item;
    384         }
    385     }
    386 
    387     /**
    388      * Returns a resource ID representing an image which should be shown when ther's no relevant
    389      * photo is available.
    390      */
    391     protected @DrawableRes int getDefaultPhotoResId() {
    392         return R.drawable.ic_contact_picture;
    393     }
    394 
    395     /**
    396      * Returns an id for the ViewGroup in an item View that contains the person ui elements.
    397      */
    398     protected @IdRes int getPersonGroupResId() {
    399         return R.id.chip_person_wrapper;
    400     }
    401 
    402     /**
    403      * Returns an id for TextView in an item View for showing a display name. By default
    404      * {@link android.R.id#title} is returned.
    405      */
    406     protected @IdRes int getDisplayNameResId() {
    407         return android.R.id.title;
    408     }
    409 
    410     /**
    411      * Returns an id for TextView in an item View for showing a destination
    412      * (an email address or a phone number).
    413      * By default {@link android.R.id#text1} is returned.
    414      */
    415     protected @IdRes int getDestinationResId() {
    416         return android.R.id.text1;
    417     }
    418 
    419     /**
    420      * Returns an id for TextView in an item View for showing the type of the destination.
    421      * By default {@link android.R.id#text2} is returned.
    422      */
    423     protected @IdRes int getDestinationTypeResId() {
    424         return android.R.id.text2;
    425     }
    426 
    427     /**
    428      * Returns an id for ImageView in an item View for showing photo image for a person. In default
    429      * {@link android.R.id#icon} is returned.
    430      */
    431     protected @IdRes int getPhotoResId() {
    432         return android.R.id.icon;
    433     }
    434 
    435     /**
    436      * Returns an id for ImageView in an item View for showing the delete button. In default
    437      * {@link android.R.id#icon1} is returned.
    438      */
    439     protected @IdRes int getDeleteResId() { return android.R.id.icon1; }
    440 
    441     /**
    442      * Returns an id for the ViewGroup in an item View that contains the request permission ui
    443      * elements.
    444      */
    445     protected @IdRes int getPermissionGroupResId() {
    446         return R.id.chip_permission_wrapper;
    447     }
    448 
    449     /**
    450      * Returns an id for ImageView in an item View for dismissing the permission request. In default
    451      * {@link android.R.id#icon2} is returned.
    452      */
    453     protected @IdRes int getPermissionRequestDismissResId() {
    454         return android.R.id.icon2;
    455     }
    456 
    457     /**
    458      * Given a constraint and a recipient entry, tries to find the constraint in the name and
    459      * destination in the recipient entry. A foreground font color style will be applied to the
    460      * section that matches the constraint. As soon as a match has been found, no further matches
    461      * are attempted.
    462      *
    463      * @param constraint A string that we will attempt to find within the results.
    464      * @param entry The recipient entry to style results for.
    465      *
    466      * @return An array of CharSequences, the length determined by the length of results. Each
    467      *     CharSequence will either be a styled SpannableString or just the input String.
    468      */
    469     protected CharSequence[] getStyledResults(@Nullable String constraint, RecipientEntry entry) {
    470       return getStyledResults(constraint, entry.getDisplayName(), entry.getDestination());
    471     }
    472 
    473     /**
    474      * Given a constraint and results, tries to find the constraint in those results, one at a time.
    475      * A foreground font color style will be applied to the section that matches the constraint. As
    476      * soon as a match has been found, no further matches are attempted.
    477      *
    478      * @param constraint A string that we will attempt to find within the results.
    479      * @param results Strings that may contain the constraint. The order given is the order used to
    480      *     search for the constraint.
    481      *
    482      * @return An array of CharSequences, the length determined by the length of results. Each
    483      *     CharSequence will either be a styled SpannableString or just the input String.
    484      */
    485     protected CharSequence[] getStyledResults(@Nullable String constraint, String... results) {
    486         if (isAllWhitespace(constraint)) {
    487             return results;
    488         }
    489 
    490         CharSequence[] styledResults = new CharSequence[results.length];
    491         boolean foundMatch = false;
    492         for (int i = 0; i < results.length; i++) {
    493             String result = results[i];
    494             if (result == null) {
    495                 continue;
    496             }
    497 
    498             if (!foundMatch) {
    499                 int index = result.toLowerCase().indexOf(constraint.toLowerCase());
    500                 if (index != -1) {
    501                     SpannableStringBuilder styled = SpannableStringBuilder.valueOf(result);
    502                     ForegroundColorSpan highlightSpan =
    503                             new ForegroundColorSpan(mContext.getResources().getColor(
    504                                     R.color.chips_dropdown_text_highlighted));
    505                     styled.setSpan(highlightSpan,
    506                             index, index + constraint.length(), Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
    507                     styledResults[i] = styled;
    508                     foundMatch = true;
    509                     continue;
    510                 }
    511             }
    512             styledResults[i] = result;
    513         }
    514         return styledResults;
    515     }
    516 
    517     private static boolean isAllWhitespace(@Nullable String string) {
    518         if (TextUtils.isEmpty(string)) {
    519             return true;
    520         }
    521 
    522         for (int i = 0; i < string.length(); ++i) {
    523             if (!Character.isWhitespace(string.charAt(i))) {
    524                 return false;
    525             }
    526         }
    527 
    528         return true;
    529     }
    530 
    531     /**
    532      * A holder class the view. Uses the getters in DropdownChipLayouter to find the id of the
    533      * corresponding views.
    534      */
    535     protected class ViewHolder {
    536         public final ViewGroup personViewGroup;
    537         public final TextView displayNameView;
    538         public final TextView destinationView;
    539         public final TextView destinationTypeView;
    540         public final TextView indicatorView;
    541         public final ImageView imageView;
    542         public final ImageView deleteView;
    543         public final View topDivider;
    544         public final View bottomDivider;
    545         public final View permissionBottomDivider;
    546 
    547         public final ViewGroup permissionViewGroup;
    548         public final ImageView permissionRequestDismissView;
    549 
    550         public ViewHolder(View view) {
    551             personViewGroup = (ViewGroup) view.findViewById(getPersonGroupResId());
    552             displayNameView = (TextView) view.findViewById(getDisplayNameResId());
    553             destinationView = (TextView) view.findViewById(getDestinationResId());
    554             destinationTypeView = (TextView) view.findViewById(getDestinationTypeResId());
    555             imageView = (ImageView) view.findViewById(getPhotoResId());
    556             deleteView = (ImageView) view.findViewById(getDeleteResId());
    557             topDivider = view.findViewById(R.id.chip_autocomplete_top_divider);
    558 
    559             bottomDivider = view.findViewById(R.id.chip_autocomplete_bottom_divider);
    560             permissionBottomDivider = view.findViewById(R.id.chip_permission_bottom_divider);
    561 
    562             indicatorView = (TextView) view.findViewById(R.id.chip_indicator_text);
    563 
    564             permissionViewGroup = (ViewGroup) view.findViewById(getPermissionGroupResId());
    565             permissionRequestDismissView =
    566                     (ImageView) view.findViewById(getPermissionRequestDismissResId());
    567         }
    568     }
    569 }
    570