summaryrefslogtreecommitdiffstats
path: root/chips/src/com/android
diff options
context:
space:
mode:
authorMindy Pereira <mindyp@google.com>2011-05-31 20:52:20 -0700
committerMindy Pereira <mindyp@google.com>2011-06-01 09:18:25 -0700
commit5e6306039bca3ad3897182a8d8bf45abe80680e5 (patch)
tree8e673d95ca124dabeac39b8496ae0d4f39d5df54 /chips/src/com/android
parent6594c740b78d9d9aa97489bb3b307fa94c2fc905 (diff)
downloadandroid_frameworks_ex-5e6306039bca3ad3897182a8d8bf45abe80680e5.tar.gz
android_frameworks_ex-5e6306039bca3ad3897182a8d8bf45abe80680e5.tar.bz2
android_frameworks_ex-5e6306039bca3ad3897182a8d8bf45abe80680e5.zip
Create wrapper for RecipientEditTextView.
Hide that this isn;t just a normal MultiAutoCompleteTextView by wrapping getRecipients in getText Change-Id: Ib11777b4fbf55bfb9aafb1cda18fd1ed246895ad
Diffstat (limited to 'chips/src/com/android')
-rw-r--r--chips/src/com/android/ex/chips/ChipsUtil.java9
-rw-r--r--chips/src/com/android/ex/chips/RecipientEditTextView.java625
-rw-r--r--chips/src/com/android/ex/chips/RecipientEditTextViewInner.java654
3 files changed, 677 insertions, 611 deletions
diff --git a/chips/src/com/android/ex/chips/ChipsUtil.java b/chips/src/com/android/ex/chips/ChipsUtil.java
index e14363b..0acb193 100644
--- a/chips/src/com/android/ex/chips/ChipsUtil.java
+++ b/chips/src/com/android/ex/chips/ChipsUtil.java
@@ -17,13 +17,10 @@
package com.android.ex.chips;
import android.content.ContentResolver;
-import android.content.ContentUris;
import android.content.ContentValues;
import android.content.Context;
-import android.net.Uri;
import android.os.Build;
import android.provider.ContactsContract;
-import android.provider.ContactsContract.Contacts;
import android.provider.ContactsContract.Data;
import android.text.TextUtils;
import android.widget.MultiAutoCompleteTextView;
@@ -44,15 +41,15 @@ public class ChipsUtil {
public static boolean tryUpdateRecencyInfo(MultiAutoCompleteTextView... views) {
for (MultiAutoCompleteTextView view : views) {
- if (view instanceof RecipientEditTextView) {
- updateRecencyInfo((RecipientEditTextView)view);
+ if (view instanceof RecipientEditTextViewInner) {
+ updateRecencyInfo((RecipientEditTextViewInner)view);
}
}
return true;
}
// TODO: check this works
- public static void updateRecencyInfo(RecipientEditTextView view) {
+ public static void updateRecencyInfo(RecipientEditTextViewInner view) {
final Context context = view.getContext();
final ContentResolver resolver = context.getContentResolver();
final long currentTimeMillis = System.currentTimeMillis();
diff --git a/chips/src/com/android/ex/chips/RecipientEditTextView.java b/chips/src/com/android/ex/chips/RecipientEditTextView.java
index abc7afb..910f2c4 100644
--- a/chips/src/com/android/ex/chips/RecipientEditTextView.java
+++ b/chips/src/com/android/ex/chips/RecipientEditTextView.java
@@ -17,637 +17,52 @@
package com.android.ex.chips;
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.os.Handler;
import android.text.Editable;
-import android.text.Layout;
-import android.text.Spannable;
-import android.text.SpannableString;
-import android.text.Spanned;
-import android.text.TextPaint;
-import android.text.TextUtils;
-import android.text.method.QwertyKeyListener;
-import android.text.style.ImageSpan;
import android.util.AttributeSet;
-import android.util.Log;
-import android.view.KeyEvent;
-import android.view.MotionEvent;
-import android.view.View;
-import android.widget.AdapterView;
-import android.widget.AdapterView.OnItemClickListener;
-import android.widget.Filter.FilterListener;
-import android.widget.ListPopupWindow;
+import android.widget.Filterable;
+import android.widget.FrameLayout;
+import android.widget.ListAdapter;
import android.widget.MultiAutoCompleteTextView;
-import android.widget.PopupWindow.OnDismissListener;
import java.util.Collection;
-import java.util.HashSet;
-import java.util.List;
-import java.util.Set;
-
-import java.util.ArrayList;
/**
* RecipientEditTextView is an auto complete text view for use with applications
* that use the new Chips UI for addressing a message to recipients.
*/
-public class RecipientEditTextView extends MultiAutoCompleteTextView
- implements OnItemClickListener {
-
- private static final String TAG = "RecipientEditTextView";
-
- private Drawable mChipBackground = null;
-
- private Drawable mChipDelete = null;
-
- private int mChipPadding;
-
- private Tokenizer mTokenizer;
-
- private final Handler mHandler;
-
- private Runnable mDelayedSelectionMode = new Runnable() {
- @Override
- public void run() {
- setSelection(getText().length());
- }
- };
-
- private Drawable mChipBackgroundPressed;
-
- private RecipientChip mSelectedChip;
-
- private int mChipDeleteWidth;
-
- private ArrayList<RecipientChip> mRecipients;
+public class RecipientEditTextView extends FrameLayout {
+ private RecipientEditTextViewInner mEditTextViewInner;
public RecipientEditTextView(Context context, AttributeSet attrs) {
super(context, attrs);
- mHandler = new Handler();
- setOnItemClickListener(this);
- mRecipients = new ArrayList<RecipientChip>();
+ mEditTextViewInner = new RecipientEditTextViewInner(context, attrs);
+ addView(mEditTextViewInner);
}
- public RecipientChip constructChipSpan(RecipientEntry contact, int offset, boolean pressed)
- throws NullPointerException {
- if (mChipBackground == null) {
- throw new NullPointerException
- ("Unable to render any chips as setChipDimensions was not called.");
- }
- String text = contact.getDisplayName();
- Layout layout = getLayout();
- int line = layout.getLineForOffset(offset);
- int lineTop = layout.getLineTop(line);
-
- TextPaint paint = getPaint();
- float defaultSize = paint.getTextSize();
-
- // Reduce the size of the text slightly so that we can get the "look" of
- // padding.
- paint.setTextSize((float) (paint.getTextSize() * .9));
-
- // 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.
- CharSequence ellipsizedText = TextUtils.ellipsize(text, paint,
- calculateAvailableWidth(pressed), TextUtils.TruncateAt.END);
-
- int height = getLineHeight();
- int width = (int) Math.floor(paint.measureText(ellipsizedText, 0, ellipsizedText.length()))
- + (mChipPadding * 2);
- if (pressed) {
- width += mChipDeleteWidth;
- }
-
- // Create the background of the chip.
- Bitmap tmpBitmap = Bitmap.createBitmap(width, height, Bitmap.Config.ARGB_8888);
- Canvas canvas = new Canvas(tmpBitmap);
- if (pressed) {
- if (mChipBackgroundPressed != null) {
- mChipBackgroundPressed.setBounds(0, 0, width, height);
- mChipBackgroundPressed.draw(canvas);
-
- // Align the display text with where the user enters text.
- canvas.drawText(ellipsizedText, 0, ellipsizedText.length(), mChipPadding, height
- - layout.getLineDescent(line), paint);
- mChipDelete.setBounds(width - mChipDeleteWidth, 0, width, height);
- mChipDelete.draw(canvas);
- } else {
- Log.w(TAG,
- "Unable to draw a background for the chips as it was never set");
- }
- } else {
- if (mChipBackground != null) {
- mChipBackground.setBounds(0, 0, width, height);
- mChipBackground.draw(canvas);
-
- // Align the display text with where the user enters text.
- canvas.drawText(ellipsizedText, 0, ellipsizedText.length(), mChipPadding, height
- - layout.getLineDescent(line), paint);
- } else {
- Log.w(TAG,
- "Unable to draw a background for the chips as it was never set");
- }
- }
-
-
- // Get the location of the widget so we can properly offset
- // the anchor for each chip.
- int[] xy = new int[2];
- getLocationOnScreen(xy);
- // Pass the full text, un-ellipsized, to the chip.
- Drawable result = new BitmapDrawable(getResources(), tmpBitmap);
- result.setBounds(0, 0, width, height);
- Rect bounds = new Rect(xy[0] + offset, xy[1] + lineTop, xy[0] + width,
- calculateLineBottom(xy[1], line));
- RecipientChip recipientChip = new RecipientChip(result, contact, offset, bounds);
-
- // Return text to the original size.
- paint.setTextSize(defaultSize);
+ Collection<Long> getContactIds() {
+ return mEditTextViewInner.getContactIds();
+ }
- return recipientChip;
+ Collection<Long> getDataIds() {
+ return mEditTextViewInner.getDataIds();
}
- // The bottom of the line the chip will be located on is calculated by 4 factors:
- // 1) which line the chip appears on
- // 2) the height of a line in the autocomplete view
- // 3) padding built into the edit text view will move the bottom position
- // 4) the position of the autocomplete view on the screen, taking into account
- // that any top padding will move this down visually
- private int calculateLineBottom(int yOffset, int line) {
- int bottomPadding = 0;
- if (line == getLineCount() - 1) {
- bottomPadding += getPaddingBottom();
- }
- return ((line + 1) * getLineHeight()) + (yOffset + getPaddingTop()) + bottomPadding;
+ public Editable getText() {
+ return mEditTextViewInner.getRecipients();
}
- // Get the max amount of space a chip can take up. The formula takes into
- // account the width of the EditTextView, any view padding, and padding
- // that will be added to the chip.
- private float calculateAvailableWidth(boolean pressed) {
- int paddingRight = 0;
- if (pressed) {
- paddingRight = mChipDeleteWidth;
- }
- return getWidth() - getPaddingLeft() - getPaddingRight() - (mChipPadding * 2)
- - paddingRight;
+ public <T extends ListAdapter & Filterable> void setAdapter(T adapter) {
+ mEditTextViewInner.setAdapter(adapter);
}
- /**
- * Set all chip dimensions and resources. This has to be done from the application
- * as this is a static library.
- * @param chipBackground drawable
- * @param padding Padding around the text in a chip
- * @param offset Offset between the chip and the dropdown of alternates
- */
public void setChipDimensions(Drawable chipBackground, Drawable chipBackgroundPressed,
Drawable chipDelete, float padding) {
- mChipBackground = chipBackground;
- mChipBackgroundPressed = chipBackgroundPressed;
- mChipDelete = chipDelete;
- mChipDeleteWidth = chipDelete.getIntrinsicWidth();
- mChipPadding = (int) padding;
- }
-
- @Override
- public void setTokenizer(Tokenizer tokenizer) {
- mTokenizer = tokenizer;
- super.setTokenizer(mTokenizer);
+ mEditTextViewInner.setChipDimensions(chipBackground, chipBackgroundPressed, chipDelete,
+ padding);
}
- // We want to handle replacing text in the onItemClickListener
- // so we can get all the associated contact information including
- // display text, address, and id.
- @Override
- protected void replaceText(CharSequence text) {
- return;
- }
-
- @Override
- public boolean onKeyUp(int keyCode, KeyEvent event) {
- switch (keyCode) {
- case KeyEvent.KEYCODE_ENTER:
- case KeyEvent.KEYCODE_DPAD_CENTER:
- case KeyEvent.KEYCODE_TAB:
- if (event.hasNoModifiers()) {
- if (isPopupShowing()) {
- // choose the first entry.
- submitItemAtPosition(0);
- dismissDropDown();
- return true;
- } else {
- int end = getSelectionEnd();
- int start = mTokenizer.findTokenStart(getText(), end);
- String text = getText().toString().substring(start, end);
- clearComposingText();
-
- Editable editable = getText();
- RecipientEntry entry = RecipientEntry.constructFakeEntry(text);
- QwertyKeyListener.markAsReplaced(editable, start, end, "");
- editable.replace(start, end, createChip(entry));
- dismissDropDown();
- }
- }
- }
- return super.onKeyUp(keyCode, event);
- }
-
- public void onChipChanged() {
- // Must be posted so that the previous span
- // is correctly replaced with the previous selection points.
- mHandler.post(mDelayedSelectionMode);
- }
-
- @Override
- public boolean onKeyDown(int keyCode, KeyEvent event) {
-
- if (mSelectedChip != null) {
- mSelectedChip.onKeyDown(keyCode, event);
- }
-
- if (keyCode == KeyEvent.KEYCODE_ENTER && event.hasNoModifiers()) {
- return true;
- }
-
- return super.onKeyDown(keyCode, event);
- }
-
- private Spannable getSpannable() {
- return (Spannable) getText();
- }
-
-
- @Override
- public boolean onTouchEvent(MotionEvent event) {
- int action = event.getAction();
- boolean handled = super.onTouchEvent(event);
- boolean chipWasSelected = false;
-
- if (action == MotionEvent.ACTION_UP || action == MotionEvent.ACTION_DOWN) {
- float x = event.getX();
- float y = event.getY();
- int offset = putOffsetInRange(getOffsetForPosition(x, y));
- RecipientChip currentChip = findChip(offset);
- if (currentChip != null) {
- if (action == MotionEvent.ACTION_UP) {
- if (mSelectedChip != null && mSelectedChip != currentChip) {
- mSelectedChip.unselectChip();
- mSelectedChip = currentChip.selectChip();
- } else if (mSelectedChip == null) {
- mSelectedChip = currentChip.selectChip();
- } else {
- mSelectedChip.onClick(this, offset, x, y);
- }
- }
- chipWasSelected = true;
- }
- }
- if (action == MotionEvent.ACTION_UP && !chipWasSelected) {
- if (mSelectedChip != null) {
- mSelectedChip.unselectChip();
- mSelectedChip = null;
- }
- }
- return handled;
- }
-
- // TODO: This algorithm will need a lot of tweaking after more people have used
- // the chips ui. This attempts to be "forgiving" to fat finger touches by favoring
- // what comes before the finger.
- private int putOffsetInRange(int o) {
- int offset = o;
- while (offset >= 0 && findChip(offset) == null) {
- // Keep walking backward!
- offset--;
- }
- return offset;
- }
-
- private RecipientChip findChip(int offset) {
- RecipientChip[] chips = getSpannable().getSpans(0, offset, RecipientChip.class);
- // Find the chip that contains this offset.
- for (int i = 0; i < chips.length; i++) {
- RecipientChip chip = chips[i];
- if (chip.matchesChip(offset)) {
- return chip;
- }
- }
- return null;
- }
-
- private CharSequence createChip(RecipientEntry entry) {
- // We want to override the tokenizer behavior with our own ending
- // token, space.
- SpannableString chipText = new SpannableString(mTokenizer.terminateToken(entry
- .getDisplayName()));
- int end = getSelectionEnd();
- int start = mTokenizer.findTokenStart(getText(), end);
- try {
- chipText.setSpan(constructChipSpan(entry, start, false), 0, entry.getDisplayName()
- .length(), Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
- } catch (NullPointerException e) {
- Log.e(TAG, e.getMessage(), e);
- return null;
- }
-
- return chipText;
- }
-
- @Override
- public void onItemClick(AdapterView<?> parent, View view, int position, long id) {
- submitItemAtPosition(position);
- }
-
- private void submitItemAtPosition(int position) {
- RecipientEntry entry = (RecipientEntry) getAdapter().getItem(position);
- clearComposingText();
-
- int end = getSelectionEnd();
- int start = mTokenizer.findTokenStart(getText(), end);
-
- Editable editable = getText();
- editable.replace(start, end, createChip(entry));
- QwertyKeyListener.markAsReplaced(editable, start, end, "");
- }
-
- public Editable getRecipients() {
- StringBuilder plainText = new StringBuilder();
- int size = mRecipients.size();
- for (int i = 0; i < size; i++) {
- plainText.append(mRecipients.get(i).getValue());
- if (i != size-1) {
- plainText.append(',');
- }
- }
- return Editable.Factory.getInstance().newEditable(plainText);
- }
-
- /** Returns a collection of contact Id for each chip inside this View. */
- /* package */ Collection<Long> getContactIds() {
- final Set<Long> result = new HashSet<Long>();
- for (RecipientChip chip : mRecipients) {
- result.add(chip.getContactId());
- }
- return result;
- }
-
- /** Returns a collection of data Id for each chip inside this View. May be null. */
- /* package */ Collection<Long> getDataIds() {
- final Set<Long> result = new HashSet<Long>();
- for (RecipientChip chip : mRecipients) {
- result.add(chip.getDataId());
- }
- return result;
- }
-
- /**
- * RecipientChip defines an ImageSpan that contains information relevant to
- * a particular recipient.
- */
- public class RecipientChip extends ImageSpan implements OnItemClickListener, OnDismissListener,
- FilterListener {
- private final CharSequence mDisplay;
-
- private final CharSequence mValue;
-
- private final int mOffset;
-
- private ListPopupWindow mPopup;
-
- private View mAnchorView;
-
- private int mLeft;
-
- private final long mContactId;
-
- private final long mDataId;
-
- private RecipientEntry mEntry;
-
- private boolean mSelected = false;
-
- private Rect mBounds;
- public RecipientChip(Drawable drawable, RecipientEntry entry, int offset, Rect bounds) {
- super(drawable);
- mDisplay = entry.getDisplayName();
- mValue = entry.getDestination();
- mContactId = entry.getContactId();
- mDataId = entry.getDataId();
- mOffset = offset;
- mEntry = entry;
- mBounds = bounds;
-
- mAnchorView = new View(getContext());
- mAnchorView.setLeft(bounds.left);
- mAnchorView.setRight(bounds.left);
- mAnchorView.setTop(bounds.bottom);
- mAnchorView.setBottom(bounds.bottom);
- mAnchorView.setVisibility(View.GONE);
- mRecipients.add(this);
- }
-
- public void unselectChip() {
- if (getChipStart() == -1 || getChipEnd() == -1) {
- mSelectedChip = null;
- return;
- }
- clearComposingText();
- RecipientChip newChipSpan = null;
- try {
- newChipSpan = constructChipSpan(mEntry, mOffset, false);
- } catch (NullPointerException e) {
- Log.e(TAG, e.getMessage(), e);
- return;
- }
- replace(newChipSpan);
- if (mPopup != null && mPopup.isShowing()) {
- mPopup.dismiss();
- }
- return;
- }
-
- public void onKeyDown(int keyCode, KeyEvent event) {
- if (keyCode == KeyEvent.KEYCODE_DEL) {
- if (mPopup != null && mPopup.isShowing()) {
- mPopup.dismiss();
- }
- removeChip();
- }
- }
-
- public boolean isCompletedContact() {
- return mContactId != -1;
- }
-
- private void replace(RecipientChip newChip) {
- Spannable spannable = getSpannable();
- int spanStart = getChipStart();
- int spanEnd = getChipEnd();
- QwertyKeyListener.markAsReplaced(getText(), spanStart, spanEnd, "");
- spannable.removeSpan(this);
- mRecipients.remove(this);
- spannable.setSpan(newChip, spanStart, spanEnd, 0);
- }
-
- public void removeChip() {
- Spannable spannable = getSpannable();
- int spanStart = getChipStart();
- int spanEnd = getChipEnd();
- if (this == mSelectedChip) {
- mSelectedChip = null;
- }
- QwertyKeyListener.markAsReplaced(getText(), spanStart, spanEnd, "");
- spannable.removeSpan(this);
- mRecipients.remove(this);
- spannable.setSpan(null, spanStart, spanEnd, 0);
- getText().delete(spanStart, spanEnd);
- }
-
- public int getChipStart() {
- return getSpannable().getSpanStart(this);
- }
-
- public int getChipEnd() {
- return getSpannable().getSpanEnd(this);
- }
-
- public void replaceChip(RecipientEntry entry) {
- clearComposingText();
-
- RecipientChip newChipSpan = null;
- try {
- newChipSpan = constructChipSpan(entry, mOffset, false);
- } catch (NullPointerException e) {
- Log.e(TAG, e.getMessage(), e);
- return;
- }
- replace(newChipSpan);
- if (mPopup != null && mPopup.isShowing()) {
- mPopup.dismiss();
- }
- onChipChanged();
- }
-
- public RecipientChip selectChip() {
- clearComposingText();
- RecipientChip newChipSpan = null;
- if (isCompletedContact()) {
- try {
- newChipSpan = constructChipSpan(mEntry, mOffset, true);
- newChipSpan.setSelected(true);
- } catch (NullPointerException e) {
- Log.e(TAG, e.getMessage(), e);
- return newChipSpan;
- }
- replace(newChipSpan);
- if (mPopup != null && mPopup.isShowing()) {
- mPopup.dismiss();
- }
- mSelected = true;
- // Make sure we call edit on the new chip span.
- newChipSpan.showAlternates();
- } else {
- CharSequence text = getValue();
- removeChip();
- Editable editable = getText();
- setSelection(editable.length());
- editable.append(text);
- }
- return newChipSpan;
- }
-
- private void showAlternates() {
- mPopup = new ListPopupWindow(RecipientEditTextView.this.getContext());
-
- if (!mPopup.isShowing()) {
- mAnchorView.setLeft(mLeft);
- mAnchorView.setRight(mLeft);
- mPopup.setAnchorView(mAnchorView);
- BaseRecipientAdapter adapter = (BaseRecipientAdapter) getAdapter();
- adapter.getFilter().filter(getValue(), this);
- mPopup.setAdapter(adapter);
- // TODO: get width from dimen.xml.
- mPopup.setWidth(getWidth());
- mPopup.setOnItemClickListener(this);
- mPopup.setOnDismissListener(this);
- }
- }
-
- private void setSelected(boolean selected) {
- mSelected = selected;
- }
-
- public CharSequence getDisplay() {
- return mDisplay;
- }
-
- public CharSequence getValue() {
- return mValue;
- }
-
- private boolean isInDelete(int offset, float x, float y) {
- // Figure out the bounds of this chip and whether or not
- // the user clicked in the X portion.
- return mSelected
- && (offset >= getChipEnd()
- || (x > (mBounds.right - mChipDeleteWidth) && x < mBounds.right));
- }
-
- public boolean matchesChip(int offset) {
- int start = getChipStart();
- int end = getChipEnd();
- return (offset >= start && offset <= end);
- }
-
- public void onClick(View widget, int offset, float x, float y) {
- if (mSelected) {
- if (isInDelete(offset, x, y)) {
- removeChip();
- return;
- }
- }
- }
-
- @Override
- public void draw(Canvas canvas, CharSequence text, int start, int end, float x, int top,
- int y, int bottom, Paint paint) {
- // Shift the bounds of this span to where it is actually drawn on the screeen.
- mLeft = (int) x;
- super.draw(canvas, text, start, end, x, top, y, bottom, paint);
- }
-
- @Override
- public void onItemClick(AdapterView<?> adapterView, View view, int position, long rowId) {
- mPopup.dismiss();
- clearComposingText();
- replaceChip((RecipientEntry) adapterView.getItemAtPosition(position));
- }
-
- // When the popup dialog is dismissed, return the cursor to the end.
- @Override
- public void onDismiss() {
- mHandler.post(mDelayedSelectionMode);
- }
-
- @Override
- public void onFilterComplete(int count) {
- if (count > 0 && mPopup != null) {
- mPopup.show();
- }
- }
-
- public long getContactId() {
- return mContactId;
- }
-
- public long getDataId() {
- return mDataId;
- }
+ public void setTokenizer(MultiAutoCompleteTextView.Tokenizer tokenizer) {
+ mEditTextViewInner.setTokenizer(tokenizer);
}
}
diff --git a/chips/src/com/android/ex/chips/RecipientEditTextViewInner.java b/chips/src/com/android/ex/chips/RecipientEditTextViewInner.java
new file mode 100644
index 0000000..24e61d0
--- /dev/null
+++ b/chips/src/com/android/ex/chips/RecipientEditTextViewInner.java
@@ -0,0 +1,654 @@
+/*
+ * Copyright (C) 2011 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.ex.chips;
+
+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.os.Handler;
+import android.text.Editable;
+import android.text.Layout;
+import android.text.Spannable;
+import android.text.SpannableString;
+import android.text.Spanned;
+import android.text.TextPaint;
+import android.text.TextUtils;
+import android.text.method.QwertyKeyListener;
+import android.text.style.ImageSpan;
+import android.util.AttributeSet;
+import android.util.Log;
+import android.view.KeyEvent;
+import android.view.MotionEvent;
+import android.view.View;
+import android.widget.AdapterView;
+import android.widget.AdapterView.OnItemClickListener;
+import android.widget.Filter.FilterListener;
+import android.widget.ListPopupWindow;
+import android.widget.MultiAutoCompleteTextView;
+import android.widget.PopupWindow.OnDismissListener;
+
+import java.util.Collection;
+import java.util.HashSet;
+import java.util.List;
+import java.util.Set;
+
+import java.util.ArrayList;
+
+/**
+ * RecipientEditTextView is an auto complete text view for use with applications
+ * that use the new Chips UI for addressing a message to recipients.
+ */
+public class RecipientEditTextViewInner extends MultiAutoCompleteTextView
+ implements OnItemClickListener {
+
+ private static final String TAG = "RecipientEditTextView";
+
+ private Drawable mChipBackground = null;
+
+ private Drawable mChipDelete = null;
+
+ private int mChipPadding;
+
+ private Tokenizer mTokenizer;
+
+ private final Handler mHandler;
+
+ private Runnable mDelayedSelectionMode = new Runnable() {
+ @Override
+ public void run() {
+ setSelection(getText().length());
+ }
+ };
+
+ private Drawable mChipBackgroundPressed;
+
+ private RecipientChip mSelectedChip;
+
+ private int mChipDeleteWidth;
+
+ private ArrayList<RecipientChip> mRecipients;
+
+ public RecipientEditTextViewInner(Context context, AttributeSet attrs) {
+ super(context, attrs);
+ mHandler = new Handler();
+ setOnItemClickListener(this);
+ mRecipients = new ArrayList<RecipientChip>();
+ }
+
+ public RecipientChip constructChipSpan(RecipientEntry contact, int offset, boolean pressed)
+ throws NullPointerException {
+ if (mChipBackground == null) {
+ throw new NullPointerException
+ ("Unable to render any chips as setChipDimensions was not called.");
+ }
+ String text = contact.getDisplayName();
+ Layout layout = getLayout();
+ int line = layout.getLineForOffset(offset);
+ int lineTop = layout.getLineTop(line);
+
+ TextPaint paint = getPaint();
+ float defaultSize = paint.getTextSize();
+
+ // Reduce the size of the text slightly so that we can get the "look" of
+ // padding.
+ paint.setTextSize((float) (paint.getTextSize() * .9));
+
+ // 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.
+ CharSequence ellipsizedText = TextUtils.ellipsize(text, paint,
+ calculateAvailableWidth(pressed), TextUtils.TruncateAt.END);
+
+ int height = getLineHeight();
+ int width = (int) Math.floor(paint.measureText(ellipsizedText, 0, ellipsizedText.length()))
+ + (mChipPadding * 2);
+ if (pressed) {
+ width += mChipDeleteWidth;
+ }
+
+ // Create the background of the chip.
+ Bitmap tmpBitmap = Bitmap.createBitmap(width, height, Bitmap.Config.ARGB_8888);
+ Canvas canvas = new Canvas(tmpBitmap);
+ if (pressed) {
+ if (mChipBackgroundPressed != null) {
+ mChipBackgroundPressed.setBounds(0, 0, width, height);
+ mChipBackgroundPressed.draw(canvas);
+
+ // Align the display text with where the user enters text.
+ canvas.drawText(ellipsizedText, 0, ellipsizedText.length(), mChipPadding, height
+ - layout.getLineDescent(line), paint);
+ mChipDelete.setBounds(width - mChipDeleteWidth, 0, width, height);
+ mChipDelete.draw(canvas);
+ } else {
+ Log.w(TAG,
+ "Unable to draw a background for the chips as it was never set");
+ }
+ } else {
+ if (mChipBackground != null) {
+ mChipBackground.setBounds(0, 0, width, height);
+ mChipBackground.draw(canvas);
+
+ // Align the display text with where the user enters text.
+ canvas.drawText(ellipsizedText, 0, ellipsizedText.length(), mChipPadding, height
+ - layout.getLineDescent(line), paint);
+ } else {
+ Log.w(TAG,
+ "Unable to draw a background for the chips as it was never set");
+ }
+ }
+
+
+ // Get the location of the widget so we can properly offset
+ // the anchor for each chip.
+ int[] xy = new int[2];
+ getLocationOnScreen(xy);
+ // Pass the full text, un-ellipsized, to the chip.
+ Drawable result = new BitmapDrawable(getResources(), tmpBitmap);
+ result.setBounds(0, 0, width, height);
+ Rect bounds = new Rect(xy[0] + offset, xy[1] + lineTop, xy[0] + width,
+ calculateLineBottom(xy[1], line));
+ RecipientChip recipientChip = new RecipientChip(result, contact, offset, bounds);
+
+ // Return text to the original size.
+ paint.setTextSize(defaultSize);
+
+ return recipientChip;
+ }
+
+ // The bottom of the line the chip will be located on is calculated by 4 factors:
+ // 1) which line the chip appears on
+ // 2) the height of a line in the autocomplete view
+ // 3) padding built into the edit text view will move the bottom position
+ // 4) the position of the autocomplete view on the screen, taking into account
+ // that any top padding will move this down visually
+ private int calculateLineBottom(int yOffset, int line) {
+ int bottomPadding = 0;
+ if (line == getLineCount() - 1) {
+ bottomPadding += getPaddingBottom();
+ }
+ return ((line + 1) * getLineHeight()) + (yOffset + getPaddingTop()) + bottomPadding;
+ }
+
+ // Get the max amount of space a chip can take up. The formula takes into
+ // account the width of the EditTextView, any view padding, and padding
+ // that will be added to the chip.
+ private float calculateAvailableWidth(boolean pressed) {
+ int paddingRight = 0;
+ if (pressed) {
+ paddingRight = mChipDeleteWidth;
+ }
+ return getWidth() - getPaddingLeft() - getPaddingRight() - (mChipPadding * 2)
+ - paddingRight;
+ }
+
+ /**
+ * Set all chip dimensions and resources. This has to be done from the application
+ * as this is a static library.
+ * @param chipBackground drawable
+ * @param padding Padding around the text in a chip
+ * @param offset Offset between the chip and the dropdown of alternates
+ */
+ public void setChipDimensions(Drawable chipBackground, Drawable chipBackgroundPressed,
+ Drawable chipDelete, float padding) {
+ mChipBackground = chipBackground;
+ mChipBackgroundPressed = chipBackgroundPressed;
+ mChipDelete = chipDelete;
+ mChipDeleteWidth = chipDelete.getIntrinsicWidth();
+ mChipPadding = (int) padding;
+ }
+
+ @Override
+ public void setTokenizer(Tokenizer tokenizer) {
+ mTokenizer = tokenizer;
+ super.setTokenizer(mTokenizer);
+ }
+
+ // We want to handle replacing text in the onItemClickListener
+ // so we can get all the associated contact information including
+ // display text, address, and id.
+ @Override
+ protected void replaceText(CharSequence text) {
+ return;
+ }
+
+ @Override
+ public boolean onKeyUp(int keyCode, KeyEvent event) {
+ switch (keyCode) {
+ case KeyEvent.KEYCODE_ENTER:
+ case KeyEvent.KEYCODE_DPAD_CENTER:
+ case KeyEvent.KEYCODE_TAB:
+ if (event.hasNoModifiers()) {
+ if (isPopupShowing()) {
+ // choose the first entry.
+ submitItemAtPosition(0);
+ dismissDropDown();
+ return true;
+ } else {
+ int end = getSelectionEnd();
+ int start = mTokenizer.findTokenStart(getText(), end);
+ String text = getText().toString().substring(start, end);
+ clearComposingText();
+
+ Editable editable = getText();
+ RecipientEntry entry = RecipientEntry.constructFakeEntry(text);
+ QwertyKeyListener.markAsReplaced(editable, start, end, "");
+ editable.replace(start, end, createChip(entry));
+ dismissDropDown();
+ }
+ }
+ }
+ return super.onKeyUp(keyCode, event);
+ }
+
+ public void onChipChanged() {
+ // Must be posted so that the previous span
+ // is correctly replaced with the previous selection points.
+ mHandler.post(mDelayedSelectionMode);
+ }
+
+ @Override
+ public boolean onKeyDown(int keyCode, KeyEvent event) {
+
+ if (mSelectedChip != null) {
+ mSelectedChip.onKeyDown(keyCode, event);
+ }
+
+ if (keyCode == KeyEvent.KEYCODE_ENTER && event.hasNoModifiers()) {
+ return true;
+ }
+
+ return super.onKeyDown(keyCode, event);
+ }
+
+ private Spannable getSpannable() {
+ return (Spannable) getText();
+ }
+
+
+ @Override
+ public boolean onTouchEvent(MotionEvent event) {
+ int action = event.getAction();
+ boolean handled = super.onTouchEvent(event);
+ boolean chipWasSelected = false;
+
+ if (action == MotionEvent.ACTION_UP || action == MotionEvent.ACTION_DOWN) {
+ float x = event.getX();
+ float y = event.getY();
+ int offset = putOffsetInRange(getOffsetForPosition(x, y));
+ RecipientChip currentChip = findChip(offset);
+ if (currentChip != null) {
+ if (action == MotionEvent.ACTION_UP) {
+ if (mSelectedChip != null && mSelectedChip != currentChip) {
+ mSelectedChip.unselectChip();
+ mSelectedChip = currentChip.selectChip();
+ } else if (mSelectedChip == null) {
+ mSelectedChip = currentChip.selectChip();
+ } else {
+ mSelectedChip.onClick(this, offset, x, y);
+ }
+ }
+ chipWasSelected = true;
+ }
+ }
+ if (action == MotionEvent.ACTION_UP && !chipWasSelected) {
+ if (mSelectedChip != null) {
+ mSelectedChip.unselectChip();
+ mSelectedChip = null;
+ }
+ }
+ return handled;
+ }
+
+ // TODO: This algorithm will need a lot of tweaking after more people have used
+ // the chips ui. This attempts to be "forgiving" to fat finger touches by favoring
+ // what comes before the finger.
+ private int putOffsetInRange(int o) {
+ int offset = o;
+ while (offset >= 0 && findChip(offset) == null) {
+ // Keep walking backward!
+ offset--;
+ }
+ return offset;
+ }
+
+ private RecipientChip findChip(int offset) {
+ RecipientChip[] chips = getSpannable().getSpans(0, offset, RecipientChip.class);
+ // Find the chip that contains this offset.
+ for (int i = 0; i < chips.length; i++) {
+ RecipientChip chip = chips[i];
+ if (chip.matchesChip(offset)) {
+ return chip;
+ }
+ }
+ return null;
+ }
+
+ private CharSequence createChip(RecipientEntry entry) {
+ // We want to override the tokenizer behavior with our own ending
+ // token, space.
+ SpannableString chipText = new SpannableString(mTokenizer.terminateToken(entry
+ .getDisplayName()));
+ int end = getSelectionEnd();
+ int start = mTokenizer.findTokenStart(getText(), end);
+ try {
+ chipText.setSpan(constructChipSpan(entry, start, false), 0, entry.getDisplayName()
+ .length(), Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
+ } catch (NullPointerException e) {
+ Log.e(TAG, e.getMessage(), e);
+ return null;
+ }
+
+ return chipText;
+ }
+
+ @Override
+ public void onItemClick(AdapterView<?> parent, View view, int position, long id) {
+ submitItemAtPosition(position);
+ }
+
+ private void submitItemAtPosition(int position) {
+ RecipientEntry entry = (RecipientEntry) getAdapter().getItem(position);
+ clearComposingText();
+
+ int end = getSelectionEnd();
+ int start = mTokenizer.findTokenStart(getText(), end);
+
+ Editable editable = getText();
+ editable.replace(start, end, createChip(entry));
+ QwertyKeyListener.markAsReplaced(editable, start, end, "");
+ }
+
+ public Editable getRecipients() {
+ StringBuilder plainText = new StringBuilder();
+ int size = mRecipients.size();
+ for (int i = 0; i < size; i++) {
+ plainText.append(mRecipients.get(i).getValue());
+ if (i != size-1) {
+ plainText.append(',');
+ }
+ }
+ return Editable.Factory.getInstance().newEditable(plainText);
+ }
+
+ /** Returns a collection of contact Id for each chip inside this View. */
+ /* package */ Collection<Long> getContactIds() {
+ final Set<Long> result = new HashSet<Long>();
+ for (RecipientChip chip : mRecipients) {
+ result.add(chip.getContactId());
+ }
+ return result;
+ }
+
+ /** Returns a collection of data Id for each chip inside this View. May be null. */
+ /* package */ Collection<Long> getDataIds() {
+ final Set<Long> result = new HashSet<Long>();
+ for (RecipientChip chip : mRecipients) {
+ result.add(chip.getDataId());
+ }
+ return result;
+ }
+
+ /**
+ * RecipientChip defines an ImageSpan that contains information relevant to
+ * a particular recipient.
+ */
+ public class RecipientChip extends ImageSpan implements OnItemClickListener, OnDismissListener,
+ FilterListener {
+ private final CharSequence mDisplay;
+
+ private final CharSequence mValue;
+
+ private final int mOffset;
+
+ private ListPopupWindow mPopup;
+
+ private View mAnchorView;
+
+ private int mLeft;
+
+ private final long mContactId;
+
+ private final long mDataId;
+
+ private RecipientEntry mEntry;
+
+ private boolean mSelected = false;
+
+ private Rect mBounds;
+ public RecipientChip(Drawable drawable, RecipientEntry entry, int offset, Rect bounds) {
+ super(drawable);
+ mDisplay = entry.getDisplayName();
+ mValue = entry.getDestination();
+ mContactId = entry.getContactId();
+ mDataId = entry.getDataId();
+ mOffset = offset;
+ mEntry = entry;
+ mBounds = bounds;
+
+ mAnchorView = new View(getContext());
+ mAnchorView.setLeft(bounds.left);
+ mAnchorView.setRight(bounds.left);
+ mAnchorView.setTop(bounds.bottom);
+ mAnchorView.setBottom(bounds.bottom);
+ mAnchorView.setVisibility(View.GONE);
+ mRecipients.add(this);
+ }
+
+ public void unselectChip() {
+ if (getChipStart() == -1 || getChipEnd() == -1) {
+ mSelectedChip = null;
+ return;
+ }
+ clearComposingText();
+ RecipientChip newChipSpan = null;
+ try {
+ newChipSpan = constructChipSpan(mEntry, mOffset, false);
+ } catch (NullPointerException e) {
+ Log.e(TAG, e.getMessage(), e);
+ return;
+ }
+ replace(newChipSpan);
+ if (mPopup != null && mPopup.isShowing()) {
+ mPopup.dismiss();
+ }
+ return;
+ }
+
+ public void onKeyDown(int keyCode, KeyEvent event) {
+ if (keyCode == KeyEvent.KEYCODE_DEL) {
+ if (mPopup != null && mPopup.isShowing()) {
+ mPopup.dismiss();
+ }
+ removeChip();
+ }
+ }
+
+ public boolean isCompletedContact() {
+ return mContactId != -1;
+ }
+
+ private void replace(RecipientChip newChip) {
+ Spannable spannable = getSpannable();
+ int spanStart = getChipStart();
+ int spanEnd = getChipEnd();
+ QwertyKeyListener.markAsReplaced(getText(), spanStart, spanEnd, "");
+ spannable.removeSpan(this);
+ mRecipients.remove(this);
+ spannable.setSpan(newChip, spanStart, spanEnd, 0);
+ }
+
+ public void removeChip() {
+ Spannable spannable = getSpannable();
+ int spanStart = getChipStart();
+ int spanEnd = getChipEnd();
+ if (this == mSelectedChip) {
+ mSelectedChip = null;
+ }
+ QwertyKeyListener.markAsReplaced(getText(), spanStart, spanEnd, "");
+ spannable.removeSpan(this);
+ mRecipients.remove(this);
+ spannable.setSpan(null, spanStart, spanEnd, 0);
+ getText().delete(spanStart, spanEnd);
+ }
+
+ public int getChipStart() {
+ return getSpannable().getSpanStart(this);
+ }
+
+ public int getChipEnd() {
+ return getSpannable().getSpanEnd(this);
+ }
+
+ public void replaceChip(RecipientEntry entry) {
+ clearComposingText();
+
+ RecipientChip newChipSpan = null;
+ try {
+ newChipSpan = constructChipSpan(entry, mOffset, false);
+ } catch (NullPointerException e) {
+ Log.e(TAG, e.getMessage(), e);
+ return;
+ }
+ replace(newChipSpan);
+ if (mPopup != null && mPopup.isShowing()) {
+ mPopup.dismiss();
+ }
+ onChipChanged();
+ }
+
+ public RecipientChip selectChip() {
+ clearComposingText();
+ RecipientChip newChipSpan = null;
+ if (isCompletedContact()) {
+ try {
+ newChipSpan = constructChipSpan(mEntry, mOffset, true);
+ newChipSpan.setSelected(true);
+ } catch (NullPointerException e) {
+ Log.e(TAG, e.getMessage(), e);
+ return newChipSpan;
+ }
+ replace(newChipSpan);
+ if (mPopup != null && mPopup.isShowing()) {
+ mPopup.dismiss();
+ }
+ mSelected = true;
+ // Make sure we call edit on the new chip span.
+ newChipSpan.showAlternates();
+ } else {
+ CharSequence text = getValue();
+ removeChip();
+ Editable editable = getText();
+ setSelection(editable.length());
+ editable.append(text);
+ }
+ return newChipSpan;
+ }
+
+ private void showAlternates() {
+ mPopup = new ListPopupWindow(RecipientEditTextViewInner.this.getContext());
+
+ if (!mPopup.isShowing()) {
+ mAnchorView.setLeft(mLeft);
+ mAnchorView.setRight(mLeft);
+ mPopup.setAnchorView(mAnchorView);
+ BaseRecipientAdapter adapter = (BaseRecipientAdapter) getAdapter();
+ adapter.getFilter().filter(getValue(), this);
+ mPopup.setAdapter(adapter);
+ // TODO: get width from dimen.xml.
+ mPopup.setWidth(getWidth());
+ mPopup.setOnItemClickListener(this);
+ mPopup.setOnDismissListener(this);
+ }
+ }
+
+ private void setSelected(boolean selected) {
+ mSelected = selected;
+ }
+
+ public CharSequence getDisplay() {
+ return mDisplay;
+ }
+
+ public CharSequence getValue() {
+ return mValue;
+ }
+
+ private boolean isInDelete(int offset, float x, float y) {
+ // Figure out the bounds of this chip and whether or not
+ // the user clicked in the X portion.
+ return mSelected
+ && (offset >= getChipEnd()
+ || (x > (mBounds.right - mChipDeleteWidth) && x < mBounds.right));
+ }
+
+ public boolean matchesChip(int offset) {
+ int start = getChipStart();
+ int end = getChipEnd();
+ return (offset >= start && offset <= end);
+ }
+
+ public void onClick(View widget, int offset, float x, float y) {
+ if (mSelected) {
+ if (isInDelete(offset, x, y)) {
+ removeChip();
+ return;
+ }
+ }
+ }
+
+ @Override
+ public void draw(Canvas canvas, CharSequence text, int start, int end, float x, int top,
+ int y, int bottom, Paint paint) {
+ // Shift the bounds of this span to where it is actually drawn on the screeen.
+ mLeft = (int) x;
+ super.draw(canvas, text, start, end, x, top, y, bottom, paint);
+ }
+
+ @Override
+ public void onItemClick(AdapterView<?> adapterView, View view, int position, long rowId) {
+ mPopup.dismiss();
+ clearComposingText();
+ replaceChip((RecipientEntry) adapterView.getItemAtPosition(position));
+ }
+
+ // When the popup dialog is dismissed, return the cursor to the end.
+ @Override
+ public void onDismiss() {
+ mHandler.post(mDelayedSelectionMode);
+ }
+
+ @Override
+ public void onFilterComplete(int count) {
+ if (count > 0 && mPopup != null) {
+ mPopup.show();
+ }
+ }
+
+ public long getContactId() {
+ return mContactId;
+ }
+
+ public long getDataId() {
+ return mDataId;
+ }
+ }
+}
+