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