RecipientEditTextView.java revision 6e8e8e8165a797611f80a2c17249147333d55ea7
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    // TODO: get correct number/ algorithm from with UX.
69    private static final int CHIP_LIMIT = 2;
70
71    // TODO: get correct size from UX.
72    private static final float MORE_WIDTH_FACTOR = 0.25f;
73
74    private Drawable mChipBackground = null;
75
76    private Drawable mChipDelete = null;
77
78    private int mChipPadding;
79
80    private Tokenizer mTokenizer;
81
82    private Drawable mChipBackgroundPressed;
83
84    private RecipientChip mSelectedChip;
85
86    private int mChipDeleteWidth;
87
88    private ArrayList<RecipientChip> mRecipients;
89
90    private int mAlternatesLayout;
91
92    private int mAlternatesSelectedLayout;
93
94    private Bitmap mDefaultContactPhoto;
95
96    private ImageSpan mMoreChip;
97
98    private int mMoreString;
99
100    private ArrayList<RecipientChip> mRemovedSpans;
101
102    private float mChipHeight;
103
104    private float mChipFontSize;
105
106    private ListPopupWindow mAlternatesPopup;
107
108    public RecipientEditTextView(Context context, AttributeSet attrs) {
109        super(context, attrs);
110        mRecipients = new ArrayList<RecipientChip>();
111        mAlternatesPopup = new ListPopupWindow(context);
112        setSuggestionsEnabled(false);
113        setOnItemClickListener(this);
114        setCustomSelectionActionModeCallback(this);
115        // When the user starts typing, make sure we unselect any selected
116        // chips.
117        addTextChangedListener(new TextWatcher() {
118            @Override
119            public void afterTextChanged(Editable s) {
120                // Do nothing.
121            }
122            @Override
123            public void onTextChanged(CharSequence s, int start, int before, int count) {
124                // Do nothing.
125            }
126            @Override
127            public void beforeTextChanged(CharSequence s, int start, int count, int after) {
128                if (mSelectedChip != null) {
129                    clearSelectedChip();
130                    setSelection(getText().length());
131                }
132            }
133        });
134    }
135
136    @Override
137    public void onSelectionChanged(int start, int end) {
138        // When selection changes, see if it is inside the chips area.
139        // If so, move the cursor back after the chips again.
140        if (mRecipients != null && mRecipients.size() > 0) {
141            Spannable span = getSpannable();
142            RecipientChip[] chips = span.getSpans(start, getText().length(), RecipientChip.class);
143            if (chips != null && chips.length > 0) {
144                // Grab the last chip and set the cursor to after it.
145                setSelection(chips[chips.length - 1].getChipEnd() + 1);
146            }
147        }
148        super.onSelectionChanged(start, end);
149    }
150
151    @Override
152    public void onFocusChanged(boolean hasFocus, int direction, Rect previous) {
153        if (!hasFocus) {
154            shrink();
155        } else {
156            expand();
157        }
158        super.onFocusChanged(hasFocus, direction, previous);
159    }
160
161    private void shrink() {
162        if (mSelectedChip != null) {
163            clearSelectedChip();
164        } else {
165            commitDefault();
166        }
167        mMoreChip = createMoreChip();
168    }
169
170    private void expand() {
171        removeMoreChip();
172        setCursorVisible(true);
173        Editable text = getText();
174        setSelection(text != null && text.length() > 0 ? text.length() : 0);
175    }
176
177    private CharSequence ellipsizeText(CharSequence text, TextPaint paint, float maxWidth) {
178        paint.setTextSize(mChipFontSize);
179        return TextUtils.ellipsize(text, paint, maxWidth, TextUtils.TruncateAt.END);
180    }
181
182    private Bitmap createSelectedChip(RecipientEntry contact, TextPaint paint, Layout layout) {
183        // Ellipsize the text so that it takes AT MOST the entire width of the
184        // autocomplete text entry area. Make sure to leave space for padding
185        // on the sides.
186        int height = (int) mChipHeight;
187        int deleteWidth = height;
188        CharSequence ellipsizedText = ellipsizeText(contact.getDisplayName(), paint,
189                calculateAvailableWidth(true) - deleteWidth);
190
191        // Make sure there is a minimum chip width so the user can ALWAYS
192        // tap a chip without difficulty.
193        int width = Math.max(deleteWidth * 2, (int) Math.floor(paint.measureText(ellipsizedText, 0,
194                ellipsizedText.length()))
195                + (mChipPadding * 2) + deleteWidth);
196
197        // Create the background of the chip.
198        Bitmap tmpBitmap = Bitmap.createBitmap(width, height, Bitmap.Config.ARGB_8888);
199        Canvas canvas = new Canvas(tmpBitmap);
200        if (mChipBackgroundPressed != null) {
201            mChipBackgroundPressed.setBounds(0, 0, width, height);
202            mChipBackgroundPressed.draw(canvas);
203
204            // Align the display text with where the user enters text.
205            canvas.drawText(ellipsizedText, 0, ellipsizedText.length(), mChipPadding, height
206                    - Math.abs(height - mChipFontSize)/2, paint);
207            // Make the delete a square.
208            mChipDelete.setBounds(width - deleteWidth, 0, width, height);
209            mChipDelete.draw(canvas);
210        } else {
211            Log.w(TAG, "Unable to draw a background for the chips as it was never set");
212        }
213        return tmpBitmap;
214    }
215
216    private Bitmap createUnselectedChip(RecipientEntry contact, TextPaint paint, Layout layout) {
217        // Ellipsize the text so that it takes AT MOST the entire width of the
218        // autocomplete text entry area. Make sure to leave space for padding
219        // on the sides.
220        int height = (int) mChipHeight;
221        int iconWidth = height;
222        CharSequence ellipsizedText = ellipsizeText(contact.getDisplayName(), paint,
223                calculateAvailableWidth(false) - iconWidth);
224        // Make sure there is a minimum chip width so the user can ALWAYS
225        // tap a chip without difficulty.
226        int width = Math.max(iconWidth * 2, (int) Math.floor(paint.measureText(ellipsizedText, 0,
227                ellipsizedText.length()))
228                + (mChipPadding * 2) + iconWidth);
229
230        // Create the background of the chip.
231        Bitmap tmpBitmap = Bitmap.createBitmap(width, height, Bitmap.Config.ARGB_8888);
232        Canvas canvas = new Canvas(tmpBitmap);
233        if (mChipBackground != null) {
234            mChipBackground.setBounds(0, 0, width, height);
235            mChipBackground.draw(canvas);
236
237            // Don't draw photos for recipients that have been typed in.
238            if (contact.getContactId() != -1) {
239                byte[] photoBytes = contact.getPhotoBytes();
240                Bitmap photo;
241                if (photoBytes != null) {
242                    // TODO: cache this in the recipient entry?
243                    photo = BitmapFactory.decodeByteArray(photoBytes, 0, photoBytes.length);
244                } else {
245                    // TODO: can the scaled down default photo be cached?
246                    photo = mDefaultContactPhoto;
247                }
248                // Draw the photo on the left side.
249                Matrix matrix = new Matrix();
250                RectF src = new RectF(0, 0, photo.getWidth(), photo.getHeight());
251                RectF dst = new RectF(0, 0, iconWidth, height);
252                matrix.setRectToRect(src, dst, Matrix.ScaleToFit.CENTER);
253                canvas.drawBitmap(photo, matrix, paint);
254            } else {
255                // Don't leave any space for the icon. It isn't being drawn.
256                iconWidth = 0;
257            }
258
259            // Align the display text with where the user enters text.
260            canvas.drawText(ellipsizedText, 0, ellipsizedText.length(), mChipPadding + iconWidth,
261                    height - Math.abs(height - mChipFontSize)/2, paint);
262        } else {
263            Log.w(TAG, "Unable to draw a background for the chips as it was never set");
264        }
265        return tmpBitmap;
266    }
267
268    public RecipientChip constructChipSpan(RecipientEntry contact, int offset, boolean pressed)
269            throws NullPointerException {
270        if (mChipBackground == null) {
271            throw new NullPointerException(
272                    "Unable to render any chips as setChipDimensions was not called.");
273        }
274        Layout layout = getLayout();
275        int line = layout.getLineForOffset(offset);
276        int lineTop = layout.getLineTop(line);
277
278        TextPaint paint = getPaint();
279        float defaultSize = paint.getTextSize();
280
281        Bitmap tmpBitmap;
282        if (pressed) {
283            tmpBitmap = createSelectedChip(contact, paint, layout);
284
285        } else {
286            tmpBitmap = createUnselectedChip(contact, paint, layout);
287        }
288
289        // Get the location of the widget so we can properly offset
290        // the anchor for each chip.
291        int[] xy = new int[2];
292        getLocationOnScreen(xy);
293        // Pass the full text, un-ellipsized, to the chip.
294        Drawable result = new BitmapDrawable(getResources(), tmpBitmap);
295        result.setBounds(0, 0, tmpBitmap.getWidth(), tmpBitmap.getHeight());
296        Rect bounds = new Rect(xy[0] + offset, xy[1] + lineTop, xy[0] + tmpBitmap.getWidth(),
297                calculateLineBottom(xy[1], line, tmpBitmap.getHeight()));
298        RecipientChip recipientChip = new RecipientChip(result, contact, offset, bounds);
299
300        // Return text to the original size.
301        paint.setTextSize(defaultSize);
302
303        return recipientChip;
304    }
305
306    // The bottom of the line the chip will be located on is calculated by 4 factors:
307    // 1) which line the chip appears on
308    // 2) the height of a line in the autocomplete view vs the heigt of a chip
309    // 3) padding built into the edit text view will move the bottom position
310    // 4) the position of the autocomplete view on the screen, taking into account
311    // that any top padding will move this down visually
312    private int calculateLineBottom(int yOffset, int line, int chipHeight) {
313        int bottomPadding = 0;
314        if (line == getLineCount() - 1) {
315            bottomPadding += getPaddingBottom();
316        }
317        return ((line + 1) * getLineHeight()) + yOffset + getPaddingTop()
318                + (chipHeight - getLineHeight()) + bottomPadding;
319    }
320
321    // Get the max amount of space a chip can take up. The formula takes into
322    // account the width of the EditTextView, any view padding, and padding
323    // that will be added to the chip.
324    private float calculateAvailableWidth(boolean pressed) {
325        return getWidth() - getPaddingLeft() - getPaddingRight() - (mChipPadding * 2);
326    }
327
328    /**
329     * Set all chip dimensions and resources. This has to be done from the application
330     * as this is a static library.
331     * @param chipBackground drawable
332     * @param chipBackgroundPressed
333     * @param chipDelete
334     * @param defaultContact
335     * @param alternatesLayout
336     * @param alternatesSelectedLayout
337     * @param padding Padding around the text in a chip
338     */
339    public void setChipDimensions(Drawable chipBackground, Drawable chipBackgroundPressed,
340            Drawable chipDelete, Bitmap defaultContact, int moreResource, int alternatesLayout,
341            int alternatesSelectedLayout, float chipHeight, float padding, float chipFontSize) {
342        mChipBackground = chipBackground;
343        mChipBackgroundPressed = chipBackgroundPressed;
344        mChipDelete = chipDelete;
345        mChipPadding = (int) padding;
346        mAlternatesLayout = alternatesLayout;
347        mAlternatesSelectedLayout = alternatesSelectedLayout;
348        mDefaultContactPhoto = defaultContact;
349        mMoreString = moreResource;
350        mChipHeight = chipHeight;
351        mChipFontSize = chipFontSize;
352    }
353
354    @Override
355    public void setTokenizer(Tokenizer tokenizer) {
356        mTokenizer = tokenizer;
357        super.setTokenizer(mTokenizer);
358    }
359
360    // We want to handle replacing text in the onItemClickListener
361    // so we can get all the associated contact information including
362    // display text, address, and id.
363    @Override
364    protected void replaceText(CharSequence text) {
365        return;
366    }
367
368    @Override
369    public boolean onKeyPreIme(int keyCode, KeyEvent event) {
370        if (keyCode == KeyEvent.KEYCODE_BACK) {
371            clearSelectedChip();
372        }
373        return super.onKeyPreIme(keyCode, event);
374    }
375
376    @Override
377    public boolean onKeyUp(int keyCode, KeyEvent event) {
378        switch (keyCode) {
379            case KeyEvent.KEYCODE_ENTER:
380            case KeyEvent.KEYCODE_DPAD_CENTER:
381            case KeyEvent.KEYCODE_TAB:
382                if (event.hasNoModifiers()) {
383                    if (commitDefault()) {
384                        return true;
385                    }
386                }
387                break;
388        }
389        return super.onKeyUp(keyCode, event);
390    }
391
392    // If the popup is showing, the default is the first item in the popup
393    // suggestions list. Otherwise, it is whatever the user had typed in.
394    private boolean commitDefault() {
395        Editable editable = getText();
396        boolean enough = enoughToFilter();
397        boolean shouldSubmitAtPosition = false;
398        int end = getSelectionEnd();
399        int start = mTokenizer.findTokenStart(editable, end);
400        if (enough) {
401            RecipientChip[] chips = getSpannable().getSpans(start, end, RecipientChip.class);
402            if ((chips == null || chips.length == 0)) {
403                // There's something being filtered or typed that has not been
404                // completed yet.
405                shouldSubmitAtPosition = true;
406            }
407        }
408
409        if (shouldSubmitAtPosition) {
410            if (getAdapter().getCount() > 0) {
411                // choose the first entry.
412                submitItemAtPosition(0);
413                dismissDropDown();
414                return true;
415            } else {
416                String text = editable.toString().substring(start, end);
417                clearComposingText();
418                if (text != null && text.length() > 0
419                        && (text.length() != 1 && text.charAt(0) != ' ')) {
420                    RecipientEntry entry = RecipientEntry.constructFakeEntry(text);
421                    QwertyKeyListener.markAsReplaced(editable, start, end, "");
422                    editable.replace(start, end, createChip(entry));
423                    dismissDropDown();
424                }
425                return false;
426            }
427        }
428        return false;
429    }
430
431    @Override
432    public boolean onKeyDown(int keyCode, KeyEvent event) {
433        if (mSelectedChip != null) {
434            mSelectedChip.onKeyDown(keyCode, event);
435        }
436
437        if (keyCode == KeyEvent.KEYCODE_ENTER && event.hasNoModifiers()) {
438            return true;
439        }
440
441        return super.onKeyDown(keyCode, event);
442    }
443
444    private Spannable getSpannable() {
445        return (Spannable) getText();
446    }
447
448    /**
449     * Instead of filtering on the entire contents of the edit box,
450     * this subclass method filters on the range from
451     * {@link Tokenizer#findTokenStart} to {@link #getSelectionEnd}
452     * if the length of that range meets or exceeds {@link #getThreshold}
453     * and makes sure that the range is not already a Chip.
454     */
455    @Override
456    protected void performFiltering(CharSequence text, int keyCode) {
457        if (enoughToFilter()) {
458            int end = getSelectionEnd();
459            int start = mTokenizer.findTokenStart(text, end);
460            // If this is a RecipientChip, don't filter
461            // on its contents.
462            Spannable span = getSpannable();
463            RecipientChip[] chips = span.getSpans(start, end, RecipientChip.class);
464            if (chips != null && chips.length > 0) {
465                return;
466            }
467        }
468        super.performFiltering(text, keyCode);
469    }
470
471    private void clearSelectedChip() {
472        if (mSelectedChip != null) {
473            mSelectedChip.unselectChip();
474            mSelectedChip = null;
475        }
476        setCursorVisible(true);
477    }
478
479    @Override
480    public boolean onTouchEvent(MotionEvent event) {
481        if (!isFocused()) {
482            // Ignore any chip taps until this view is focused.
483            return super.onTouchEvent(event);
484        }
485
486        boolean handled = super.onTouchEvent(event);
487        int action = event.getAction();
488        boolean chipWasSelected = false;
489
490        if (action == MotionEvent.ACTION_UP || action == MotionEvent.ACTION_DOWN) {
491            float x = event.getX();
492            float y = event.getY();
493            int offset = putOffsetInRange(getOffsetForPosition(x, y));
494            RecipientChip currentChip = findChip(offset);
495            if (currentChip != null) {
496                if (action == MotionEvent.ACTION_UP) {
497                    if (mSelectedChip != null && mSelectedChip != currentChip) {
498                        clearSelectedChip();
499                        mSelectedChip = currentChip.selectChip();
500                    } else if (mSelectedChip == null) {
501                        mSelectedChip = currentChip.selectChip();
502                    } else {
503                        mSelectedChip.onClick(this, offset, x, y);
504                    }
505                }
506                chipWasSelected = true;
507            }
508        }
509        if (action == MotionEvent.ACTION_UP && !chipWasSelected) {
510            clearSelectedChip();
511        }
512        return handled;
513    }
514
515    // TODO: This algorithm will need a lot of tweaking after more people have used
516    // the chips ui. This attempts to be "forgiving" to fat finger touches by favoring
517    // what comes before the finger.
518    private int putOffsetInRange(int o) {
519        int offset = o;
520        Editable text = getText();
521        int length = text.length();
522        // Remove whitespace from end to find "real end"
523        int realLength = length;
524        for (int i = length - 1; i >= 0; i--) {
525            if (text.charAt(i) == ' ') {
526                realLength--;
527            } else {
528                break;
529            }
530        }
531
532        // If the offset is beyond or at the end of the text,
533        // leave it alone.
534        if (offset >= realLength) {
535            return offset;
536        }
537        Editable editable = getText();
538        while (offset >= 0 && findText(editable, offset) == -1 && findChip(offset) == null) {
539            // Keep walking backward!
540            offset--;
541        }
542        return offset;
543    }
544
545    private int findText(Editable text, int offset) {
546        if (text.charAt(offset) != ' ') {
547            return offset;
548        }
549        return -1;
550    }
551
552    private RecipientChip findChip(int offset) {
553        RecipientChip[] chips = getSpannable().getSpans(0, getText().length(), RecipientChip.class);
554        // Find the chip that contains this offset.
555        for (int i = 0; i < chips.length; i++) {
556            RecipientChip chip = chips[i];
557            if (chip.matchesChip(offset)) {
558                return chip;
559            }
560        }
561        return null;
562    }
563
564    private CharSequence createChip(RecipientEntry entry) {
565        CharSequence displayText = mTokenizer.terminateToken(entry.getDestination());
566        // Always leave a blank space at the end of a chip.
567        int textLength = displayText.length();
568        if (displayText.charAt(textLength - 1) == ' ') {
569            textLength--;
570        } else {
571            displayText = displayText.toString().concat(" ");
572            textLength = displayText.length();
573        }
574        SpannableString chipText = new SpannableString(displayText);
575        int end = getSelectionEnd();
576        int start = mTokenizer.findTokenStart(getText(), end);
577        try {
578            chipText.setSpan(constructChipSpan(entry, start, false), 0, textLength,
579                    Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
580        } catch (NullPointerException e) {
581            Log.e(TAG, e.getMessage(), e);
582            return null;
583        }
584
585        return chipText;
586    }
587
588    @Override
589    public void onItemClick(AdapterView<?> parent, View view, int position, long id) {
590        submitItemAtPosition(position);
591    }
592
593    private void submitItemAtPosition(int position) {
594        RecipientEntry entry = (RecipientEntry) getAdapter().getItem(position);
595        // If the display name and the address are the same, then make this
596        // a fake recipient that is editable.
597        if (TextUtils.equals(entry.getDisplayName(), entry.getDestination())) {
598            entry = RecipientEntry.constructFakeEntry(entry.getDestination());
599        }
600        clearComposingText();
601
602        int end = getSelectionEnd();
603        int start = mTokenizer.findTokenStart(getText(), end);
604
605        Editable editable = getText();
606        QwertyKeyListener.markAsReplaced(editable, start, end, "");
607        editable.replace(start, end, createChip(entry));
608    }
609
610    /** Returns a collection of contact Id for each chip inside this View. */
611    /* package */ Collection<Long> getContactIds() {
612        final Set<Long> result = new HashSet<Long>();
613        for (RecipientChip chip : mRecipients) {
614            result.add(chip.getContactId());
615        }
616        return result;
617    }
618
619    /** Returns a collection of data Id for each chip inside this View. May be null. */
620    /* package */ Collection<Long> getDataIds() {
621        final Set<Long> result = new HashSet<Long>();
622        for (RecipientChip chip : mRecipients) {
623            result.add(chip.getDataId());
624        }
625        return result;
626    }
627
628
629    @Override
630    public boolean onActionItemClicked(ActionMode mode, MenuItem item) {
631        return false;
632    }
633
634    @Override
635    public void onDestroyActionMode(ActionMode mode) {
636    }
637
638    @Override
639    public boolean onPrepareActionMode(ActionMode mode, Menu menu) {
640        return false;
641    }
642
643    // Prevent selection of chips.
644    @Override
645    public boolean onCreateActionMode(ActionMode mode, Menu menu) {
646        return false;
647    }
648
649    // The more chip is text that replaces any chips that do not fit in the pre-defined
650    // available space when the RecipientEditTextView loses focus and is drawn in a
651    // collapsed fashion.
652    private ImageSpan createMoreChip() {
653        if (mRecipients == null || mRecipients.size() <= CHIP_LIMIT) {
654            return null;
655        }
656        int numRecipients = mRecipients.size();
657        int overage = numRecipients - CHIP_LIMIT;
658        Editable text = getText();
659        // TODO: get the correct size from visual design.
660        int width = (int) Math.floor(getWidth() * MORE_WIDTH_FACTOR);
661        int height = getLineHeight();
662        Bitmap drawable = Bitmap.createBitmap(width, height, Bitmap.Config.ARGB_8888);
663        Canvas canvas = new Canvas(drawable);
664        String moreText = getResources().getString(mMoreString, overage);
665        canvas.drawText(moreText, 0, moreText.length(), 0, height - getLayout().getLineDescent(0),
666                getPaint());
667
668        Drawable result = new BitmapDrawable(getResources(), drawable);
669        result.setBounds(0, 0, width, height);
670        ImageSpan moreSpan = new ImageSpan(result);
671        Spannable spannable = getSpannable();
672        // Remove the overage chips.
673        RecipientChip[] chips = spannable.getSpans(0, text.length(), RecipientChip.class);
674        if (chips == null || chips.length == 0) {
675            Log.w(TAG,
676                "We have recipients. Tt should not be possible to have zero RecipientChips.");
677            return null;
678        }
679        mRemovedSpans = new ArrayList<RecipientChip>();
680        int totalReplaceStart = 0;
681        int totalReplaceEnd = 0;
682        for (int i = numRecipients - overage; i < chips.length; i++) {
683            mRemovedSpans.add(chips[i]);
684            if (i == numRecipients - overage) {
685                totalReplaceStart = chips[i].getChipStart();
686            }
687            if (i == chips.length - 1) {
688                totalReplaceEnd = chips[i].getChipEnd();
689            }
690            chips[i].setPreviousChipStart(chips[i].getChipStart());
691            chips[i].setPreviousChipEnd(chips[i].getChipEnd());
692            spannable.removeSpan(chips[i]);
693        }
694
695        for (int i = chips.length - 1; i >= numRecipients - overage; i--) {
696            mRecipients.remove(i);
697        }
698        SpannableString chipText = new SpannableString(text.subSequence(totalReplaceStart,
699                totalReplaceEnd));
700        chipText.setSpan(moreSpan, 0, chipText.length(), Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
701        text.replace(totalReplaceStart, totalReplaceEnd, chipText);
702        return moreSpan;
703    }
704
705    // Replace the more chip, if it exists, with all of the recipient chips it had
706    // replaced when the RecipientEditTextView gains focus.
707    private void removeMoreChip() {
708        if (mMoreChip != null) {
709            Spannable span = getSpannable();
710            span.removeSpan(mMoreChip);
711            mMoreChip = null;
712            // Re-add the spans that were removed.
713            if (mRemovedSpans != null && mRemovedSpans.size() > 0) {
714                // Recreate each removed span.
715                Editable editable = getText();
716                SpannableString associatedText;
717                for (RecipientChip chip : mRemovedSpans) {
718                    int chipStart = chip.getPreviousChipStart();
719                    int chipEnd = chip.getPreviousChipEnd();
720                    associatedText = new SpannableString(editable.subSequence(chipStart, chipEnd));
721                    associatedText.setSpan(chip, 0, associatedText.length(),
722                            Spannable.SPAN_EXCLUSIVE_EXCLUSIVE);
723                    editable.replace(chipStart, chipEnd, associatedText);
724                    mRecipients.add(chip);
725                }
726                mRemovedSpans.clear();
727            }
728        }
729    }
730
731    /**
732     * RecipientChip defines an ImageSpan that contains information relevant to
733     * a particular recipient.
734     */
735    public class RecipientChip extends ImageSpan implements OnItemClickListener {
736        private final CharSequence mDisplay;
737
738        private final CharSequence mValue;
739
740        private final int mOffset;
741
742        private View mAnchorView;
743
744        private int mLeft;
745
746        private final long mContactId;
747
748        private final long mDataId;
749
750        private RecipientEntry mEntry;
751
752        private boolean mSelected = false;
753
754        private RecipientAlternatesAdapter mAlternatesAdapter;
755
756        private Rect mBounds;
757
758        private int mStart = -1;
759        private int mEnd = -1;
760
761        public RecipientChip(Drawable drawable, RecipientEntry entry, int offset, Rect bounds) {
762            super(drawable);
763            mDisplay = entry.getDisplayName();
764            mValue = entry.getDestination();
765            mContactId = entry.getContactId();
766            mDataId = entry.getDataId();
767            mOffset = offset;
768            mEntry = entry;
769            mBounds = bounds;
770
771            mAnchorView = new View(getContext());
772            mAnchorView.setLeft(bounds.left);
773            mAnchorView.setRight(bounds.left);
774            mAnchorView.setTop(bounds.bottom);
775            mAnchorView.setBottom(bounds.bottom);
776            mAnchorView.setVisibility(View.GONE);
777            mRecipients.add(this);
778            mStart = offset;
779            // Add +1 for comma (?)
780            mEnd = offset + mValue.length() + 1;
781        }
782
783        public int getPreviousChipStart() {
784            return mStart;
785        }
786
787        public int getPreviousChipEnd() {
788            return mEnd;
789        }
790
791        public void setPreviousChipStart(int start) {
792            mStart = start;
793        }
794
795        public void setPreviousChipEnd(int end) {
796            mEnd = end;
797        }
798
799        public void unselectChip() {
800            if (getChipStart() == -1 || getChipEnd() == -1) {
801                mSelectedChip = null;
802                return;
803            }
804            clearComposingText();
805            RecipientChip newChipSpan = null;
806            try {
807                newChipSpan = constructChipSpan(mEntry, mOffset, false);
808            } catch (NullPointerException e) {
809                Log.e(TAG, e.getMessage(), e);
810                return;
811            }
812            replace(newChipSpan);
813            if (mAlternatesPopup != null && mAlternatesPopup.isShowing()) {
814                mAlternatesPopup.dismiss();
815            }
816            return;
817        }
818
819        public void onKeyDown(int keyCode, KeyEvent event) {
820            if (keyCode == KeyEvent.KEYCODE_DEL) {
821                if (mAlternatesPopup != null && mAlternatesPopup.isShowing()) {
822                    mAlternatesPopup.dismiss();
823                }
824                removeChip();
825            }
826        }
827
828        public boolean isCompletedContact() {
829            return mContactId != -1;
830        }
831
832        private void replace(RecipientChip newChip) {
833            Spannable spannable = getSpannable();
834            int spanStart = getChipStart();
835            int spanEnd = getChipEnd();
836            boolean wasSelected = this == mSelectedChip;
837            if (wasSelected) {
838                mSelectedChip = null;
839            }
840            QwertyKeyListener.markAsReplaced(getText(), spanStart, spanEnd, "");
841            spannable.removeSpan(this);
842            mRecipients.remove(this);
843            spannable.setSpan(newChip, spanStart, spanEnd, 0);
844            if (wasSelected) {
845                clearSelectedChip();
846                mSelectedChip = newChip;
847            }
848        }
849
850        public void removeChip() {
851            Spannable spannable = getSpannable();
852            int spanStart = spannable.getSpanStart(this);
853            int spanEnd = spannable.getSpanEnd(this);
854            Editable text = getText();
855            int toDelete = spanEnd;
856            boolean wasSelected = this == mSelectedChip;
857            // Clear that there is a selected chip before updating any text.
858            if (wasSelected) {
859                mSelectedChip = null;
860            }
861            // Always remove trailing spaces when removing a chip.
862            while (toDelete >= 0 && toDelete < text.length() - 1 && text.charAt(toDelete) == ' ') {
863                toDelete++;
864            }
865            spannable.removeSpan(this);
866            mRecipients.remove(this);
867            text.delete(spanStart, toDelete);
868            if (wasSelected) {
869                clearSelectedChip();
870            }
871        }
872
873        public int getChipStart() {
874            return getSpannable().getSpanStart(this);
875        }
876
877        public int getChipEnd() {
878            return getSpannable().getSpanEnd(this);
879        }
880
881        public void replaceChip(RecipientEntry entry) {
882            clearComposingText();
883
884            RecipientChip newChipSpan = null;
885            try {
886                newChipSpan = constructChipSpan(entry, mOffset, false);
887            } catch (NullPointerException e) {
888                Log.e(TAG, e.getMessage(), e);
889                return;
890            }
891            replace(newChipSpan);
892            if (mAlternatesPopup != null && mAlternatesPopup.isShowing()) {
893                mAlternatesPopup.dismiss();
894            }
895        }
896
897        public RecipientChip selectChip() {
898            clearComposingText();
899            RecipientChip newChipSpan = null;
900            if (isCompletedContact()) {
901                try {
902                    newChipSpan = constructChipSpan(mEntry, mOffset, true);
903                    newChipSpan.setSelected(true);
904                } catch (NullPointerException e) {
905                    Log.e(TAG, e.getMessage(), e);
906                    return newChipSpan;
907                }
908                replace(newChipSpan);
909                if (mAlternatesPopup != null && mAlternatesPopup.isShowing()) {
910                    mAlternatesPopup.dismiss();
911                }
912                mSelected = true;
913                // Make sure we call edit on the new chip span.
914                newChipSpan.showAlternates();
915                setCursorVisible(false);
916            } else {
917                CharSequence text = getValue();
918                removeChip();
919                Editable editable = getText();
920                editable.append(text);
921                setCursorVisible(true);
922                setSelection(editable.length());
923            }
924            return newChipSpan;
925        }
926
927        private void showAlternates() {
928          //  mAlternatesPopup = new ListPopupWindow(getContext());
929
930            if (!mAlternatesPopup.isShowing()) {
931                mAlternatesAdapter = new RecipientAlternatesAdapter(
932                        getContext(),
933                        mEntry.getContactId(), mEntry.getDataId(),
934                        mAlternatesLayout, mAlternatesSelectedLayout);
935                mAnchorView.setLeft(mLeft);
936                mAnchorView.setRight(mLeft);
937                mAlternatesPopup.setAnchorView(mAnchorView);
938                mAlternatesPopup.setAdapter(mAlternatesAdapter);
939                mAlternatesPopup.setWidth(getWidth());
940                mAlternatesPopup.setOnItemClickListener(this);
941                mAlternatesPopup.show();
942            }
943        }
944
945        private void setSelected(boolean selected) {
946            mSelected = selected;
947        }
948
949        public CharSequence getDisplay() {
950            return mDisplay;
951        }
952
953        public CharSequence getValue() {
954            return mValue;
955        }
956
957        private boolean isInDelete(int offset, float x, float y) {
958            // Figure out the bounds of this chip and whether or not
959            // the user clicked in the X portion.
960            return mSelected
961                    && (offset == getChipEnd()
962                            || (x > (mBounds.right - mChipDeleteWidth) && x < mBounds.right));
963        }
964
965        public boolean matchesChip(int offset) {
966            int start = getChipStart();
967            int end = getChipEnd();
968            return (offset >= start && offset <= end);
969        }
970
971        public void onClick(View widget, int offset, float x, float y) {
972            if (mSelected) {
973                if (isInDelete(offset, x, y)) {
974                    removeChip();
975                } else {
976                    clearSelectedChip();
977                }
978            }
979        }
980
981        @Override
982        public void draw(Canvas canvas, CharSequence text, int start, int end, float x, int top,
983                int y, int bottom, Paint paint) {
984            // Shift the bounds of this span to where it is actually drawn on the screeen.
985            mLeft = (int) x;
986            super.draw(canvas, text, start, end, x, top, y, bottom, paint);
987        }
988
989        @Override
990        public void onItemClick(AdapterView<?> adapterView, View view, int position, long rowId) {
991            mAlternatesPopup.dismiss();
992            clearComposingText();
993            replaceChip(mAlternatesAdapter.getRecipientEntry(position));
994        }
995
996        public long getContactId() {
997            return mContactId;
998        }
999
1000        public long getDataId() {
1001            return mDataId;
1002        }
1003    }
1004}
1005
1006