Chips implementation.
Handles making chips from:
1) typing a lot then pressing enter
2) tapping a match in the dropdown
3) delete with tap on / backspace with cursor
4) replace
Does not handle: wrapping to new lines
enter submits first entry
Uses temporary backgrounds/ internal hardcoded values
Once I merge with Daisuke's changes for using library resources,
will update this.
Change-Id: Ic417c81a41aa85473772a2ea11643dd733080a7f
diff --git a/java/com/android/ex/chips/RecipientEditText.java b/java/com/android/ex/chips/RecipientEditText.java
deleted file mode 100644
index bab8058..0000000
--- a/java/com/android/ex/chips/RecipientEditText.java
+++ /dev/null
@@ -1,197 +0,0 @@
-/*
- * 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.content.res.Resources;
-import android.graphics.Bitmap;
-import android.graphics.Canvas;
-import android.graphics.drawable.BitmapDrawable;
-import android.graphics.drawable.Drawable;
-import android.text.Layout;
-import android.text.Spannable;
-import android.text.SpannableStringBuilder;
-import android.text.Spanned;
-import android.text.TextPaint;
-import android.text.TextUtils;
-import android.text.style.ImageSpan;
-import android.text.util.Rfc822Token;
-import android.util.AttributeSet;
-import android.util.Log;
-import android.view.MotionEvent;
-import android.widget.MultiAutoCompleteTextView;
-import android.widget.TextView;
-
-public class RecipientEditText extends MultiAutoCompleteTextView {
- private static final String TAG = "RecipientEditText";
- private static final boolean DEBUG = false;
-
- public RecipientEditText(Context context) {
- super(context);
- }
-
- public RecipientEditText(Context context, AttributeSet attrs) {
- super(context, attrs);
- }
-
- public RecipientEditText(Context context, AttributeSet attrs, int defStyle) {
- super(context, attrs, defStyle);
- }
-
- @Override
- public boolean onTouchEvent(MotionEvent event) {
- boolean superResult = super.onTouchEvent(event);
- final int action = event.getActionMasked();
- int offset = getOffset((int)event.getX(), (int)event.getY());
- int lineTop = getLayout().getLineTop(getLineAtCoordinate((int)event.getY()));
- int lineBottom = getLayout().getLineBottom(getLineAtCoordinate((int)event.getY()));
-
- CharSequence text = getText();
- if ((text instanceof Spannable)) {
- Spannable spannable = (Spannable) text;
- ChipSpan[] chips = spannable.getSpans(offset, offset, ChipSpan.class);
- int chipsCount = chips.length;
- if (chipsCount > 0) {
- if (chipsCount > 1) {
- Log.d(TAG, "chips too many: " + chipsCount);
- }
- ChipSpan chip = chips[0];
-
- int spanStart = spannable.getSpanStart(chip);
- int spanEnd = spannable.getSpanEnd(chip);
- CharSequence chipText = chip.getText();
- spannable.removeSpan(chip);
-
- TextPaint paint = getPaint();
- int width = (int) Math.floor(paint.measureText(text, 0, text.length()));
- int height = lineBottom - lineTop;
- float ascent = getLayout().getLineAscent(getLineAtCoordinate((int)event.getY()));
-
- if (action == MotionEvent.ACTION_DOWN) {
- spannable.setSpan(constructChipSpan(this, chipText, true),
- spanStart, spanEnd, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
- } else {
- spannable.setSpan(constructChipSpan(this, chipText, false),
- spanStart, spanEnd, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
- }
- setCursorVisible(false);
- } else {
- setCursorVisible(true);
- }
- }
- return superResult;
- }
-
- /* copied from TextView. TextView#getOffset() is hidden */
-
- public int getOffset(int x, int y) {
- if (getLayout() == null) return -1;
- final int line = getLineAtCoordinate(y);
- final int offset = getOffsetAtCoordinate(line, x);
- return offset;
- }
-
- private int convertToLocalHorizontalCoordinate(int x) {
- x -= getTotalPaddingLeft();
- // Clamp the position to inside of the view.
- x = Math.max(0, x);
- x = Math.min(getWidth() - getTotalPaddingRight() - 1, x);
- x += getScrollX();
- return x;
- }
-
- private int getLineAtCoordinate(int y) {
- y -= getTotalPaddingTop();
- // Clamp the position to inside of the view.
- y = Math.max(0, y);
- y = Math.min(getHeight() - getTotalPaddingBottom() - 1, y);
- y += getScrollY();
- return getLayout().getLineForVertical(y);
- }
-
- private int getOffsetAtCoordinate(int line, int x) {
- x = convertToLocalHorizontalCoordinate(x);
- return getLayout().getOffsetForHorizontal(line, x);
- }
-
- /* end of copied from TextView */
-
- @Override
- protected CharSequence convertSelectionToString(Object selectedItem) {
- final RecipientListEntry entry = (RecipientListEntry)selectedItem;
- final String displayName = entry.getDisplayName();
- final String email = entry.getDestination();
- if (TextUtils.isEmpty(displayName) && TextUtils.isEmpty(email)) {
- if (DEBUG) {
- Log.w(TAG, "Both a display name and an email are null");
- }
- return null;
- } else {
- final Rfc822Token token = new Rfc822Token(displayName, email, null);
- final CharSequence underlyingText = token.toString() + ", ";
- final CharSequence displayText =
- !TextUtils.isEmpty(displayName) ? displayName : email;
- SpannableStringBuilder builder = new SpannableStringBuilder(underlyingText);
- builder.setSpan(constructChipSpan(this, displayText, false),
- 0, builder.length(), Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
- return builder;
- }
- }
-
- private static ChipSpan constructChipSpan(
- TextView view, CharSequence text, boolean pressed) {
- final Layout layout = view.getLayout();
- final Resources res = view.getContext().getResources();
- final TextPaint paint = view.getPaint();
-
- int line = layout.getLineForOffset(0);
- int lineTop = layout.getLineTop(line);
- int lineBottom = layout.getLineBottom(line);
- int lineBaseline = layout.getLineBaseline(line);
- int ascent = layout.getLineAscent(line);
- int descent = layout.getLineDescent(line);
- int width = (int) Math.floor(paint.measureText(text, 0, text.length()));
- int height = lineBottom - lineTop;
-
- Bitmap tmpBitmap = Bitmap.createBitmap(width, height, Bitmap.Config.ARGB_8888);
- Canvas canvas = new Canvas(tmpBitmap);
- if (pressed) {
- canvas.drawColor(0xFFDDFFFF);
- } else {
- canvas.drawColor(0xFF00FFFF);
- }
-
- canvas.drawText(text, 0, text.length(), 0, Math.abs(ascent), paint);
-
- Drawable result = new BitmapDrawable(res, tmpBitmap);
- result.setBounds(0, 0, width, height);
- return new ChipSpan(result, text);
- }
-
- private static class ChipSpan extends ImageSpan {
- private final CharSequence mText;
-
- public ChipSpan(Drawable drawable, CharSequence text) {
- super(drawable);
- mText = text;
- }
-
- public CharSequence getText() {
- return mText;
- }
- }
-}
\ No newline at end of file
diff --git a/java/com/android/ex/chips/RecipientEditTextView.java b/java/com/android/ex/chips/RecipientEditTextView.java
new file mode 100644
index 0000000..b68afac
--- /dev/null
+++ b/java/com/android/ex/chips/RecipientEditTextView.java
@@ -0,0 +1,407 @@
+/*
+ * 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.ColorDrawable;
+import android.graphics.drawable.Drawable;
+import android.os.Handler;
+import android.text.Editable;
+import android.text.Layout;
+import android.text.Selection;
+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.view.KeyEvent;
+import android.view.MotionEvent;
+import android.view.View;
+import android.widget.AdapterView;
+import android.widget.AdapterView.OnItemClickListener;
+import android.widget.PopupWindow.OnDismissListener;
+import android.widget.ListPopupWindow;
+import android.widget.ListView;
+import android.widget.MultiAutoCompleteTextView;
+
+/**
+ * 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 int DEFAULT_CHIP_BACKGROUND = 0x77CCCCCC;
+
+ private static final int CHIP_PADDING = 10;
+
+ public static String CHIP_BACKGROUND = "chipBackground";
+
+ // TODO: eliminate this and take the pressed state from the provided
+ // drawable.
+ public static String CHIP_BACKGROUND_PRESSED = "chipBackgroundPressed";
+
+ private Drawable mChipBackground = null;
+
+ private Tokenizer mTokenizer;
+
+ private final Handler mHandler;
+
+ private Runnable mDelayedSelectionMode = new Runnable() {
+ @Override
+ public void run() {
+ setSelection(getText().length());
+ }
+ };
+
+
+ public RecipientEditTextView(Context context, AttributeSet attrs) {
+ super(context, attrs);
+ mHandler = new Handler();
+ setOnItemClickListener(this);
+ }
+
+ public RecipientChip constructChipSpan(CharSequence text, int offset, boolean pressed) {
+ Layout layout = getLayout();
+ int line = layout.getLineForOffset(offset);
+ int lineTop = layout.getLineTop(line);
+ int lineBottom = layout.getLineBottom(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(),
+ TextUtils.TruncateAt.END);
+
+ int height = getLineHeight();
+ int width = (int) Math.floor(paint.measureText(ellipsizedText, 0, ellipsizedText.length()))
+ + (CHIP_PADDING * 2);
+
+ // Create the background of the chip.
+ Bitmap tmpBitmap = Bitmap.createBitmap(width, height, Bitmap.Config.ARGB_8888);
+ Canvas canvas = new Canvas(tmpBitmap);
+ if (mChipBackground != null) {
+ mChipBackground.setBounds(0, 0, width, height);
+ mChipBackground.draw(canvas);
+ } else {
+ ColorDrawable color = new ColorDrawable(DEFAULT_CHIP_BACKGROUND);
+ color.setBounds(0, 0, width, height);
+ color.draw(canvas);
+ }
+
+ // Align the display text with where the user enters text.
+ canvas.drawText(ellipsizedText, 0, ellipsizedText.length(), CHIP_PADDING, height
+ - layout.getLineDescent(line), paint);
+
+ // 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, xy[1] + lineBottom);
+ RecipientChip recipientChip = new RecipientChip(result, text, text, -1, offset, bounds);
+
+ // Return text to the original size.
+ paint.setTextSize(defaultSize);
+
+ return recipientChip;
+ }
+
+ // 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() {
+ return getWidth() - getPaddingLeft() - getPaddingRight() - (CHIP_PADDING * 2);
+ }
+
+ public void setChipBackgroundDrawable(Drawable d) {
+ mChipBackground = d;
+ }
+
+ @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;
+ }
+
+ // TODO: this should be handled by the framework directly; working with
+ // @debunne to figure out why it isn't being handled properly.
+ @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 (getListSelection() != ListView.INVALID_POSITION) {
+ performCompletion();
+ return true;
+ } else {
+ int end = getSelectionEnd();
+ int start = mTokenizer.findTokenStart(getText(), end);
+ String text = getText().toString().substring(start, end);
+ clearComposingText();
+
+ Editable editable = getText();
+
+ QwertyKeyListener.markAsReplaced(editable, start, end, "");
+ editable.replace(start, end, createChip(text));
+ 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) {
+ int start = getSelectionStart();
+ int end = getSelectionEnd();
+ Spannable span = getSpannable();
+
+ RecipientChip[] chips = span.getSpans(start, end, RecipientChip.class);
+ if (chips != null) {
+ for (RecipientChip chip : chips) {
+ chip.onKeyDown(keyCode, event);
+ }
+ }
+
+ if (keyCode == KeyEvent.KEYCODE_ENTER && event.hasNoModifiers()) {
+ return true;
+ }
+
+ return super.onKeyDown(keyCode, event);
+ }
+
+ public Spannable getSpannable() {
+ return (Spannable) getText();
+ }
+
+ /**
+ * RecipientChip defines an ImageSpan that contains information relevant to
+ * a particular recipient.
+ */
+ public class RecipientChip extends ImageSpan implements OnItemClickListener, OnDismissListener {
+ private final CharSequence mDisplay;
+
+ private final CharSequence mValue;
+
+ private final int mOffset;
+
+ private ListPopupWindow mPopup;
+
+ private View mAnchorView;
+
+ private int mLeft;
+
+ private int mId = -1;
+
+ public RecipientChip(Drawable drawable, CharSequence text, CharSequence value, int id,
+ int offset, Rect bounds) {
+ super(drawable);
+ mDisplay = text;
+ mValue = value;
+ mOffset = offset;
+ mAnchorView = new View(getContext());
+ mAnchorView.setLeft(bounds.left);
+ mAnchorView.setRight(bounds.left);
+ mAnchorView.setTop(bounds.right + CHIP_PADDING);
+ mAnchorView.setBottom(bounds.right + CHIP_PADDING);
+ mAnchorView.setVisibility(View.GONE);
+
+ mId = id;
+ }
+
+ public void onKeyDown(int keyCode, KeyEvent event) {
+ if (keyCode == KeyEvent.KEYCODE_DEL) {
+ if (mPopup != null && mPopup.isShowing()) {
+ mPopup.dismiss();
+ }
+ removeChip();
+ }
+ }
+
+ public boolean isCompletedContact() {
+ return mId != -1;
+ }
+
+ private void replace(RecipientChip newChip) {
+ Spannable spannable = getSpannable();
+ int spanStart = getChipStart();
+ int spanEnd = getChipEnd();
+ QwertyKeyListener.markAsReplaced(getText(), spanStart, spanEnd, "");
+ spannable.removeSpan(this);
+ spannable.setSpan(newChip, spanStart, spanEnd, 0);
+ }
+
+ public void removeChip() {
+ Spannable spannable = getSpannable();
+ int spanStart = getChipStart();
+ int spanEnd = getChipEnd();
+ QwertyKeyListener.markAsReplaced(getText(), spanStart, spanEnd, "");
+ spannable.removeSpan(this);
+ spannable.setSpan(null, spanStart, spanEnd, 0);
+ onChipChanged();
+ }
+
+ public int getChipStart() {
+ return getSpannable().getSpanStart(this);
+ }
+
+ public int getChipEnd() {
+ return getSpannable().getSpanEnd(this);
+ }
+
+ public void replaceChip(String text) {
+ clearComposingText();
+
+ RecipientChip newChipSpan = constructChipSpan(text, mOffset, false);
+ replace(newChipSpan);
+ if (mPopup != null && mPopup.isShowing()) {
+ mPopup.dismiss();
+ }
+ onChipChanged();
+ }
+
+ public CharSequence getDisplay() {
+ return mDisplay;
+ }
+
+ public CharSequence getValue() {
+ return mValue;
+ }
+
+ public void onClick(View widget) {
+ mPopup = new ListPopupWindow(widget.getContext());
+
+ if (!mPopup.isShowing()) {
+ mAnchorView.setLeft(mLeft);
+ mAnchorView.setRight(mLeft);
+ mPopup.setAnchorView(mAnchorView);
+ mPopup.setAdapter(getAdapter());
+ // TODO: get width from dimen.xml.
+ mPopup.setWidth(200);
+ mPopup.setOnItemClickListener(this);
+ mPopup.setOnDismissListener(this);
+ mPopup.show();
+ }
+ }
+
+ @Override
+ public void draw(Canvas canvas, CharSequence text, int start, int end, float x, int top,
+ int y, int bottom, Paint paint) {
+ 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();
+ RecipientListEntry entry = (RecipientListEntry) adapterView.getItemAtPosition(position);
+ replaceChip(entry.getDisplayName());
+ }
+
+ // When the popup dialog is dismissed, return the cursor to the end.
+ @Override
+ public void onDismiss() {
+ mHandler.post(mDelayedSelectionMode);
+ }
+ }
+
+ @Override
+ public boolean onTouchEvent(MotionEvent event) {
+ int action = event.getAction();
+ if (action == MotionEvent.ACTION_UP || action == MotionEvent.ACTION_DOWN) {
+ Spannable span = getSpannable();
+ int offset = getOffsetForPosition(event.getX(), event.getY());
+ int start = offset;
+ int end = span.length();
+ RecipientChip[] chips = span.getSpans(start, end, RecipientChip.class);
+ if (chips != null && chips.length > 0) {
+ // Get the first chip that matched.
+ final RecipientChip currentChip = chips[0];
+
+ if (action == MotionEvent.ACTION_UP) {
+ currentChip.onClick(this);
+ } else if (action == MotionEvent.ACTION_DOWN) {
+ Selection.setSelection(getSpannable(), currentChip.getChipStart(), currentChip
+ .getChipEnd());
+ }
+ return true;
+ }
+ }
+
+ return super.onTouchEvent(event);
+ }
+
+ private CharSequence createChip(String text) {
+ // We want to override the tokenizer behavior with our own ending
+ // token, space.
+ SpannableString chipText = new SpannableString(mTokenizer.terminateToken(text));
+ int end = getSelectionEnd();
+ int start = mTokenizer.findTokenStart(getText(), end);
+ chipText.setSpan(constructChipSpan(text, start, false), 0, text.length(),
+ Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
+ return chipText;
+ }
+
+ @Override
+ public void onItemClick(AdapterView<?> parent, View view, int position, long id) {
+ // Figure out what got clicked!
+ RecipientListEntry entry = (RecipientListEntry) parent.getItemAtPosition(position);
+ clearComposingText();
+
+ int end = getSelectionEnd();
+ int start = mTokenizer.findTokenStart(getText(), end);
+
+ Editable editable = getText();
+ editable.replace(start, end, createChip(entry.getDisplayName()));
+ QwertyKeyListener.markAsReplaced(editable, start, end, "");
+ }
+}
diff --git a/java/com/android/ex/chips/Rfc822ChipTokenizer.java b/java/com/android/ex/chips/Rfc822ChipTokenizer.java
new file mode 100644
index 0000000..1268643
--- /dev/null
+++ b/java/com/android/ex/chips/Rfc822ChipTokenizer.java
@@ -0,0 +1,104 @@
+/*
+ * 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.text.util.Rfc822Tokenizer;
+
+/**
+ * Extension of the Rfc822Tokenizer for use with the RecipientEditTextView that
+ * will tokenize properly to create recipient chips.
+ */
+public class Rfc822ChipTokenizer extends Rfc822Tokenizer {
+
+ /**
+ * {@inheritDoc}
+ */
+ public int findTokenEnd(CharSequence text, int cursor) {
+ int len = text.length();
+ int i = cursor;
+
+ while (i < len) {
+ char c = text.charAt(i);
+
+ if (c == ',' || c == ';' || c == ' ') {
+ return i;
+ } else if (c == '"') {
+ i++;
+
+ while (i < len) {
+ c = text.charAt(i);
+
+ if (c == '"') {
+ i++;
+ break;
+ } else if (c == '\\' && i + 1 < len) {
+ i += 2;
+ } else {
+ i++;
+ }
+ }
+ } else if (c == '(') {
+ int level = 1;
+ i++;
+
+ while (i < len && level > 0) {
+ c = text.charAt(i);
+
+ if (c == ')') {
+ level--;
+ i++;
+ } else if (c == '(') {
+ level++;
+ i++;
+ } else if (c == '\\' && i + 1 < len) {
+ i += 2;
+ } else {
+ i++;
+ }
+ }
+ } else if (c == '<') {
+ i++;
+
+ while (i < len) {
+ c = text.charAt(i);
+
+ if (c == '>') {
+ i++;
+ break;
+ } else {
+ i++;
+ }
+ }
+ } else {
+ i++;
+ }
+ }
+
+ return i;
+ }
+
+ /**
+ * Terminates the specified address with a space. This assumes that the
+ * specified text already has valid syntax. The Adapter subclass's
+ * convertToString() method must make that guarantee.
+ */
+ public CharSequence terminateToken(CharSequence text) {
+ // We want to override the tokenizer behavior with our own ending
+ // token, space.
+ return (((String) text).concat(" "));
+ }
+}