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