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