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