package com.mycardboarddreams.autocompletebubbletext; import android.content.Context; import android.graphics.Bitmap; import android.graphics.Canvas; import android.graphics.Paint; import android.graphics.Rect; import android.graphics.drawable.BitmapDrawable; import android.graphics.drawable.Drawable; import android.text.Editable; import android.text.InputType; import android.text.SpanWatcher; import android.text.Spannable; import android.text.SpannableStringBuilder; import android.text.TextUtils; import android.text.TextWatcher; import android.text.method.LinkMovementMethod; import android.text.method.TextKeyListener; import android.text.style.ClickableSpan; import android.text.style.ImageSpan; import android.util.AttributeSet; import android.util.Log; import android.util.SparseArray; import android.util.SparseBooleanArray; import android.view.View; import android.view.ViewGroup; import android.widget.AbsListView; import android.widget.AdapterView; import android.widget.ArrayAdapter; import android.widget.EditText; import android.widget.ListView; import android.widget.TextView; import java.util.ArrayList; import java.util.HashSet; import java.util.List; import java.util.Set; public class MultiSelectEditText<T extends MultiSelectItem> extends EditText { private static final String TAG = MultiSelectEditText.class.getSimpleName(); private int bubbleDrawableResource; private ListView listView; private ArrayAdapter<T> adapter; protected BubbleClickListener<T> listener; final private List<T> originalItems = new ArrayList<T>(); final private Set<String> checkedIds = new HashSet<String>(); private BubbleWatcher watcher; public MultiSelectEditText(Context context) { super(context); } public MultiSelectEditText(Context context, AttributeSet attrs) { super(context, attrs); } public MultiSelectEditText(Context context, AttributeSet attrs, int defStyle) { super(context, attrs, defStyle); } @Override protected void onFinishInflate() { super.onFinishInflate(); init(); } protected void init(){ setInitialComponents(); setFreezesText(true); setMovementMethod(LinkMovementMethod.getInstance()); listView.setOnItemClickListener(new AdapterView.OnItemClickListener() { @Override public void onItemClick(AdapterView<?> parent, View view, int position, long id) { SparseBooleanArray checked = listView.getCheckedItemPositions(); if(checked.get(position)) addItemChecked(adapter.getItem(position)); else removeItemChecked(adapter.getItem(position)); setString(); } }); watcher = new BubbleWatcher(); addTextChangedListener(watcher); setMinHeight(getPaddingBottom() + getPaddingTop() + calculateLineHeight()); } private void addItemChecked(T item){ checkedIds.add(item.getId()); } private void removeItemChecked(T item){ checkedIds.remove(item.getId()); } private void setCheckedItems() { for(int i = 0; i < adapter.getCount(); i++){ T item = adapter.getItem(i); if(checkedIds.contains(item.getId())) listView.setItemChecked(i, true); else listView.setItemChecked(i, false); } } private void updateFilteredItems(String lastValue){ if(originalItems != null) { List<T> filtered = filterData(originalItems, lastValue); adapter.clear(); for (T item : filtered) { adapter.add(item); } adapter.notifyDataSetChanged(); setCheckedItems(); } } private void setInitialComponents() { listView = onCreateListView(); if(listView == null) throw new IllegalStateException("The ListView cannot be null"); listView.setChoiceMode(AbsListView.CHOICE_MODE_MULTIPLE); adapter = onCreateAdapter(); if(adapter == null) throw new IllegalStateException("The Adapter cannot be null"); listView.setAdapter(adapter); bubbleDrawableResource = getBubbleResource(); if(bubbleDrawableResource == 0) throw new IllegalStateException("The resource drawable for the bubble cannot be null"); } private String getLastDelineatedValue() { String fullText = getText().toString(); if(TextUtils.isEmpty(fullText)) return ""; final String[] commaDelineated = fullText.split(getDelimiter().trim()); Editable spannedText = getEditableText(); ImageSpan[] spans = spannedText.getSpans(0, fullText.length(), ImageSpan.class); String lastString = commaDelineated[commaDelineated.length - 1].trim(); if(spans.length == 0) return lastString; int spanEndPoint = spannedText.getSpanEnd(spans[spans.length - 1]); if(spanEndPoint == spannedText.length() - 1) return ""; return lastString; } /** * Override this the replace the filtering of list items. * @param originalItems Original full list of items * @param lastCommaValue text after the last delimiter * @return a filtered list of the same items */ protected List<T> filterData(final List<T> originalItems, final String lastCommaValue){ if(TextUtils.isEmpty(lastCommaValue)){ return originalItems; } List<T> filtered = new ArrayList<T>(); for(T item : originalItems){ if(item.getReadableName().toLowerCase().startsWith(lastCommaValue.toLowerCase())) filtered.add(item); } return filtered; } /** * Override this to customize the adapter * @return a custom ArrayAdapter */ protected ArrayAdapter<T> onCreateAdapter(){ return new ArrayAdapter<T>(getContext(), getListItemLayout()){ @Override public View getView(int position, View convertView, ViewGroup parent) { View v = super.getView(position, convertView, parent); v.setBackgroundResource(R.drawable.list_item_background); return v; } }; } /** * Override this to customize the ListView * @return a custom ListView */ protected ListView onCreateListView(){ ListView lv = new ListView(getContext()); lv.setSelector(R.drawable.selector_list_checked); return lv; } protected int getListItemLayout(){ return android.R.layout.simple_list_item_1; } /** * Override this to customize the drawable resource behind the individual items * @return the resource id of the bubble drawable */ protected int getBubbleResource(){ return R.drawable.sample_bubble; } protected int calculateLineHeight(){ Drawable bubbleDrawable = getResources().getDrawable(getBubbleResource()); int lineHeight = getLineHeight(); Rect rect = new Rect(); if(bubbleDrawable.getPadding(rect)){ return lineHeight + rect.top + rect.bottom; } return lineHeight; } /** * Fetch the ListView associated with this MultiSelectEditText, that was created in onCreateListView() * @return the ListView associated with this MultiSelectEditText */ public final ListView getListView(){ return listView; } public void addAllItems(List<T> allItems){ clearAllItems(); originalItems.addAll(allItems); adapter.addAll(allItems); updateFilteredItems(getLastDelineatedValue()); } public void clearAllItems(){ originalItems.clear(); adapter.clear(); } public void removeItem(String itemName){ for(int i = 0; i < adapter.getCount(); i++) { T item = adapter.getItem(i); if(TextUtils.equals(item.getId(), itemName)){ listView.setItemChecked(i, false); removeItemChecked(item); } } setString(); updateFilteredItems(getLastDelineatedValue()); } public int getCheckedItemsCount(){ return listView.getCheckedItemPositions().size(); } /** * Override this to choose a different delimiter between the items * @return the delimiter string */ protected String getDelimiter(){ return ", "; } /** * Call this to set a listener for bubble clicks * @param listener */ public void setBubbleClickListener(BubbleClickListener<T> listener){ this.listener = listener; } public void setString(){ final SpannableStringBuilder sb = new SpannableStringBuilder(); SparseBooleanArray checked = listView.getCheckedItemPositions(); for (int i = 0; i < adapter.getCount(); i++) { if(!checked.get(i)) continue; final T item = adapter.getItem(i); String name = item.getReadableName(); TextView tv = createItemTextView(name); tv.setTextColor(getCurrentTextColor()); BitmapDrawable bd = convertViewToDrawable(tv); sb.append(name); final int start = sb.length() - name.length(); final int end = sb.length(); sb.setSpan(new BubbleSpan(bd, item.getId()), start, end, Spannable.SPAN_EXCLUSIVE_EXCLUSIVE); sb.setSpan(new ClickableSpan(){ @Override public void onClick(View view) { if(listener != null) listener.onClick(item); } }, start, end, Spannable.SPAN_EXCLUSIVE_EXCLUSIVE); sb.append(getDelimiter()); } setText(sb); int position = getText().length() - 1; if (position < 0) position = 0; setSelection(position); Editable editable = getText(); editable.setSpan(watcher, 0, editable.length(), 0); updateFilteredItems(getLastDelineatedValue()); } protected TextView createItemTextView(String text){ TextView tv = new TextView(getContext()); tv.setText(text); tv.setBackgroundResource(bubbleDrawableResource); return tv; } protected BitmapDrawable convertViewToDrawable(View textView) { int spec = MeasureSpec.makeMeasureSpec(0, MeasureSpec.UNSPECIFIED); textView.measure(spec, spec); textView.layout(0, 0, textView.getMeasuredWidth(), textView.getMeasuredHeight()); Bitmap b = Bitmap.createBitmap(textView.getMeasuredWidth(), textView.getMeasuredHeight(), Bitmap.Config.ARGB_8888); Canvas c = new Canvas(b); c.translate(-textView.getScrollX(), -textView.getScrollY()); textView.draw(c); textView.setDrawingCacheEnabled(true); Bitmap cacheBmp = textView.getDrawingCache(); Bitmap viewBmp = cacheBmp.copy(Bitmap.Config.ARGB_8888, true); textView.destroyDrawingCache(); BitmapDrawable bd = new BitmapDrawable(getContext().getResources(), viewBmp); bd.setBounds(0, 0, viewBmp.getWidth(), viewBmp.getHeight()); return bd; } private class BubbleWatcher implements SpanWatcher, TextWatcher { @Override public void onSpanAdded(Spannable text, Object what, int start, int end) { } @Override public void onSpanRemoved(Spannable text, Object what, int start, int end) { } @Override public void onSpanChanged(Spannable text, Object what, int ostart, int oend, int nstart, int nend) { if (what instanceof ImageSpan){ if(ostart != nstart && (ostart - nstart) == (oend - nend)) return; String readableName = ((ImageSpan) what).getSource(); removeItem(readableName); } } @Override public void beforeTextChanged(CharSequence s, int start, int count, int after) { } @Override public void onTextChanged(CharSequence s, int start, int before, int count) { } @Override public void afterTextChanged(Editable s) { updateFilteredItems(getLastDelineatedValue()); } } private static class BubbleSpan extends ImageSpan { public BubbleSpan(Drawable d, String source) { super(d, source); } @Override public void draw(Canvas canvas, CharSequence text, int start, int end, float x, int top, int y, int bottom, Paint paint) { if(text instanceof Spannable){ Spannable spanned = ((Spannable)text); ImageSpan[] includingSpans = spanned.getSpans(0, end, ImageSpan.class); if(includingSpans.length != 0){ ImageSpan lastSpan = includingSpans[includingSpans.length-1]; int endPoint = spanned.getSpanEnd(lastSpan); if(end == endPoint) super.draw(canvas, text, start, end, x, top, y, bottom, paint); } } else super.draw(canvas, text, start, end, x, top, y, bottom, paint); } } public interface BubbleClickListener<T>{ void onClick(T item); } }