RecipientEditTextView.java revision efcac0cbb3efc645cd6cf1cb1e2431e1bd2b2d2a
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.Canvas;
22import android.graphics.Paint;
23import android.graphics.Rect;
24import android.graphics.drawable.BitmapDrawable;
25import android.graphics.drawable.Drawable;
26import android.os.Handler;
27import android.text.Editable;
28import android.text.Layout;
29import android.text.Selection;
30import android.text.Spannable;
31import android.text.SpannableString;
32import android.text.Spanned;
33import android.text.TextPaint;
34import android.text.TextUtils;
35import android.text.method.QwertyKeyListener;
36import android.text.style.ImageSpan;
37import android.util.AttributeSet;
38import android.util.Log;
39import android.view.KeyEvent;
40import android.view.MotionEvent;
41import android.view.View;
42import android.widget.AdapterView;
43import android.widget.AdapterView.OnItemClickListener;
44import android.widget.PopupWindow.OnDismissListener;
45import android.widget.ListPopupWindow;
46import android.widget.MultiAutoCompleteTextView;
47
48/**
49 * RecipientEditTextView is an auto complete text view for use with applications
50 * that use the new Chips UI for addressing a message to recipients.
51 */
52public class RecipientEditTextView extends MultiAutoCompleteTextView
53    implements OnItemClickListener {
54
55    private static final String TAG = "RecipientEditTextView";
56
57    private Drawable mChipBackground = null;
58
59    private int mChipPadding;
60
61    private int mChipPopupOffset;
62
63    private Tokenizer mTokenizer;
64
65    private final Handler mHandler;
66
67    private Runnable mDelayedSelectionMode = new Runnable() {
68        @Override
69        public void run() {
70            setSelection(getText().length());
71        }
72    };
73
74    public RecipientEditTextView(Context context, AttributeSet attrs) {
75        super(context, attrs);
76        mHandler = new Handler();
77        setOnItemClickListener(this);
78    }
79
80    public RecipientChip constructChipSpan(CharSequence text, int offset, boolean pressed)
81        throws NullPointerException {
82        if (mChipBackground == null) {
83            throw new NullPointerException
84                ("Unable to render any chips as setChipDimensions was not called.");
85        }
86
87        Layout layout = getLayout();
88        int line = layout.getLineForOffset(offset);
89        int lineTop = layout.getLineTop(line);
90        int lineBottom = layout.getLineBottom(line);
91
92        TextPaint paint = getPaint();
93        float defaultSize = paint.getTextSize();
94
95        // Reduce the size of the text slightly so that we can get the "look" of
96        // padding.
97        paint.setTextSize((float) (paint.getTextSize() * .9));
98
99        // Ellipsize the text so that it takes AT MOST the entire width of the
100        // autocomplete text entry area. Make sure to leave space for padding
101        // on the sides.
102        CharSequence ellipsizedText = TextUtils.ellipsize(text, paint, calculateAvailableWidth(),
103                TextUtils.TruncateAt.END);
104
105        int height = getLineHeight();
106        int width = (int) Math.floor(paint.measureText(ellipsizedText, 0, ellipsizedText.length()))
107                + (mChipPadding * 2);
108
109        // Create the background of the chip.
110        Bitmap tmpBitmap = Bitmap.createBitmap(width, height, Bitmap.Config.ARGB_8888);
111        Canvas canvas = new Canvas(tmpBitmap);
112        if (mChipBackground != null) {
113            mChipBackground.setBounds(0, 0, width, height);
114            mChipBackground.draw(canvas);
115        } else {
116            Log.w(TAG,
117                    "Unable to draw a background for the chips as it was never set");
118        }
119
120        // Align the display text with where the user enters text.
121        canvas.drawText(ellipsizedText, 0, ellipsizedText.length(), mChipPadding, height
122                - layout.getLineDescent(line), paint);
123
124        // Get the location of the widget so we can properly offset
125        // the anchor for each chip.
126        int[] xy = new int[2];
127        getLocationOnScreen(xy);
128        // Pass the full text, un-ellipsized, to the chip.
129        Drawable result = new BitmapDrawable(getResources(), tmpBitmap);
130        result.setBounds(0, 0, width, height);
131        Rect bounds = new Rect(xy[0] + offset, xy[1] + lineTop, xy[0] + width, xy[1] + lineBottom);
132        RecipientChip recipientChip = new RecipientChip(result, text, text, -1, offset, bounds);
133
134        // Return text to the original size.
135        paint.setTextSize(defaultSize);
136
137        return recipientChip;
138    }
139
140    // Get the max amount of space a chip can take up. The formula takes into
141    // account the width of the EditTextView, any view padding, and padding
142    // that will be added to the chip.
143    private float calculateAvailableWidth() {
144        return getWidth() - getPaddingLeft() - getPaddingRight() - (mChipPadding * 2);
145    }
146
147    /**
148     * Set all chip dimensions and resources. This has to be done from the application
149     * as this is a static library.
150     * @param chipBackground drawable
151     * @param padding Padding around the text in a chip
152     * @param offset Offset between the chip and the dropdown of alternates
153     */
154    public void setChipDimensions(Drawable chipBackground, float padding, float offset) {
155        mChipBackground = chipBackground;
156        mChipPadding = (int) padding;
157        mChipPopupOffset = (int) offset;
158    }
159
160    @Override
161    public void setTokenizer(Tokenizer tokenizer) {
162        mTokenizer = tokenizer;
163        super.setTokenizer(mTokenizer);
164    }
165
166    // We want to handle replacing text in the onItemClickListener
167    // so we can get all the associated contact information including
168    // display text, address, and id.
169    @Override
170    protected void replaceText(CharSequence text) {
171        return;
172    }
173
174    @Override
175    public boolean onKeyUp(int keyCode, KeyEvent event) {
176        switch (keyCode) {
177            case KeyEvent.KEYCODE_ENTER:
178            case KeyEvent.KEYCODE_DPAD_CENTER:
179            case KeyEvent.KEYCODE_TAB:
180                if (event.hasNoModifiers()) {
181                    if (isPopupShowing()) {
182                        // choose the first entry.
183                        submitItemAtPosition(0);
184                        dismissDropDown();
185                        return true;
186                    } else {
187                        int end = getSelectionEnd();
188                        int start = mTokenizer.findTokenStart(getText(), end);
189                        String text = getText().toString().substring(start, end);
190                        clearComposingText();
191
192                        Editable editable = getText();
193
194                        QwertyKeyListener.markAsReplaced(editable, start, end, "");
195                        editable.replace(start, end, createChip(text));
196                        dismissDropDown();
197                    }
198                }
199        }
200        return super.onKeyUp(keyCode, event);
201    }
202
203    public void onChipChanged() {
204        // Must be posted so that the previous span
205        // is correctly replaced with the previous selection points.
206        mHandler.post(mDelayedSelectionMode);
207    }
208
209    @Override
210    public boolean onKeyDown(int keyCode, KeyEvent event) {
211        int start = getSelectionStart();
212        int end = getSelectionEnd();
213        Spannable span = getSpannable();
214
215        RecipientChip[] chips = span.getSpans(start, end, RecipientChip.class);
216        if (chips != null) {
217            for (RecipientChip chip : chips) {
218                chip.onKeyDown(keyCode, event);
219            }
220        }
221
222        if (keyCode == KeyEvent.KEYCODE_ENTER && event.hasNoModifiers()) {
223            return true;
224        }
225
226        return super.onKeyDown(keyCode, event);
227    }
228
229    private Spannable getSpannable() {
230        return (Spannable) getText();
231    }
232
233
234    @Override
235    public boolean onTouchEvent(MotionEvent event) {
236        int action = event.getAction();
237        if (action == MotionEvent.ACTION_UP || action == MotionEvent.ACTION_DOWN) {
238            Spannable span = getSpannable();
239            int offset = getOffsetForPosition(event.getX(), event.getY());
240            int start = offset;
241            int end = span.length();
242            RecipientChip[] chips = span.getSpans(start, end, RecipientChip.class);
243            if (chips != null && chips.length > 0) {
244                // Get the first chip that matched.
245                final RecipientChip currentChip = chips[0];
246
247                if (action == MotionEvent.ACTION_UP) {
248                    currentChip.onClick(this);
249                } else if (action == MotionEvent.ACTION_DOWN) {
250                    Selection.setSelection(getSpannable(), currentChip.getChipStart(), currentChip
251                            .getChipEnd());
252                }
253                return true;
254            }
255        }
256
257        return super.onTouchEvent(event);
258    }
259
260    private CharSequence createChip(String text) {
261        // We want to override the tokenizer behavior with our own ending
262        // token, space.
263        SpannableString chipText = new SpannableString(mTokenizer.terminateToken(text));
264        int end = getSelectionEnd();
265        int start = mTokenizer.findTokenStart(getText(), end);
266        try {
267            chipText.setSpan(constructChipSpan(text, start, false), 0, text.length(),
268                    Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
269        } catch (NullPointerException e) {
270            Log.e(TAG, e.getMessage());
271            return null;
272        }
273
274        return chipText;
275    }
276
277    @Override
278    public void onItemClick(AdapterView<?> parent, View view, int position, long id) {
279        submitItemAtPosition(position);
280    }
281
282    private void submitItemAtPosition(int position) {
283        RecipientListEntry entry = (RecipientListEntry) getAdapter().getItem(position);
284        clearComposingText();
285
286        int end = getSelectionEnd();
287        int start = mTokenizer.findTokenStart(getText(), end);
288
289        Editable editable = getText();
290        editable.replace(start, end, createChip(entry.getDisplayName()));
291        QwertyKeyListener.markAsReplaced(editable, start, end, "");
292    }
293
294    /**
295     * RecipientChip defines an ImageSpan that contains information relevant to
296     * a particular recipient.
297     */
298    public class RecipientChip extends ImageSpan implements OnItemClickListener, OnDismissListener {
299        private final CharSequence mDisplay;
300
301        private final CharSequence mValue;
302
303        private final int mOffset;
304
305        private ListPopupWindow mPopup;
306
307        private View mAnchorView;
308
309        private int mLeft;
310
311        private int mId = -1;
312
313        public RecipientChip(Drawable drawable, CharSequence text, CharSequence value, int id,
314                int offset, Rect bounds) {
315            super(drawable);
316            mDisplay = text;
317            mValue = value;
318            mOffset = offset;
319            mAnchorView = new View(getContext());
320            mAnchorView.setLeft(bounds.left);
321            mAnchorView.setRight(bounds.left);
322            mAnchorView.setTop(bounds.bottom + mChipPopupOffset);
323            mAnchorView.setBottom(bounds.bottom + mChipPopupOffset);
324            mAnchorView.setVisibility(View.GONE);
325
326            mId = id;
327        }
328
329        public void onKeyDown(int keyCode, KeyEvent event) {
330            if (keyCode == KeyEvent.KEYCODE_DEL) {
331                if (mPopup != null && mPopup.isShowing()) {
332                    mPopup.dismiss();
333                }
334                removeChip();
335            }
336        }
337
338        public boolean isCompletedContact() {
339            return mId != -1;
340        }
341
342        private void replace(RecipientChip newChip) {
343            Spannable spannable = getSpannable();
344            int spanStart = getChipStart();
345            int spanEnd = getChipEnd();
346            QwertyKeyListener.markAsReplaced(getText(), spanStart, spanEnd, "");
347            spannable.removeSpan(this);
348            spannable.setSpan(newChip, spanStart, spanEnd, 0);
349        }
350
351        public void removeChip() {
352            Spannable spannable = getSpannable();
353            int spanStart = getChipStart();
354            int spanEnd = getChipEnd();
355            QwertyKeyListener.markAsReplaced(getText(), spanStart, spanEnd, "");
356            spannable.removeSpan(this);
357            spannable.setSpan(null, spanStart, spanEnd, 0);
358            onChipChanged();
359        }
360
361        public int getChipStart() {
362            return getSpannable().getSpanStart(this);
363        }
364
365        public int getChipEnd() {
366            return getSpannable().getSpanEnd(this);
367        }
368
369        public void replaceChip(String text) {
370            clearComposingText();
371
372            RecipientChip newChipSpan = null;
373            try {
374                newChipSpan = constructChipSpan(text, mOffset, false);
375            } catch (NullPointerException e) {
376                Log.e(TAG, e.getMessage());
377                return;
378            }
379            replace(newChipSpan);
380            if (mPopup != null && mPopup.isShowing()) {
381                mPopup.dismiss();
382            }
383            onChipChanged();
384        }
385
386        public CharSequence getDisplay() {
387            return mDisplay;
388        }
389
390        public CharSequence getValue() {
391            return mValue;
392        }
393
394        public void onClick(View widget) {
395            mPopup = new ListPopupWindow(widget.getContext());
396
397            if (!mPopup.isShowing()) {
398                mAnchorView.setLeft(mLeft);
399                mAnchorView.setRight(mLeft);
400                mPopup.setAnchorView(mAnchorView);
401                mPopup.setAdapter(getAdapter());
402                // TODO: get width from dimen.xml.
403                mPopup.setWidth(200);
404                mPopup.setOnItemClickListener(this);
405                mPopup.setOnDismissListener(this);
406                mPopup.show();
407            }
408        }
409
410        @Override
411        public void draw(Canvas canvas, CharSequence text, int start, int end, float x, int top,
412                int y, int bottom, Paint paint) {
413            mLeft = (int) x;
414            super.draw(canvas, text, start, end, x, top, y, bottom, paint);
415        }
416
417        @Override
418        public void onItemClick(AdapterView<?> adapterView, View view, int position, long rowId) {
419            mPopup.dismiss();
420            clearComposingText();
421            RecipientListEntry entry = (RecipientListEntry) adapterView.getItemAtPosition(position);
422            replaceChip(entry.getDisplayName());
423        }
424
425        // When the popup dialog is dismissed, return the cursor to the end.
426        @Override
427        public void onDismiss() {
428            mHandler.post(mDelayedSelectionMode);
429        }
430    }
431}
432