blob: 170008f1838083533d9ce0d643e6d1f31392143f [file] [log] [blame]
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);
}
}
}