RecipientEditTextView.java revision f026dfb761c894942354060746a8ab7dd563386c
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.ColorDrawable; 26import android.graphics.drawable.Drawable; 27import android.os.Handler; 28import android.text.Editable; 29import android.text.Layout; 30import android.text.Selection; 31import android.text.Spannable; 32import android.text.SpannableString; 33import android.text.Spanned; 34import android.text.TextPaint; 35import android.text.TextUtils; 36import android.text.method.QwertyKeyListener; 37import android.text.style.ImageSpan; 38import android.util.AttributeSet; 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.ListView; 47import android.widget.MultiAutoCompleteTextView; 48 49/** 50 * RecipientEditTextView is an auto complete text view for use with applications 51 * that use the new Chips UI for addressing a message to recipients. 52 */ 53public class RecipientEditTextView extends MultiAutoCompleteTextView 54 implements OnItemClickListener { 55 56 private static final int DEFAULT_CHIP_BACKGROUND = 0x77CCCCCC; 57 58 private static final int CHIP_PADDING = 10; 59 60 public static String CHIP_BACKGROUND = "chipBackground"; 61 62 // TODO: eliminate this and take the pressed state from the provided 63 // drawable. 64 public static String CHIP_BACKGROUND_PRESSED = "chipBackgroundPressed"; 65 66 private Drawable mChipBackground = null; 67 68 private Tokenizer mTokenizer; 69 70 private final Handler mHandler; 71 72 private Runnable mDelayedSelectionMode = new Runnable() { 73 @Override 74 public void run() { 75 setSelection(getText().length()); 76 } 77 }; 78 79 80 public RecipientEditTextView(Context context, AttributeSet attrs) { 81 super(context, attrs); 82 mHandler = new Handler(); 83 setOnItemClickListener(this); 84 } 85 86 public RecipientChip constructChipSpan(CharSequence text, int offset, boolean pressed) { 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 + (CHIP_PADDING * 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 ColorDrawable color = new ColorDrawable(DEFAULT_CHIP_BACKGROUND); 117 color.setBounds(0, 0, width, height); 118 color.draw(canvas); 119 } 120 121 // Align the display text with where the user enters text. 122 canvas.drawText(ellipsizedText, 0, ellipsizedText.length(), CHIP_PADDING, height 123 - layout.getLineDescent(line), paint); 124 125 // Get the location of the widget so we can properly offset 126 // the anchor for each chip. 127 int[] xy = new int[2]; 128 getLocationOnScreen(xy); 129 // Pass the full text, un-ellipsized, to the chip. 130 Drawable result = new BitmapDrawable(getResources(), tmpBitmap); 131 result.setBounds(0, 0, width, height); 132 Rect bounds = new Rect(xy[0] + offset, xy[1] + lineTop, xy[0] + width, xy[1] + lineBottom); 133 RecipientChip recipientChip = new RecipientChip(result, text, text, -1, offset, bounds); 134 135 // Return text to the original size. 136 paint.setTextSize(defaultSize); 137 138 return recipientChip; 139 } 140 141 // Get the max amount of space a chip can take up. The formula takes into 142 // account the width of the EditTextView, any view padding, and padding 143 // that will be added to the chip. 144 private float calculateAvailableWidth() { 145 return getWidth() - getPaddingLeft() - getPaddingRight() - (CHIP_PADDING * 2); 146 } 147 148 public void setChipBackgroundDrawable(Drawable d) { 149 mChipBackground = d; 150 } 151 152 @Override 153 public void setTokenizer(Tokenizer tokenizer) { 154 mTokenizer = tokenizer; 155 super.setTokenizer(mTokenizer); 156 } 157 158 // We want to handle replacing text in the onItemClickListener 159 // so we can get all the associated contact information including 160 // display text, address, and id. 161 @Override 162 protected void replaceText(CharSequence text) { 163 return; 164 } 165 166 // TODO: this should be handled by the framework directly; working with 167 // @debunne to figure out why it isn't being handled properly. 168 @Override 169 public boolean onKeyUp(int keyCode, KeyEvent event) { 170 switch (keyCode) { 171 case KeyEvent.KEYCODE_ENTER: 172 case KeyEvent.KEYCODE_DPAD_CENTER: 173 case KeyEvent.KEYCODE_TAB: 174 if (event.hasNoModifiers()) { 175 if (getListSelection() != ListView.INVALID_POSITION) { 176 performCompletion(); 177 return true; 178 } else { 179 int end = getSelectionEnd(); 180 int start = mTokenizer.findTokenStart(getText(), end); 181 String text = getText().toString().substring(start, end); 182 clearComposingText(); 183 184 Editable editable = getText(); 185 186 QwertyKeyListener.markAsReplaced(editable, start, end, ""); 187 editable.replace(start, end, createChip(text)); 188 dismissDropDown(); 189 } 190 } 191 } 192 return super.onKeyUp(keyCode, event); 193 } 194 195 public void onChipChanged() { 196 // Must be posted so that the previous span 197 // is correctly replaced with the previous selection points. 198 mHandler.post(mDelayedSelectionMode); 199 } 200 201 @Override 202 public boolean onKeyDown(int keyCode, KeyEvent event) { 203 int start = getSelectionStart(); 204 int end = getSelectionEnd(); 205 Spannable span = getSpannable(); 206 207 RecipientChip[] chips = span.getSpans(start, end, RecipientChip.class); 208 if (chips != null) { 209 for (RecipientChip chip : chips) { 210 chip.onKeyDown(keyCode, event); 211 } 212 } 213 214 if (keyCode == KeyEvent.KEYCODE_ENTER && event.hasNoModifiers()) { 215 return true; 216 } 217 218 return super.onKeyDown(keyCode, event); 219 } 220 221 public Spannable getSpannable() { 222 return (Spannable) getText(); 223 } 224 225 /** 226 * RecipientChip defines an ImageSpan that contains information relevant to 227 * a particular recipient. 228 */ 229 public class RecipientChip extends ImageSpan implements OnItemClickListener, OnDismissListener { 230 private final CharSequence mDisplay; 231 232 private final CharSequence mValue; 233 234 private final int mOffset; 235 236 private ListPopupWindow mPopup; 237 238 private View mAnchorView; 239 240 private int mLeft; 241 242 private int mId = -1; 243 244 public RecipientChip(Drawable drawable, CharSequence text, CharSequence value, int id, 245 int offset, Rect bounds) { 246 super(drawable); 247 mDisplay = text; 248 mValue = value; 249 mOffset = offset; 250 mAnchorView = new View(getContext()); 251 mAnchorView.setLeft(bounds.left); 252 mAnchorView.setRight(bounds.left); 253 mAnchorView.setTop(bounds.right + CHIP_PADDING); 254 mAnchorView.setBottom(bounds.right + CHIP_PADDING); 255 mAnchorView.setVisibility(View.GONE); 256 257 mId = id; 258 } 259 260 public void onKeyDown(int keyCode, KeyEvent event) { 261 if (keyCode == KeyEvent.KEYCODE_DEL) { 262 if (mPopup != null && mPopup.isShowing()) { 263 mPopup.dismiss(); 264 } 265 removeChip(); 266 } 267 } 268 269 public boolean isCompletedContact() { 270 return mId != -1; 271 } 272 273 private void replace(RecipientChip newChip) { 274 Spannable spannable = getSpannable(); 275 int spanStart = getChipStart(); 276 int spanEnd = getChipEnd(); 277 QwertyKeyListener.markAsReplaced(getText(), spanStart, spanEnd, ""); 278 spannable.removeSpan(this); 279 spannable.setSpan(newChip, spanStart, spanEnd, 0); 280 } 281 282 public void removeChip() { 283 Spannable spannable = getSpannable(); 284 int spanStart = getChipStart(); 285 int spanEnd = getChipEnd(); 286 QwertyKeyListener.markAsReplaced(getText(), spanStart, spanEnd, ""); 287 spannable.removeSpan(this); 288 spannable.setSpan(null, spanStart, spanEnd, 0); 289 onChipChanged(); 290 } 291 292 public int getChipStart() { 293 return getSpannable().getSpanStart(this); 294 } 295 296 public int getChipEnd() { 297 return getSpannable().getSpanEnd(this); 298 } 299 300 public void replaceChip(String text) { 301 clearComposingText(); 302 303 RecipientChip newChipSpan = constructChipSpan(text, mOffset, false); 304 replace(newChipSpan); 305 if (mPopup != null && mPopup.isShowing()) { 306 mPopup.dismiss(); 307 } 308 onChipChanged(); 309 } 310 311 public CharSequence getDisplay() { 312 return mDisplay; 313 } 314 315 public CharSequence getValue() { 316 return mValue; 317 } 318 319 public void onClick(View widget) { 320 mPopup = new ListPopupWindow(widget.getContext()); 321 322 if (!mPopup.isShowing()) { 323 mAnchorView.setLeft(mLeft); 324 mAnchorView.setRight(mLeft); 325 mPopup.setAnchorView(mAnchorView); 326 mPopup.setAdapter(getAdapter()); 327 // TODO: get width from dimen.xml. 328 mPopup.setWidth(200); 329 mPopup.setOnItemClickListener(this); 330 mPopup.setOnDismissListener(this); 331 mPopup.show(); 332 } 333 } 334 335 @Override 336 public void draw(Canvas canvas, CharSequence text, int start, int end, float x, int top, 337 int y, int bottom, Paint paint) { 338 mLeft = (int) x; 339 super.draw(canvas, text, start, end, x, top, y, bottom, paint); 340 } 341 342 @Override 343 public void onItemClick(AdapterView<?> adapterView, View view, int position, long rowId) { 344 mPopup.dismiss(); 345 clearComposingText(); 346 RecipientListEntry entry = (RecipientListEntry) adapterView.getItemAtPosition(position); 347 replaceChip(entry.getDisplayName()); 348 } 349 350 // When the popup dialog is dismissed, return the cursor to the end. 351 @Override 352 public void onDismiss() { 353 mHandler.post(mDelayedSelectionMode); 354 } 355 } 356 357 @Override 358 public boolean onTouchEvent(MotionEvent event) { 359 int action = event.getAction(); 360 if (action == MotionEvent.ACTION_UP || action == MotionEvent.ACTION_DOWN) { 361 Spannable span = getSpannable(); 362 int offset = getOffsetForPosition(event.getX(), event.getY()); 363 int start = offset; 364 int end = span.length(); 365 RecipientChip[] chips = span.getSpans(start, end, RecipientChip.class); 366 if (chips != null && chips.length > 0) { 367 // Get the first chip that matched. 368 final RecipientChip currentChip = chips[0]; 369 370 if (action == MotionEvent.ACTION_UP) { 371 currentChip.onClick(this); 372 } else if (action == MotionEvent.ACTION_DOWN) { 373 Selection.setSelection(getSpannable(), currentChip.getChipStart(), currentChip 374 .getChipEnd()); 375 } 376 return true; 377 } 378 } 379 380 return super.onTouchEvent(event); 381 } 382 383 private CharSequence createChip(String text) { 384 // We want to override the tokenizer behavior with our own ending 385 // token, space. 386 SpannableString chipText = new SpannableString(mTokenizer.terminateToken(text)); 387 int end = getSelectionEnd(); 388 int start = mTokenizer.findTokenStart(getText(), end); 389 chipText.setSpan(constructChipSpan(text, start, false), 0, text.length(), 390 Spanned.SPAN_EXCLUSIVE_EXCLUSIVE); 391 return chipText; 392 } 393 394 @Override 395 public void onItemClick(AdapterView<?> parent, View view, int position, long id) { 396 // Figure out what got clicked! 397 RecipientListEntry entry = (RecipientListEntry) parent.getItemAtPosition(position); 398 clearComposingText(); 399 400 int end = getSelectionEnd(); 401 int start = mTokenizer.findTokenStart(getText(), end); 402 403 Editable editable = getText(); 404 editable.replace(start, end, createChip(entry.getDisplayName())); 405 QwertyKeyListener.markAsReplaced(editable, start, end, ""); 406 } 407} 408