RecipientEditTextView.java revision f396b39a04ce6edee58ae259cb4d0888474a20cd
1/*
2 * Copyright (C) 2011 The Android Open Source Project
3 *
4 * Licensed under the Apache License, Version 2.0 (the "License");
5 * you may not use this file except in compliance with the License.
6 * You may obtain a copy of the License at
7 *
8 *      http://www.apache.org/licenses/LICENSE-2.0
9 *
10 * Unless required by applicable law or agreed to in writing, software
11 * distributed under the License is distributed on an "AS IS" BASIS,
12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 * See the License for the specific language governing permissions and
14 * limitations under the License.
15 */
16
17package com.android.ex.chips;
18
19import android.content.Context;
20import android.graphics.Bitmap;
21import android.graphics.BitmapFactory;
22import android.graphics.Canvas;
23import android.graphics.Matrix;
24import android.graphics.Paint;
25import android.graphics.Rect;
26import android.graphics.RectF;
27import android.graphics.drawable.BitmapDrawable;
28import android.graphics.drawable.Drawable;
29import android.text.Editable;
30import android.text.Layout;
31import android.text.Spannable;
32import android.text.SpannableString;
33import android.text.Spanned;
34import android.text.TextPaint;
35import android.text.TextUtils;
36import android.text.TextWatcher;
37import android.text.method.QwertyKeyListener;
38import android.text.style.ImageSpan;
39import android.util.AttributeSet;
40import android.util.Log;
41import android.view.ActionMode;
42import android.view.KeyEvent;
43import android.view.Menu;
44import android.view.MenuItem;
45import android.view.MotionEvent;
46import android.view.View;
47import android.view.ActionMode.Callback;
48import android.widget.AdapterView;
49import android.widget.AdapterView.OnItemClickListener;
50import android.widget.ListPopupWindow;
51import android.widget.MultiAutoCompleteTextView;
52
53import java.util.Collection;
54import java.util.HashSet;
55import java.util.Set;
56
57import java.util.ArrayList;
58
59/**
60 * RecipientEditTextView is an auto complete text view for use with applications
61 * that use the new Chips UI for addressing a message to recipients.
62 */
63public class RecipientEditTextView extends MultiAutoCompleteTextView
64    implements OnItemClickListener, Callback {
65
66    private static final String TAG = "RecipientEditTextView";
67
68    private Drawable mChipBackground = null;
69
70    private Drawable mChipDelete = null;
71
72    private int mChipPadding;
73
74    private Tokenizer mTokenizer;
75
76    private Drawable mChipBackgroundPressed;
77
78    private RecipientChip mSelectedChip;
79
80    private int mChipDeleteWidth;
81
82    private ArrayList<RecipientChip> mRecipients;
83
84    private int mAlternatesLayout;
85
86    private int mAlternatesSelectedLayout;
87
88    private Bitmap mDefaultContactPhoto;
89
90    public RecipientEditTextView(Context context, AttributeSet attrs) {
91        super(context, attrs);
92        mRecipients = new ArrayList<RecipientChip>();
93        setSuggestionsEnabled(false);
94        setOnItemClickListener(this);
95        setCustomSelectionActionModeCallback(this);
96        // When the user starts typing, make sure we unselect any selected
97        // chips.
98        addTextChangedListener(new TextWatcher() {
99            @Override
100            public void afterTextChanged(Editable s) {
101                // Do nothing.
102            }
103            @Override
104            public void onTextChanged(CharSequence s, int start, int before, int count) {
105                // Do nothing.
106            }
107            @Override
108            public void beforeTextChanged(CharSequence s, int start, int count, int after) {
109                if (mSelectedChip != null) {
110                    clearSelectedChip();
111                    setSelection(getText().length());
112                }
113            }
114        });
115    }
116
117    @Override
118    public void onSelectionChanged(int start, int end) {
119        // When selection changes, see if it is inside the chips area.
120        // If so, move the cursor back after the chips again.
121        if (mRecipients != null && mRecipients.size() > 0) {
122            Spannable span = getSpannable();
123            RecipientChip[] chips = span.getSpans(start, getText().length(), RecipientChip.class);
124            if (chips != null && chips.length > 0) {
125                // Grab the last chip and set the cursor to after it.
126                setSelection(chips[chips.length - 1].getChipEnd() + 1);
127            }
128        }
129        super.onSelectionChanged(start, end);
130    }
131
132    @Override
133    public void onFocusChanged(boolean hasFocus, int direction, Rect previous) {
134        if (!hasFocus) {
135            if (mSelectedChip != null) {
136                clearSelectedChip();
137            } else {
138                commitDefault();
139            }
140        } else {
141            setCursorVisible(true);
142            Editable text = getText();
143            setSelection(text != null && text.length() > 0 ? text.length() : 0);
144        }
145        super.onFocusChanged(hasFocus, direction, previous);
146    }
147
148    private CharSequence ellipsizeText(CharSequence text, TextPaint paint, float maxWidth) {
149        return TextUtils.ellipsize(text, paint, maxWidth, TextUtils.TruncateAt.END);
150    }
151
152    private Bitmap createSelectedChip(RecipientEntry contact, TextPaint paint, Layout layout,
153            int height, int line) {
154        // Ellipsize the text so that it takes AT MOST the entire width of the
155        // autocomplete text entry area. Make sure to leave space for padding
156        // on the sides.
157        int deleteWidth = height;
158        CharSequence ellipsizedText = ellipsizeText(contact.getDisplayName(), paint,
159                calculateAvailableWidth(true) - deleteWidth);
160
161        // Make sure there is a minimum chip width so the user can ALWAYS
162        // tap a chip without difficulty.
163        int width = Math.max(deleteWidth * 2, (int) Math.floor(paint.measureText(ellipsizedText, 0,
164                ellipsizedText.length()))
165                + (mChipPadding * 2) + deleteWidth);
166
167        // Create the background of the chip.
168        Bitmap tmpBitmap = Bitmap.createBitmap(width, height, Bitmap.Config.ARGB_8888);
169        Canvas canvas = new Canvas(tmpBitmap);
170        if (mChipBackgroundPressed != null) {
171            mChipBackgroundPressed.setBounds(0, 0, width, height);
172            mChipBackgroundPressed.draw(canvas);
173
174            // Align the display text with where the user enters text.
175            canvas.drawText(ellipsizedText, 0, ellipsizedText.length(), mChipPadding, height
176                    - layout.getLineDescent(line), paint);
177            // Make the delete a square.
178            mChipDelete.setBounds(width - deleteWidth, 0, width, height);
179            mChipDelete.draw(canvas);
180        } else {
181            Log.w(TAG, "Unable to draw a background for the chips as it was never set");
182        }
183        return tmpBitmap;
184    }
185
186    private Bitmap createUnselectedChip(RecipientEntry contact, TextPaint paint, Layout layout,
187            int height, int line) {
188        // Ellipsize the text so that it takes AT MOST the entire width of the
189        // autocomplete text entry area. Make sure to leave space for padding
190        // on the sides.
191        int iconWidth = height;
192        CharSequence ellipsizedText = ellipsizeText(contact.getDisplayName(), paint,
193                calculateAvailableWidth(false) - iconWidth);
194        // Make sure there is a minimum chip width so the user can ALWAYS
195        // tap a chip without difficulty.
196        int width = Math.max(iconWidth * 2, (int) Math.floor(paint.measureText(ellipsizedText, 0,
197                ellipsizedText.length()))
198                + (mChipPadding * 2) + iconWidth);
199
200        // Create the background of the chip.
201        Bitmap tmpBitmap = Bitmap.createBitmap(width, height, Bitmap.Config.ARGB_8888);
202        Canvas canvas = new Canvas(tmpBitmap);
203        if (mChipBackground != null) {
204            mChipBackground.setBounds(0, 0, width, height);
205            mChipBackground.draw(canvas);
206
207            byte[] photoBytes = contact.getPhotoBytes();
208            Bitmap photo;
209            if (photoBytes != null) {
210                // TODO: cache this in the recipient entry?
211                photo = BitmapFactory.decodeByteArray(photoBytes, 0, photoBytes.length);
212            } else {
213                // TODO: can the scaled down default photo be cached?
214                photo = mDefaultContactPhoto;
215            }
216            // Draw the photo on the left side.
217            Matrix matrix = new Matrix();
218            RectF src = new RectF(0, 0, photo.getWidth(), photo.getHeight());
219            RectF dst = new RectF(0, 0, iconWidth, height);
220            matrix.setRectToRect(src, dst, Matrix.ScaleToFit.CENTER);
221            canvas.drawBitmap(photo, matrix, paint);
222
223            // Align the display text with where the user enters text.
224            canvas.drawText(ellipsizedText, 0, ellipsizedText.length(), mChipPadding + iconWidth,
225                    height - layout.getLineDescent(line), paint);
226        } else {
227            Log.w(TAG, "Unable to draw a background for the chips as it was never set");
228        }
229        return tmpBitmap;
230    }
231
232    public RecipientChip constructChipSpan(RecipientEntry contact, int offset, boolean pressed)
233            throws NullPointerException {
234        if (mChipBackground == null) {
235            throw new NullPointerException(
236                    "Unable to render any chips as setChipDimensions was not called.");
237        }
238        Layout layout = getLayout();
239        int line = layout.getLineForOffset(offset);
240        int lineTop = layout.getLineTop(line);
241
242        TextPaint paint = getPaint();
243        float defaultSize = paint.getTextSize();
244
245        Bitmap tmpBitmap;
246        if (pressed) {
247            tmpBitmap = createSelectedChip(contact, paint, layout, getLineHeight(), line);
248
249        } else {
250            tmpBitmap = createUnselectedChip(contact, paint, layout, getLineHeight(), line);
251        }
252
253        // Get the location of the widget so we can properly offset
254        // the anchor for each chip.
255        int[] xy = new int[2];
256        getLocationOnScreen(xy);
257        // Pass the full text, un-ellipsized, to the chip.
258        Drawable result = new BitmapDrawable(getResources(), tmpBitmap);
259        result.setBounds(0, 0, tmpBitmap.getWidth(), tmpBitmap.getHeight());
260        Rect bounds = new Rect(xy[0] + offset, xy[1] + lineTop, xy[0] + tmpBitmap.getWidth(),
261                calculateLineBottom(xy[1], line));
262        RecipientChip recipientChip = new RecipientChip(result, contact, offset, bounds);
263
264        // Return text to the original size.
265        paint.setTextSize(defaultSize);
266
267        return recipientChip;
268    }
269
270    // The bottom of the line the chip will be located on is calculated by 4 factors:
271    // 1) which line the chip appears on
272    // 2) the height of a line in the autocomplete view
273    // 3) padding built into the edit text view will move the bottom position
274    // 4) the position of the autocomplete view on the screen, taking into account
275    // that any top padding will move this down visually
276    private int calculateLineBottom(int yOffset, int line) {
277        int bottomPadding = 0;
278        if (line == getLineCount() - 1) {
279            bottomPadding += getPaddingBottom();
280        }
281        return ((line + 1) * getLineHeight()) + (yOffset + getPaddingTop()) + bottomPadding;
282    }
283
284    // Get the max amount of space a chip can take up. The formula takes into
285    // account the width of the EditTextView, any view padding, and padding
286    // that will be added to the chip.
287    private float calculateAvailableWidth(boolean pressed) {
288        return getWidth() - getPaddingLeft() - getPaddingRight() - (mChipPadding * 2);
289    }
290
291    /**
292     * Set all chip dimensions and resources. This has to be done from the application
293     * as this is a static library.
294     * @param chipBackground drawable
295     * @param chipBackgroundPressed
296     * @param chipDelete
297     * @param defaultContact
298     * @param alternatesLayout
299     * @param alternatesSelectedLayout
300     * @param padding Padding around the text in a chip
301     */
302    public void setChipDimensions(Drawable chipBackground, Drawable chipBackgroundPressed,
303            Drawable chipDelete, Bitmap defaultContact, int alternatesLayout,
304            int alternatesSelectedLayout, float padding) {
305        mChipBackground = chipBackground;
306        mChipBackgroundPressed = chipBackgroundPressed;
307        mChipDelete = chipDelete;
308        mChipPadding = (int) padding;
309        mAlternatesLayout = alternatesLayout;
310        mAlternatesSelectedLayout = alternatesSelectedLayout;
311        mDefaultContactPhoto = defaultContact;
312    }
313
314    @Override
315    public void setTokenizer(Tokenizer tokenizer) {
316        mTokenizer = tokenizer;
317        super.setTokenizer(mTokenizer);
318    }
319
320    // We want to handle replacing text in the onItemClickListener
321    // so we can get all the associated contact information including
322    // display text, address, and id.
323    @Override
324    protected void replaceText(CharSequence text) {
325        return;
326    }
327
328    @Override
329    public boolean onKeyUp(int keyCode, KeyEvent event) {
330        switch (keyCode) {
331            case KeyEvent.KEYCODE_ENTER:
332            case KeyEvent.KEYCODE_DPAD_CENTER:
333            case KeyEvent.KEYCODE_TAB:
334                if (event.hasNoModifiers()) {
335                    if (commitDefault()) {
336                        return true;
337                    }
338                }
339        }
340        return super.onKeyUp(keyCode, event);
341    }
342
343    // If the popup is showing, the default is the first item in the popup
344    // suggestions list. Otherwise, it is whatever the user had typed in.
345    private boolean commitDefault() {
346        if (enoughToFilter() && getAdapter().getCount() > 0) {
347            // choose the first entry.
348            submitItemAtPosition(0);
349            dismissDropDown();
350            return true;
351        } else {
352            int end = getSelectionEnd();
353            int start = mTokenizer.findTokenStart(getText(), end);
354            String text = getText().toString().substring(start, end);
355            clearComposingText();
356            if (text != null && text.length() > 0) {
357                Editable editable = getText();
358                RecipientEntry entry = RecipientEntry.constructFakeEntry(text);
359                QwertyKeyListener.markAsReplaced(editable, start, end, "");
360                editable.replace(start, end, createChip(entry));
361                dismissDropDown();
362            }
363            return false;
364        }
365    }
366
367    @Override
368    public boolean onKeyDown(int keyCode, KeyEvent event) {
369        if (mSelectedChip != null) {
370            mSelectedChip.onKeyDown(keyCode, event);
371        }
372
373        if (keyCode == KeyEvent.KEYCODE_ENTER && event.hasNoModifiers()) {
374            return true;
375        }
376
377        return super.onKeyDown(keyCode, event);
378    }
379
380    private Spannable getSpannable() {
381        return (Spannable) getText();
382    }
383
384    /**
385     * Instead of filtering on the entire contents of the edit box,
386     * this subclass method filters on the range from
387     * {@link Tokenizer#findTokenStart} to {@link #getSelectionEnd}
388     * if the length of that range meets or exceeds {@link #getThreshold}
389     * and makes sure that the range is not already a Chip.
390     */
391    @Override
392    protected void performFiltering(CharSequence text, int keyCode) {
393        if (enoughToFilter()) {
394            int end = getSelectionEnd();
395            int start = mTokenizer.findTokenStart(text, end);
396            // If this is a RecipientChip, don't filter
397            // on its contents.
398            Spannable span = getSpannable();
399            RecipientChip[] chips = span.getSpans(start, end, RecipientChip.class);
400            if (chips != null && chips.length > 0) {
401                return;
402            }
403        }
404        super.performFiltering(text, keyCode);
405    }
406
407    private void clearSelectedChip() {
408        if (mSelectedChip != null) {
409            mSelectedChip.unselectChip();
410            mSelectedChip = null;
411        }
412        setCursorVisible(true);
413    }
414
415    @Override
416    public boolean onTouchEvent(MotionEvent event) {
417        if (!isFocused()) {
418            // Ignore any chip taps until this view is focused.
419            return super.onTouchEvent(event);
420        }
421
422        boolean handled = super.onTouchEvent(event);
423        int action = event.getAction();
424        boolean chipWasSelected = false;
425
426        if (action == MotionEvent.ACTION_UP || action == MotionEvent.ACTION_DOWN) {
427            float x = event.getX();
428            float y = event.getY();
429            int offset = putOffsetInRange(getOffsetForPosition(x, y));
430            RecipientChip currentChip = findChip(offset);
431            if (currentChip != null) {
432                if (action == MotionEvent.ACTION_UP) {
433                    if (mSelectedChip != null && mSelectedChip != currentChip) {
434                        clearSelectedChip();
435                        mSelectedChip = currentChip.selectChip();
436                    } else if (mSelectedChip == null) {
437                        mSelectedChip = currentChip.selectChip();
438                    } else {
439                        mSelectedChip.onClick(this, offset, x, y);
440                    }
441                }
442                chipWasSelected = true;
443            }
444        }
445        if (action == MotionEvent.ACTION_UP && !chipWasSelected) {
446            clearSelectedChip();
447        }
448        return handled;
449    }
450
451    // TODO: This algorithm will need a lot of tweaking after more people have used
452    // the chips ui. This attempts to be "forgiving" to fat finger touches by favoring
453    // what comes before the finger.
454    private int putOffsetInRange(int o) {
455        int offset = o;
456        Editable text = getText();
457        int length = text.length();
458        // Remove whitespace from end to find "real end"
459        int realLength = length;
460        for (int i = length - 1; i >= 0; i--) {
461            if (text.charAt(i) == ' ') {
462                realLength--;
463            } else {
464                break;
465            }
466        }
467
468        // If the offset is beyond or at the end of the text,
469        // leave it alone.
470        if (offset >= realLength) {
471            return offset;
472        }
473        Editable editable = getText();
474        while (offset >= 0 && findText(editable, offset) == -1 && findChip(offset) == null) {
475            // Keep walking backward!
476            offset--;
477        }
478        return offset;
479    }
480
481    private int findText(Editable text, int offset) {
482        if (text.charAt(offset) != ' ') {
483            return offset;
484        }
485        return -1;
486    }
487
488    private RecipientChip findChip(int offset) {
489        RecipientChip[] chips = getSpannable().getSpans(0, getText().length(), RecipientChip.class);
490        // Find the chip that contains this offset.
491        for (int i = 0; i < chips.length; i++) {
492            RecipientChip chip = chips[i];
493            if (chip.matchesChip(offset)) {
494                return chip;
495            }
496        }
497        return null;
498    }
499
500    private CharSequence createChip(RecipientEntry entry) {
501        CharSequence displayText = mTokenizer.terminateToken(entry.getDestination());
502        // Always leave a blank space at the end of a chip.
503        int textLength = displayText.length();
504        if (displayText.charAt(textLength - 1) == ' ') {
505            textLength--;
506        } else {
507            displayText = displayText.toString().concat(" ");
508            textLength = displayText.length();
509        }
510        SpannableString chipText = new SpannableString(displayText);
511        int end = getSelectionEnd();
512        int start = mTokenizer.findTokenStart(getText(), end);
513        try {
514            chipText.setSpan(constructChipSpan(entry, start, false), 0, textLength,
515                    Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
516        } catch (NullPointerException e) {
517            Log.e(TAG, e.getMessage(), e);
518            return null;
519        }
520
521        return chipText;
522    }
523
524    @Override
525    public void onItemClick(AdapterView<?> parent, View view, int position, long id) {
526        submitItemAtPosition(position);
527    }
528
529    private void submitItemAtPosition(int position) {
530        RecipientEntry entry = (RecipientEntry) getAdapter().getItem(position);
531        clearComposingText();
532
533        int end = getSelectionEnd();
534        int start = mTokenizer.findTokenStart(getText(), end);
535
536        Editable editable = getText();
537        editable.replace(start, end, createChip(entry));
538        QwertyKeyListener.markAsReplaced(editable, start, end, "");
539    }
540
541    /** Returns a collection of contact Id for each chip inside this View. */
542    /* package */ Collection<Long> getContactIds() {
543        final Set<Long> result = new HashSet<Long>();
544        for (RecipientChip chip : mRecipients) {
545            result.add(chip.getContactId());
546        }
547        return result;
548    }
549
550    /** Returns a collection of data Id for each chip inside this View. May be null. */
551    /* package */ Collection<Long> getDataIds() {
552        final Set<Long> result = new HashSet<Long>();
553        for (RecipientChip chip : mRecipients) {
554            result.add(chip.getDataId());
555        }
556        return result;
557    }
558
559    /**
560     * RecipientChip defines an ImageSpan that contains information relevant to
561     * a particular recipient.
562     */
563    public class RecipientChip extends ImageSpan implements OnItemClickListener {
564        private final CharSequence mDisplay;
565
566        private final CharSequence mValue;
567
568        private final int mOffset;
569
570        private ListPopupWindow mPopup;
571
572        private View mAnchorView;
573
574        private int mLeft;
575
576        private final long mContactId;
577
578        private final long mDataId;
579
580        private RecipientEntry mEntry;
581
582        private boolean mSelected = false;
583
584        private RecipientAlternatesAdapter mAlternatesAdapter;
585
586        private Rect mBounds;
587
588        public RecipientChip(Drawable drawable, RecipientEntry entry, int offset, Rect bounds) {
589            super(drawable);
590            mDisplay = entry.getDisplayName();
591            mValue = entry.getDestination();
592            mContactId = entry.getContactId();
593            mDataId = entry.getDataId();
594            mOffset = offset;
595            mEntry = entry;
596            mBounds = bounds;
597
598            mAnchorView = new View(getContext());
599            mAnchorView.setLeft(bounds.left);
600            mAnchorView.setRight(bounds.left);
601            mAnchorView.setTop(bounds.bottom);
602            mAnchorView.setBottom(bounds.bottom);
603            mAnchorView.setVisibility(View.GONE);
604            mRecipients.add(this);
605        }
606
607        public void unselectChip() {
608            if (getChipStart() == -1 || getChipEnd() == -1) {
609                mSelectedChip = null;
610                return;
611            }
612            clearComposingText();
613            RecipientChip newChipSpan = null;
614            try {
615                newChipSpan = constructChipSpan(mEntry, mOffset, false);
616            } catch (NullPointerException e) {
617                Log.e(TAG, e.getMessage(), e);
618                return;
619            }
620            replace(newChipSpan);
621            if (mPopup != null && mPopup.isShowing()) {
622                mPopup.dismiss();
623            }
624            return;
625        }
626
627        public void onKeyDown(int keyCode, KeyEvent event) {
628            if (keyCode == KeyEvent.KEYCODE_DEL) {
629                if (mPopup != null && mPopup.isShowing()) {
630                    mPopup.dismiss();
631                }
632                removeChip();
633            }
634        }
635
636        public boolean isCompletedContact() {
637            return mContactId != -1;
638        }
639
640        private void replace(RecipientChip newChip) {
641            Spannable spannable = getSpannable();
642            int spanStart = getChipStart();
643            int spanEnd = getChipEnd();
644            QwertyKeyListener.markAsReplaced(getText(), spanStart, spanEnd, "");
645            spannable.removeSpan(this);
646            mRecipients.remove(this);
647            spannable.setSpan(newChip, spanStart, spanEnd, 0);
648        }
649
650        public void removeChip() {
651            Spannable spannable = getSpannable();
652            int spanStart = getChipStart();
653            int spanEnd = getChipEnd();
654            Editable text = getText();
655            int toDelete = spanEnd;
656            // Always remove trailing spaces when removing a chip.
657            while (toDelete >= 0 && toDelete < text.length() - 1 && text.charAt(toDelete) == ' ') {
658                toDelete++;
659            }
660            QwertyKeyListener.markAsReplaced(getText(), spanStart, spanEnd, "");
661            spannable.removeSpan(this);
662            mRecipients.remove(this);
663            spannable.setSpan(null, spanStart, spanEnd, 0);
664            text.delete(spanStart, toDelete);
665            if (this == mSelectedChip) {
666                mSelectedChip = null;
667                clearSelectedChip();
668            }
669        }
670
671        public int getChipStart() {
672            return getSpannable().getSpanStart(this);
673        }
674
675        public int getChipEnd() {
676            return getSpannable().getSpanEnd(this);
677        }
678
679        public void replaceChip(RecipientEntry entry) {
680            clearComposingText();
681
682            RecipientChip newChipSpan = null;
683            try {
684                newChipSpan = constructChipSpan(entry, mOffset, false);
685            } catch (NullPointerException e) {
686                Log.e(TAG, e.getMessage(), e);
687                return;
688            }
689            replace(newChipSpan);
690            if (mPopup != null && mPopup.isShowing()) {
691                mPopup.dismiss();
692            }
693        }
694
695        public RecipientChip selectChip() {
696            clearComposingText();
697            RecipientChip newChipSpan = null;
698            if (isCompletedContact()) {
699                try {
700                    newChipSpan = constructChipSpan(mEntry, mOffset, true);
701                    newChipSpan.setSelected(true);
702                } catch (NullPointerException e) {
703                    Log.e(TAG, e.getMessage(), e);
704                    return newChipSpan;
705                }
706                replace(newChipSpan);
707                if (mPopup != null && mPopup.isShowing()) {
708                    mPopup.dismiss();
709                }
710                mSelected = true;
711                // Make sure we call edit on the new chip span.
712                newChipSpan.showAlternates();
713                setCursorVisible(false);
714            } else {
715                CharSequence text = getValue();
716                removeChip();
717                Editable editable = getText();
718                editable.append(text);
719                setCursorVisible(true);
720                setSelection(editable.length());
721            }
722            return newChipSpan;
723        }
724
725        private void showAlternates() {
726            mPopup = new ListPopupWindow(RecipientEditTextView.this.getContext());
727
728            if (!mPopup.isShowing()) {
729                mAlternatesAdapter = new RecipientAlternatesAdapter(
730                        RecipientEditTextView.this.getContext(),
731                        mEntry.getContactId(), mEntry.getDataId(),
732                        mAlternatesLayout, mAlternatesSelectedLayout);
733                mAnchorView.setLeft(mLeft);
734                mAnchorView.setRight(mLeft);
735                mPopup.setAnchorView(mAnchorView);
736                mPopup.setAdapter(mAlternatesAdapter);
737                mPopup.setWidth(getWidth());
738                mPopup.setOnItemClickListener(this);
739                mPopup.show();
740            }
741        }
742
743        private void setSelected(boolean selected) {
744            mSelected = selected;
745        }
746
747        public CharSequence getDisplay() {
748            return mDisplay;
749        }
750
751        public CharSequence getValue() {
752            return mValue;
753        }
754
755        private boolean isInDelete(int offset, float x, float y) {
756            // Figure out the bounds of this chip and whether or not
757            // the user clicked in the X portion.
758            return mSelected
759                    && (offset == getChipEnd()
760                            || (x > (mBounds.right - mChipDeleteWidth) && x < mBounds.right));
761        }
762
763        public boolean matchesChip(int offset) {
764            int start = getChipStart();
765            int end = getChipEnd();
766            return (offset >= start && offset <= end);
767        }
768
769        public void onClick(View widget, int offset, float x, float y) {
770            if (mSelected) {
771                if (isInDelete(offset, x, y)) {
772                    removeChip();
773                } else {
774                    clearSelectedChip();
775                }
776            }
777        }
778
779        @Override
780        public void draw(Canvas canvas, CharSequence text, int start, int end, float x, int top,
781                int y, int bottom, Paint paint) {
782            // Shift the bounds of this span to where it is actually drawn on the screeen.
783            mLeft = (int) x;
784            super.draw(canvas, text, start, end, x, top, y, bottom, paint);
785        }
786
787        @Override
788        public void onItemClick(AdapterView<?> adapterView, View view, int position, long rowId) {
789            mPopup.dismiss();
790            clearComposingText();
791            replaceChip(mAlternatesAdapter.getRecipientEntry(position));
792        }
793
794        public long getContactId() {
795            return mContactId;
796        }
797
798        public long getDataId() {
799            return mDataId;
800        }
801    }
802
803    @Override
804    public boolean onActionItemClicked(ActionMode mode, MenuItem item) {
805        return false;
806    }
807
808    @Override
809    public void onDestroyActionMode(ActionMode mode) {
810    }
811
812    @Override
813    public boolean onPrepareActionMode(ActionMode mode, Menu menu) {
814        return false;
815    }
816
817    // Prevent selection of chips.
818    @Override
819    public boolean onCreateActionMode(ActionMode mode, Menu menu) {
820        return false;
821    }
822}
823
824