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