diff options
Diffstat (limited to 'chips')
| -rw-r--r-- | chips/res/values/attrs.xml | 14 | ||||
| -rw-r--r-- | chips/src/com/android/ex/chips/BaseRecipientAdapter.java | 124 | ||||
| -rw-r--r-- | chips/src/com/android/ex/chips/DropdownChipLayouter.java | 274 | ||||
| -rw-r--r-- | chips/src/com/android/ex/chips/RecipientAlternatesAdapter.java | 53 | ||||
| -rw-r--r-- | chips/src/com/android/ex/chips/RecipientEditTextView.java | 277 | ||||
| -rw-r--r-- | chips/src/com/android/ex/chips/SingleRecipientArrayAdapter.java | 40 |
6 files changed, 481 insertions, 301 deletions
diff --git a/chips/res/values/attrs.xml b/chips/res/values/attrs.xml index 44c2500..6b26bcb 100644 --- a/chips/res/values/attrs.xml +++ b/chips/res/values/attrs.xml @@ -15,13 +15,17 @@ --> <resources> <declare-styleable name="RecipientEditTextView"> - <attr name="invalidChipBackground" format="reference" /> + <attr name="avatarPosition"> + <enum name="right" value="0" /> + <enum name="left" value="1" /> + </attr> <attr name="chipBackground" format="reference" /> <attr name="chipBackgroundPressed" format="reference" /> <attr name="chipDelete" format="reference" /> - <attr name="chipAlternatesLayout" format="reference" /> - <attr name="chipPadding" format="reference" /> - <attr name="chipHeight" format="reference" /> <attr name="chipFontSize" format="reference" /> + <attr name="chipHeight" format="reference" /> + <attr name="chipPadding" format="reference" /> + <attr name="disableDelete" format="boolean" /> + <attr name="invalidChipBackground" format="reference" /> </declare-styleable> -</resources> +</resources>
\ No newline at end of file diff --git a/chips/src/com/android/ex/chips/BaseRecipientAdapter.java b/chips/src/com/android/ex/chips/BaseRecipientAdapter.java index b779776..d52c7ca 100644 --- a/chips/src/com/android/ex/chips/BaseRecipientAdapter.java +++ b/chips/src/com/android/ex/chips/BaseRecipientAdapter.java @@ -23,8 +23,6 @@ import android.content.pm.PackageManager; import android.content.pm.PackageManager.NameNotFoundException; import android.content.res.Resources; import android.database.Cursor; -import android.graphics.Bitmap; -import android.graphics.BitmapFactory; import android.net.Uri; import android.os.AsyncTask; import android.os.Handler; @@ -43,12 +41,12 @@ import android.widget.AutoCompleteTextView; import android.widget.BaseAdapter; import android.widget.Filter; import android.widget.Filterable; -import android.widget.ImageView; -import android.widget.TextView; + +import com.android.ex.chips.DropdownChipLayouter.AdapterType; import java.io.ByteArrayOutputStream; -import java.io.InputStream; import java.io.IOException; +import java.io.InputStream; import java.util.ArrayList; import java.util.HashSet; import java.util.LinkedHashMap; @@ -460,6 +458,7 @@ public abstract class BaseRecipientAdapter extends BaseAdapter implements Filter private final LayoutInflater mInflater; private Account mAccount; private final int mPreferredMaxResultCount; + private DropdownChipLayouter mDropdownChipLayouter; /** * {@link #mEntries} is responsible for showing every result for this Adapter. To @@ -570,6 +569,15 @@ public abstract class BaseRecipientAdapter extends BaseAdapter implements Filter return mQueryType; } + public void setDropdownChipLayouter(DropdownChipLayouter dropdownChipLayouter) { + mDropdownChipLayouter = dropdownChipLayouter; + mDropdownChipLayouter.setQuery(mQuery); + } + + public DropdownChipLayouter getDropdownChipLayouter() { + return mDropdownChipLayouter; + } + /** * Set the account when known. Causes the search to prioritize contacts from that account. */ @@ -943,111 +951,9 @@ public abstract class BaseRecipientAdapter extends BaseAdapter implements Filter @Override public View getView(int position, View convertView, ViewGroup parent) { final RecipientEntry entry = getEntries().get(position); - String displayName = entry.getDisplayName(); - String destination = entry.getDestination(); - 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; - } - } - - final View itemView = convertView != null ? convertView : mInflater.inflate( - getItemLayout(), parent, false); - final TextView displayNameView = (TextView) itemView.findViewById(getDisplayNameId()); - final TextView destinationView = (TextView) itemView.findViewById(getDestinationId()); - final TextView destinationTypeView = (TextView) itemView - .findViewById(getDestinationTypeId()); - final ImageView imageView = (ImageView) itemView.findViewById(getPhotoId()); - displayNameView.setText(displayName); - if (!TextUtils.isEmpty(destination)) { - destinationView.setText(destination); - } else { - destinationView.setText(null); - } - if (destinationTypeView != null) { - final CharSequence destinationType = mQuery - .getTypeLabel(mContext.getResources(), entry.getDestinationType(), - entry.getDestinationLabel()).toString().toUpperCase(); - - destinationTypeView.setText(destinationType); - } - - if (entry.isFirstLevel()) { - displayNameView.setVisibility(View.VISIBLE); - if (imageView != null) { - imageView.setVisibility(View.VISIBLE); - final byte[] photoBytes = entry.getPhotoBytes(); - if (photoBytes != null) { - final Bitmap photo = BitmapFactory.decodeByteArray(photoBytes, 0, - photoBytes.length); - imageView.setImageBitmap(photo); - } else { - imageView.setImageResource(getDefaultPhotoResource()); - } - } - } else { - displayNameView.setVisibility(View.GONE); - if (imageView != null) { - imageView.setVisibility(View.INVISIBLE); - } - } - return itemView; - } - /** - * 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 #getDisplayNameId()}, - * {@link #getDestinationId()}, and {@link #getPhotoId()}. - */ - protected int getItemLayout() { - 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 int getDefaultPhotoResource() { - 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 int getDisplayNameId() { - 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 int getDestinationId() { - 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 int getDestinationTypeId() { - 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 int getPhotoId() { - return android.R.id.icon; + return mDropdownChipLayouter.bindView(convertView, parent, entry, position, + AdapterType.BASE_RECIPIENT, mCurrentConstraint.toString()); } public Account getAccount() { diff --git a/chips/src/com/android/ex/chips/DropdownChipLayouter.java b/chips/src/com/android/ex/chips/DropdownChipLayouter.java new file mode 100644 index 0000000..aceefab --- /dev/null +++ b/chips/src/com/android/ex/chips/DropdownChipLayouter.java @@ -0,0 +1,274 @@ +package com.android.ex.chips; + +import android.content.Context; +import android.graphics.Bitmap; +import android.graphics.BitmapFactory; +import android.net.Uri; +import android.text.TextUtils; +import android.text.util.Rfc822Tokenizer; +import android.view.LayoutInflater; +import android.view.View; +import android.view.ViewGroup; +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 + } + + private final LayoutInflater mInflater; + private final Context mContext; + private Query mQuery; + + public DropdownChipLayouter(LayoutInflater inflater, Context context) { + mInflater = inflater; + mContext = context; + } + + public void setQuery(Query query) { + mQuery = query; + } + + + /** + * 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) { + // Default to show all the information + String displayName = entry.getDisplayName(); + String destination = entry.getDestination(); + 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; + } + 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); + + return itemView; + } + + /** + * Returns a new view with {@link #getItemLayoutResId()}. + */ + public View newView() { + return mInflater.inflate(getItemLayoutResId(), 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(); + switch (type) { + case BASE_RECIPIENT: + case RECIPIENT_ALTERNATES: + break; + case SINGLE_RECIPIENT: + itemLayout = getAlternateItemLayoutResId(); + 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(entry.getPhotoThumbnailUri()); + } else { + view.setImageResource(getDefaultPhotoResId()); + } + break; + case SINGLE_RECIPIENT: + default: + break; + } + view.setVisibility(View.VISIBLE); + } else { + view.setVisibility(View.GONE); + } + } + + 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 int getItemLayoutResId() { + 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 int getAlternateItemLayoutResId() { + return R.layout.chips_alternate_item; + } + + /** + * Returns a resource ID representing an image which should be shown when ther's no relevant + * photo is available. + */ + protected 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 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 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 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 int getPhotoResId() { + return android.R.id.icon; + } + + /** + * 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 ViewHolder(View view) { + displayNameView = (TextView) view.findViewById(getDisplayNameResId()); + destinationView = (TextView) view.findViewById(getDestinationResId()); + destinationTypeView = (TextView) view.findViewById(getDestinationTypeResId()); + imageView = (ImageView) view.findViewById(getPhotoResId()); + } + } +} diff --git a/chips/src/com/android/ex/chips/RecipientAlternatesAdapter.java b/chips/src/com/android/ex/chips/RecipientAlternatesAdapter.java index b9a7c80..547a76b 100644 --- a/chips/src/com/android/ex/chips/RecipientAlternatesAdapter.java +++ b/chips/src/com/android/ex/chips/RecipientAlternatesAdapter.java @@ -27,15 +27,13 @@ import android.text.TextUtils; import android.text.util.Rfc822Token; import android.text.util.Rfc822Tokenizer; import android.util.Log; -import android.view.LayoutInflater; import android.view.View; import android.view.ViewGroup; import android.widget.CursorAdapter; -import android.widget.ImageView; -import android.widget.TextView; import com.android.ex.chips.BaseRecipientAdapter.DirectoryListQuery; import com.android.ex.chips.BaseRecipientAdapter.DirectorySearchParams; +import com.android.ex.chips.DropdownChipLayouter.AdapterType; import com.android.ex.chips.Queries.Query; import java.util.ArrayList; @@ -51,7 +49,6 @@ import java.util.Set; */ public class RecipientAlternatesAdapter extends CursorAdapter { static final int MAX_LOOKUPS = 50; - private final LayoutInflater mLayoutInflater; private final long mCurrentId; @@ -64,6 +61,7 @@ public class RecipientAlternatesAdapter extends CursorAdapter { public static final int QUERY_TYPE_EMAIL = 0; public static final int QUERY_TYPE_PHONE = 1; private Query mQuery; + private DropdownChipLayouter mDropdownChipLayouter; public interface RecipientMatchCallback { public void matchesFound(Map<String, RecipientEntry> results); @@ -328,14 +326,9 @@ public class RecipientAlternatesAdapter extends CursorAdapter { } public RecipientAlternatesAdapter(Context context, long contactId, long currentId, - OnCheckedItemChangedListener listener) { - this(context, contactId, currentId, QUERY_TYPE_EMAIL, listener); - } - - public RecipientAlternatesAdapter(Context context, long contactId, long currentId, - int queryMode, OnCheckedItemChangedListener listener) { + int queryMode, OnCheckedItemChangedListener listener, + DropdownChipLayouter dropdownChipLayouter) { super(context, getCursorForConstruction(context, contactId, queryMode), 0); - mLayoutInflater = LayoutInflater.from(context); mCurrentId = currentId; mCheckedItemChangedListener = listener; @@ -347,6 +340,8 @@ public class RecipientAlternatesAdapter extends CursorAdapter { mQuery = Queries.EMAIL; Log.e(TAG, "Unsupported query type: " + queryMode); } + + mDropdownChipLayouter = dropdownChipLayouter; } private static Cursor getCursorForConstruction(Context context, long contactId, int queryType) { @@ -439,7 +434,7 @@ public class RecipientAlternatesAdapter extends CursorAdapter { Cursor cursor = getCursor(); cursor.moveToPosition(position); if (convertView == null) { - convertView = newView(); + convertView = mDropdownChipLayouter.newView(); } if (cursor.getLong(Queries.Query.DATA_ID) == mCurrentId) { mCheckedItemPosition = position; @@ -451,44 +446,18 @@ public class RecipientAlternatesAdapter extends CursorAdapter { return convertView; } - // TODO: this is VERY similar to the BaseRecipientAdapter. Can we combine - // somehow? @Override public void bindView(View view, Context context, Cursor cursor) { int position = cursor.getPosition(); - - TextView display = (TextView) view.findViewById(android.R.id.title); - ImageView imageView = (ImageView) view.findViewById(android.R.id.icon); RecipientEntry entry = getRecipientEntry(position); - if (position == 0) { - display.setText(cursor.getString(Queries.Query.NAME)); - display.setVisibility(View.VISIBLE); - // TODO: see if this needs to be done outside the main thread - // as it may be too slow to get immediately. - imageView.setImageURI(entry.getPhotoThumbnailUri()); - imageView.setVisibility(View.VISIBLE); - } else { - display.setVisibility(View.GONE); - imageView.setVisibility(View.GONE); - } - TextView destination = (TextView) view.findViewById(android.R.id.text1); - destination.setText(cursor.getString(Queries.Query.DESTINATION)); - - TextView destinationType = (TextView) view.findViewById(android.R.id.text2); - if (destinationType != null) { - destinationType.setText(mQuery.getTypeLabel(context.getResources(), - cursor.getInt(Queries.Query.DESTINATION_TYPE), - cursor.getString(Queries.Query.DESTINATION_LABEL)).toString().toUpperCase()); - } + + mDropdownChipLayouter.bindView(view, null, entry, position, + AdapterType.RECIPIENT_ALTERNATES, null); } @Override public View newView(Context context, Cursor cursor, ViewGroup parent) { - return newView(); - } - - private View newView() { - return mLayoutInflater.inflate(R.layout.chips_recipient_dropdown_item, null); + return mDropdownChipLayouter.newView(); } /*package*/ static interface OnCheckedItemChangedListener { diff --git a/chips/src/com/android/ex/chips/RecipientEditTextView.java b/chips/src/com/android/ex/chips/RecipientEditTextView.java index 3cba87b..b355c1b 100644 --- a/chips/src/com/android/ex/chips/RecipientEditTextView.java +++ b/chips/src/com/android/ex/chips/RecipientEditTextView.java @@ -30,6 +30,7 @@ import android.graphics.Bitmap; import android.graphics.BitmapFactory; import android.graphics.Canvas; import android.graphics.Matrix; +import android.graphics.Paint; import android.graphics.Point; import android.graphics.Rect; import android.graphics.RectF; @@ -149,14 +150,24 @@ public class RecipientEditTextView extends MultiAutoCompleteTextView implements private int mChipPadding; + /** + * Enumerator for avatar position. See attr.xml for more details. + * 0 for right, 1 for left. + */ + private int mAvatarPosition; + + private static final int AVATAR_POSITION_RIGHT = 0; + + private static final int AVATAR_POSITION_LEFT = 1; + + private boolean mDisableDelete; + private Tokenizer mTokenizer; private Validator mValidator; private DrawableRecipientChip mSelectedChip; - private int mAlternatesLayout; - private Bitmap mDefaultContactPhoto; private ImageSpan mMoreChip; @@ -254,6 +265,8 @@ public class RecipientEditTextView extends MultiAutoCompleteTextView implements private boolean mAttachedToWindow; + private DropdownChipLayouter mDropdownChipLayouter; + public RecipientEditTextView(Context context, AttributeSet attrs) { super(context, attrs); setChipDimensions(context, attrs); @@ -293,6 +306,12 @@ public class RecipientEditTextView extends MultiAutoCompleteTextView implements addTextChangedListener(mTextWatcher); mGestureDetector = new GestureDetector(context, this); setOnEditorActionListener(this); + + setDropdownChipLayouter(new DropdownChipLayouter(LayoutInflater.from(context), context)); + } + + protected void setDropdownChipLayouter(DropdownChipLayouter dropdownChipLayouter) { + mDropdownChipLayouter = dropdownChipLayouter; } @Override @@ -433,17 +452,18 @@ public class RecipientEditTextView extends MultiAutoCompleteTextView implements @Override public <T extends ListAdapter & Filterable> void setAdapter(T adapter) { super.setAdapter(adapter); - ((BaseRecipientAdapter) adapter) - .registerUpdateObserver(new BaseRecipientAdapter.EntriesUpdatedObserver() { - @Override - public void onChanged(List<RecipientEntry> entries) { - // Scroll the chips field to the top of the screen so - // that the user can see as many results as possible. - if (entries != null && entries.size() > 0) { - scrollBottomIntoView(); - } - } - }); + BaseRecipientAdapter baseAdapter = (BaseRecipientAdapter) adapter; + baseAdapter.registerUpdateObserver(new BaseRecipientAdapter.EntriesUpdatedObserver() { + @Override + public void onChanged(List<RecipientEntry> entries) { + // Scroll the chips field to the top of the screen so + // that the user can see as many results as possible. + if (entries != null && entries.size() > 0) { + scrollBottomIntoView(); + } + } + }); + baseAdapter.setDropdownChipLayouter(mDropdownChipLayouter); } private void scrollBottomIntoView() { @@ -543,124 +563,129 @@ public class RecipientEditTextView extends MultiAutoCompleteTextView implements TextUtils.TruncateAt.END); } + /** + * Creates a bitmap of the given contact on a selected chip. + * + * @param contact The recipient entry to pull data from. + * @param paint The paint to use to draw the bitmap. + */ private Bitmap createSelectedChip(RecipientEntry contact, TextPaint paint) { - // Ellipsize the text so that it takes AT MOST the entire width of the - // autocomplete text entry area. Make sure to leave space for padding - // on the sides. - int height = (int) mChipHeight; - int deleteWidth = height; - float[] widths = new float[1]; - paint.getTextWidths(" ", widths); - CharSequence ellipsizedText = ellipsizeText(createChipDisplayText(contact), paint, - calculateAvailableWidth() - deleteWidth - widths[0]); - - // Make sure there is a minimum chip width so the user can ALWAYS - // tap a chip without difficulty. - int width = Math.max(deleteWidth * 2, (int) Math.floor(paint.measureText(ellipsizedText, 0, - ellipsizedText.length())) - + (mChipPadding * 2) + deleteWidth); - - // Create the background of the chip. - Bitmap tmpBitmap = Bitmap.createBitmap(width, height, Bitmap.Config.ARGB_8888); - Canvas canvas = new Canvas(tmpBitmap); - if (mChipBackgroundPressed != null) { - mChipBackgroundPressed.setBounds(0, 0, width, height); - mChipBackgroundPressed.draw(canvas); - paint.setColor(sSelectedTextColor); - // Vertically center the text in the chip. - canvas.drawText(ellipsizedText, 0, ellipsizedText.length(), mChipPadding, - getTextYOffset((String) ellipsizedText, paint, height), paint); - // Make the delete a square. - Rect backgroundPadding = new Rect(); - mChipBackgroundPressed.getPadding(backgroundPadding); - mChipDelete.setBounds(width - deleteWidth + backgroundPadding.left, - 0 + backgroundPadding.top, - width - backgroundPadding.right, - height - backgroundPadding.bottom); - mChipDelete.draw(canvas); + paint.setColor(sSelectedTextColor); + Bitmap photo; + if (mDisableDelete) { + // Show the avatar instead if we don't want to delete + photo = getAvatarIcon(contact); } else { - Log.w(TAG, "Unable to draw a background for the chips as it was never set"); + photo = ((BitmapDrawable) mChipDelete).getBitmap(); } - return tmpBitmap; + return createChipBitmap(contact, paint, photo, mChipBackgroundPressed); } - + /** + * Creates a bitmap of the given contact on a selected chip. + * + * @param contact The recipient entry to pull data from. + * @param paint The paint to use to draw the bitmap. + */ + // TODO: Is leaveBlankIconSpacer obsolete now that we have left and right attributes? private Bitmap createUnselectedChip(RecipientEntry contact, TextPaint paint, boolean leaveBlankIconSpacer) { + Drawable background = getChipBackground(contact); + Bitmap photo = getAvatarIcon(contact); + paint.setColor(getContext().getResources().getColor(android.R.color.black)); + return createChipBitmap(contact, paint, photo, background); + } + + private Bitmap createChipBitmap(RecipientEntry contact, TextPaint paint, Bitmap icon, + Drawable background) { + if (background == null) { + Log.w(TAG, "Unable to draw a background for the chips as it was never set"); + Bitmap.createBitmap((int) mChipHeight * 2, (int) mChipHeight, Bitmap.Config.ARGB_8888); + } + + Rect backgroundPadding = new Rect(); + background.getPadding(backgroundPadding); + // Ellipsize the text so that it takes AT MOST the entire width of the // autocomplete text entry area. Make sure to leave space for padding // on the sides. int height = (int) mChipHeight; - int iconWidth = height; + // Since the icon is a square, it's width is equal to the maximum height it can be inside + // the chip. + int iconWidth = height - backgroundPadding.top - backgroundPadding.bottom; float[] widths = new float[1]; paint.getTextWidths(" ", widths); CharSequence ellipsizedText = ellipsizeText(createChipDisplayText(contact), paint, calculateAvailableWidth() - iconWidth - widths[0]); + int textWidth = (int) paint.measureText(ellipsizedText, 0, ellipsizedText.length()); + // Make sure there is a minimum chip width so the user can ALWAYS // tap a chip without difficulty. - int width = Math.max(iconWidth * 2, (int) Math.floor(paint.measureText(ellipsizedText, 0, - ellipsizedText.length())) - + (mChipPadding * 2) + iconWidth); + int width = Math.max(iconWidth * 2, textWidth + (mChipPadding * 2) + iconWidth + + backgroundPadding.left + backgroundPadding.right); // Create the background of the chip. Bitmap tmpBitmap = Bitmap.createBitmap(width, height, Bitmap.Config.ARGB_8888); Canvas canvas = new Canvas(tmpBitmap); - Drawable background = getChipBackground(contact); - if (background != null) { - background.setBounds(0, 0, width, height); - background.draw(canvas); - - // Don't draw photos for recipients that have been typed in OR generated on the fly. - long contactId = contact.getContactId(); - boolean drawPhotos = isPhoneQuery() ? - contactId != RecipientEntry.INVALID_CONTACT - : (contactId != RecipientEntry.INVALID_CONTACT - && (contactId != RecipientEntry.GENERATED_CONTACT && - !TextUtils.isEmpty(contact.getDisplayName()))); - if (drawPhotos) { - byte[] photoBytes = contact.getPhotoBytes(); - // There may not be a photo yet if anything but the first contact address - // was selected. - if (photoBytes == null && contact.getPhotoThumbnailUri() != null) { - // TODO: cache this in the recipient entry? - getAdapter().fetchPhoto(contact, contact.getPhotoThumbnailUri()); - photoBytes = contact.getPhotoBytes(); - } - Bitmap photo; - if (photoBytes != null) { - photo = BitmapFactory.decodeByteArray(photoBytes, 0, photoBytes.length); - } else { - // TODO: can the scaled down default photo be cached? - photo = mDefaultContactPhoto; - } - // Draw the photo on the left side. - if (photo != null) { - RectF src = new RectF(0, 0, photo.getWidth(), photo.getHeight()); - Rect backgroundPadding = new Rect(); - mChipBackground.getPadding(backgroundPadding); - RectF dst = new RectF(width - iconWidth + backgroundPadding.left, - 0 + backgroundPadding.top, - width - backgroundPadding.right, - height - backgroundPadding.bottom); - Matrix matrix = new Matrix(); - matrix.setRectToRect(src, dst, Matrix.ScaleToFit.FILL); - canvas.drawBitmap(photo, matrix, paint); - } - } else if (!leaveBlankIconSpacer || isPhoneQuery()) { - iconWidth = 0; - } - paint.setColor(getContext().getResources().getColor(android.R.color.black)); - // Vertically center the text in the chip. - canvas.drawText(ellipsizedText, 0, ellipsizedText.length(), mChipPadding, - getTextYOffset((String)ellipsizedText, paint, height), paint); - } else { - Log.w(TAG, "Unable to draw a background for the chips as it was never set"); + // Draw the background drawable + background.setBounds(0, 0, width, height); + background.draw(canvas); + // Draw the text vertically aligned + int textX = mAvatarPosition == AVATAR_POSITION_RIGHT ? + mChipPadding + backgroundPadding.left : + width - backgroundPadding.right - mChipPadding - textWidth; + canvas.drawText(ellipsizedText, 0, ellipsizedText.length(), + textX, getTextYOffset(ellipsizedText.toString(), paint, height), paint); + if (icon != null) { + // Draw the icon + int iconX = mAvatarPosition == AVATAR_POSITION_RIGHT ? + width - backgroundPadding.right - iconWidth : + backgroundPadding.left; + RectF src = new RectF(0, 0, icon.getWidth(), icon.getHeight()); + RectF dst = new RectF(iconX, + 0 + backgroundPadding.top, + iconX + iconWidth, + height - backgroundPadding.bottom); + drawIconOnCanvas(icon, canvas, paint, src, dst); } return tmpBitmap; } /** + * Returns the avatar icon to use for this recipient entry. Returns null if we don't want to + * draw an icon for this recipient. + */ + private Bitmap getAvatarIcon(RecipientEntry contact) { + // Don't draw photos for recipients that have been typed in OR generated on the fly. + long contactId = contact.getContactId(); + boolean drawPhotos = isPhoneQuery() ? + contactId != RecipientEntry.INVALID_CONTACT + : (contactId != RecipientEntry.INVALID_CONTACT + && (contactId != RecipientEntry.GENERATED_CONTACT && + !TextUtils.isEmpty(contact.getDisplayName()))); + + if (drawPhotos) { + byte[] photoBytes = contact.getPhotoBytes(); + // There may not be a photo yet if anything but the first contact address + // was selected. + if (photoBytes == null && contact.getPhotoThumbnailUri() != null) { + // TODO: cache this in the recipient entry? + getAdapter().fetchPhoto(contact, contact.getPhotoThumbnailUri()); + photoBytes = contact.getPhotoBytes(); + } + if (photoBytes != null) { + return BitmapFactory.decodeByteArray(photoBytes, 0, photoBytes.length); + } else { + // TODO: can the scaled down default photo be cached? + return mDefaultContactPhoto; + } + } + + return null; + } + + /** * Get the background drawable for a RecipientChip. */ // Visible for testing. @@ -668,13 +693,26 @@ public class RecipientEditTextView extends MultiAutoCompleteTextView implements return contact.isValid() ? mChipBackground : mInvalidChipBackground; } - private static float getTextYOffset(String text, TextPaint paint, int height) { + /** + * Given a height, returns a Y offset that will draw the text in the middle of the height. + */ + protected float getTextYOffset(String text, TextPaint paint, int height) { Rect bounds = new Rect(); paint.getTextBounds(text, 0, text.length(), bounds); int textHeight = bounds.bottom - bounds.top ; return height - ((height - textHeight) / 2) - (int)paint.descent(); } + /** + * Draws the icon onto the canvas given the source rectangle of the bitmap and the destination + * rectangle of the canvas. + */ + protected void drawIconOnCanvas(Bitmap icon, Canvas canvas, Paint paint, RectF src, RectF dst) { + Matrix matrix = new Matrix(); + matrix.setRectToRect(src, dst, Matrix.ScaleToFit.FILL); + canvas.drawBitmap(icon, matrix, paint); + } + private DrawableRecipientChip constructChipSpan(RecipientEntry contact, boolean pressed, boolean leaveIconSpace) throws NullPointerException { if (mChipBackground == null) { @@ -749,11 +787,6 @@ public class RecipientEditTextView extends MultiAutoCompleteTextView implements if (mChipPadding == -1) { mChipPadding = (int) r.getDimension(R.dimen.chip_padding); } - mAlternatesLayout = a.getResourceId(R.styleable.RecipientEditTextView_chipAlternatesLayout, - -1); - if (mAlternatesLayout == -1) { - mAlternatesLayout = R.layout.chips_alternate_item; - } mDefaultContactPhoto = BitmapFactory.decodeResource(r, R.drawable.ic_contact_picture); @@ -772,6 +805,9 @@ public class RecipientEditTextView extends MultiAutoCompleteTextView implements if (mInvalidChipBackground == null) { mInvalidChipBackground = r.getDrawable(R.drawable.chip_background_invalid); } + mAvatarPosition = a.getInt(R.styleable.RecipientEditTextView_avatarPosition, 0); + mDisableDelete = a.getBoolean(R.styleable.RecipientEditTextView_avatarPosition, false); + mLineSpacingExtra = r.getDimension(R.dimen.line_spacing_extra); mMaxLines = r.getInteger(R.integer.chips_max_lines); TypedValue tv = new TypedValue(); @@ -779,6 +815,7 @@ public class RecipientEditTextView extends MultiAutoCompleteTextView implements mActionBarHeight = TypedValue.complexToDimensionPixelSize(tv.data, getResources() .getDisplayMetrics()); } + a.recycle(); } @@ -798,6 +835,10 @@ public class RecipientEditTextView extends MultiAutoCompleteTextView implements mChipHeight = height; } + public float getChipHeight() { + return mChipHeight; + } + /** * Set whether to shrink the recipients field such that at most * one line of recipients chips are shown when the field loses @@ -1511,12 +1552,12 @@ public class RecipientEditTextView extends MultiAutoCompleteTextView implements private ListAdapter createAlternatesAdapter(DrawableRecipientChip chip) { return new RecipientAlternatesAdapter(getContext(), chip.getContactId(), chip.getDataId(), - getAdapter().getQueryType(), this); + getAdapter().getQueryType(), this, mDropdownChipLayouter); } private ListAdapter createSingleAddressAdapter(DrawableRecipientChip currentChip) { - return new SingleRecipientArrayAdapter(getContext(), mAlternatesLayout, currentChip - .getEntry()); + return new SingleRecipientArrayAdapter(getContext(), currentChip.getEntry(), + mDropdownChipLayouter); } @Override @@ -2121,14 +2162,20 @@ public class RecipientEditTextView extends MultiAutoCompleteTextView implements // Figure out the bounds of this chip and whether or not // the user clicked in the X portion. // TODO: Should x and y be used, or removed? - return chip.isSelected() && offset == getChipEnd(chip); + if (mDisableDelete) { + return false; + } + + return chip.isSelected() && + ((mAvatarPosition == 0 && offset == getChipEnd(chip)) || + (mAvatarPosition != 0 && offset == getChipStart(chip))); } /** * Remove the chip and any text associated with it from the RecipientEditTextView. */ // Visible for testing. - /*pacakge*/ void removeChip(DrawableRecipientChip chip) { + /* package */void removeChip(DrawableRecipientChip chip) { Spannable spannable = getSpannable(); int spanStart = spannable.getSpanStart(chip); int spanEnd = spannable.getSpanEnd(chip); diff --git a/chips/src/com/android/ex/chips/SingleRecipientArrayAdapter.java b/chips/src/com/android/ex/chips/SingleRecipientArrayAdapter.java index 0571a4e..985953f 100644 --- a/chips/src/com/android/ex/chips/SingleRecipientArrayAdapter.java +++ b/chips/src/com/android/ex/chips/SingleRecipientArrayAdapter.java @@ -17,47 +17,27 @@ package com.android.ex.chips; import android.content.Context; -import android.text.util.Rfc822Tokenizer; -import android.view.LayoutInflater; import android.view.View; import android.view.ViewGroup; import android.widget.ArrayAdapter; -import android.widget.ImageView; -import android.widget.TextView; -class SingleRecipientArrayAdapter extends ArrayAdapter<RecipientEntry> { - private int mLayoutId; +import com.android.ex.chips.DropdownChipLayouter.AdapterType; - private final LayoutInflater mLayoutInflater; +class SingleRecipientArrayAdapter extends ArrayAdapter<RecipientEntry> { + private final DropdownChipLayouter mDropdownChipLayouter; - public SingleRecipientArrayAdapter(Context context, int resourceId, RecipientEntry entry) { - super(context, resourceId, new RecipientEntry[] { + public SingleRecipientArrayAdapter(Context context, RecipientEntry entry, + DropdownChipLayouter dropdownChipLayouter) { + super(context, dropdownChipLayouter.getAlternateItemLayoutResId(), new RecipientEntry[] { entry }); - mLayoutInflater = LayoutInflater.from(context); - mLayoutId = resourceId; + + mDropdownChipLayouter = dropdownChipLayouter; } @Override public View getView(int position, View convertView, ViewGroup parent) { - if (convertView == null) { - convertView = newView(); - } - bindView(convertView, getItem(position)); - return convertView; - } - - private View newView() { - return mLayoutInflater.inflate(mLayoutId, null); - } - - private static void bindView(View view, RecipientEntry entry) { - TextView display = (TextView) view.findViewById(android.R.id.title); - ImageView imageView = (ImageView) view.findViewById(android.R.id.icon); - display.setText(entry.getDisplayName()); - display.setVisibility(View.VISIBLE); - imageView.setVisibility(View.VISIBLE); - TextView destination = (TextView) view.findViewById(android.R.id.text1); - destination.setText(Rfc822Tokenizer.tokenize(entry.getDestination())[0].getAddress()); + return mDropdownChipLayouter.bindView(convertView, parent, getItem(position), position, + AdapterType.SINGLE_RECIPIENT, null); } } |
