WebTextView.java revision 0857767516a73cc87e10c8ababa5262114fb0578
1/* 2 * Copyright (C) 2007 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 android.webkit; 18 19import android.content.Context; 20import android.graphics.Canvas; 21import android.graphics.Color; 22import android.graphics.ColorFilter; 23import android.graphics.Paint; 24import android.graphics.PixelFormat; 25import android.graphics.Rect; 26import android.graphics.drawable.Drawable; 27import android.text.Editable; 28import android.text.InputFilter; 29import android.text.Selection; 30import android.text.Spannable; 31import android.text.TextPaint; 32import android.text.TextUtils; 33import android.text.method.MovementMethod; 34import android.util.Log; 35import android.view.Gravity; 36import android.view.KeyCharacterMap; 37import android.view.KeyEvent; 38import android.view.MotionEvent; 39import android.view.View; 40import android.view.ViewGroup; 41import android.view.inputmethod.EditorInfo; 42import android.view.inputmethod.InputMethodManager; 43import android.view.inputmethod.InputConnection; 44import android.widget.AbsoluteLayout.LayoutParams; 45import android.widget.ArrayAdapter; 46import android.widget.AutoCompleteTextView; 47import android.widget.TextView; 48 49import java.util.ArrayList; 50 51/** 52 * WebTextView is a specialized version of EditText used by WebView 53 * to overlay html textfields (and textareas) to use our standard 54 * text editing. 55 */ 56/* package */ class WebTextView extends AutoCompleteTextView { 57 58 static final String LOGTAG = "webtextview"; 59 60 private WebView mWebView; 61 private boolean mSingle; 62 private int mWidthSpec; 63 private int mHeightSpec; 64 private int mNodePointer; 65 // FIXME: This is a hack for blocking unmatched key ups, in particular 66 // on the enter key. The method for blocking unmatched key ups prevents 67 // the shift key from working properly. 68 private boolean mGotEnterDown; 69 private int mMaxLength; 70 // Keep track of the text before the change so we know whether we actually 71 // need to send down the DOM events. 72 private String mPreChange; 73 private Drawable mBackground; 74 // Array to store the final character added in onTextChanged, so that its 75 // KeyEvents may be determined. 76 private char[] mCharacter = new char[1]; 77 // This is used to reset the length filter when on a textfield 78 // with no max length. 79 // FIXME: This can be replaced with TextView.NO_FILTERS if that 80 // is made public/protected. 81 private static final InputFilter[] NO_FILTERS = new InputFilter[0]; 82 83 /** 84 * Create a new WebTextView. 85 * @param context The Context for this WebTextView. 86 * @param webView The WebView that created this. 87 */ 88 /* package */ WebTextView(Context context, WebView webView) { 89 super(context); 90 mWebView = webView; 91 mMaxLength = -1; 92 setImeOptions(EditorInfo.IME_ACTION_NONE); 93 } 94 95 @Override 96 public boolean dispatchKeyEvent(KeyEvent event) { 97 if (event.isSystem()) { 98 return super.dispatchKeyEvent(event); 99 } 100 // Treat ACTION_DOWN and ACTION MULTIPLE the same 101 boolean down = event.getAction() != KeyEvent.ACTION_UP; 102 int keyCode = event.getKeyCode(); 103 104 boolean isArrowKey = false; 105 switch(keyCode) { 106 case KeyEvent.KEYCODE_DPAD_LEFT: 107 case KeyEvent.KEYCODE_DPAD_RIGHT: 108 case KeyEvent.KEYCODE_DPAD_UP: 109 case KeyEvent.KEYCODE_DPAD_DOWN: 110 if (!mWebView.nativeCursorMatchesFocus()) { 111 return down ? mWebView.onKeyDown(keyCode, event) : mWebView 112 .onKeyUp(keyCode, event); 113 114 } 115 isArrowKey = true; 116 break; 117 } 118 119 if (!isArrowKey && mWebView.nativeFocusNodePointer() != mNodePointer) { 120 mWebView.nativeClearCursor(); 121 remove(); 122 return mWebView.dispatchKeyEvent(event); 123 } 124 125 Spannable text = (Spannable) getText(); 126 int oldLength = text.length(); 127 // Normally the delete key's dom events are sent via onTextChanged. 128 // However, if the length is zero, the text did not change, so we 129 // go ahead and pass the key down immediately. 130 if (KeyEvent.KEYCODE_DEL == keyCode && 0 == oldLength) { 131 sendDomEvent(event); 132 return true; 133 } 134 135 if ((mSingle && KeyEvent.KEYCODE_ENTER == keyCode)) { 136 if (isPopupShowing()) { 137 return super.dispatchKeyEvent(event); 138 } 139 if (!down) { 140 // Hide the keyboard, since the user has just submitted this 141 // form. The submission happens thanks to the two calls 142 // to sendDomEvent. 143 InputMethodManager.getInstance(mContext) 144 .hideSoftInputFromWindow(getWindowToken(), 0); 145 sendDomEvent(new KeyEvent(KeyEvent.ACTION_DOWN, keyCode)); 146 sendDomEvent(event); 147 } 148 return super.dispatchKeyEvent(event); 149 } else if (KeyEvent.KEYCODE_DPAD_CENTER == keyCode) { 150 // Note that this handles center key and trackball. 151 if (isPopupShowing()) { 152 return super.dispatchKeyEvent(event); 153 } 154 if (!mWebView.nativeCursorMatchesFocus()) { 155 return down ? mWebView.onKeyDown(keyCode, event) : mWebView 156 .onKeyUp(keyCode, event); 157 } 158 // Center key should be passed to a potential onClick 159 if (!down) { 160 mWebView.shortPressOnTextField(); 161 } 162 // Pass to super to handle longpress. 163 return super.dispatchKeyEvent(event); 164 } 165 166 // Ensure there is a layout so arrow keys are handled properly. 167 if (getLayout() == null) { 168 measure(mWidthSpec, mHeightSpec); 169 } 170 int oldStart = Selection.getSelectionStart(text); 171 int oldEnd = Selection.getSelectionEnd(text); 172 173 boolean maxedOut = mMaxLength != -1 && oldLength == mMaxLength; 174 // If we are at max length, and there is a selection rather than a 175 // cursor, we need to store the text to compare later, since the key 176 // may have changed the string. 177 String oldText; 178 if (maxedOut && oldEnd != oldStart) { 179 oldText = text.toString(); 180 } else { 181 oldText = ""; 182 } 183 if (super.dispatchKeyEvent(event)) { 184 // If the WebTextView handled the key it was either an alphanumeric 185 // key, a delete, or a movement within the text. All of those are 186 // ok to pass to javascript. 187 188 // UNLESS there is a max length determined by the html. In that 189 // case, if the string was already at the max length, an 190 // alphanumeric key will be erased by the LengthFilter, 191 // so do not pass down to javascript, and instead 192 // return true. If it is an arrow key or a delete key, we can go 193 // ahead and pass it down. 194 if (KeyEvent.KEYCODE_ENTER == keyCode) { 195 // For multi-line text boxes, newlines will 196 // trigger onTextChanged for key down (which will send both 197 // key up and key down) but not key up. 198 mGotEnterDown = true; 199 } 200 if (maxedOut && !isArrowKey && keyCode != KeyEvent.KEYCODE_DEL) { 201 if (oldEnd == oldStart) { 202 // Return true so the key gets dropped. 203 return true; 204 } else if (!oldText.equals(getText().toString())) { 205 // FIXME: This makes the text work properly, but it 206 // does not pass down the key event, so it may not 207 // work for a textfield that has the type of 208 // behavior of GoogleSuggest. That said, it is 209 // unlikely that a site would combine the two in 210 // one textfield. 211 Spannable span = (Spannable) getText(); 212 int newStart = Selection.getSelectionStart(span); 213 int newEnd = Selection.getSelectionEnd(span); 214 mWebView.replaceTextfieldText(0, oldLength, span.toString(), 215 newStart, newEnd); 216 return true; 217 } 218 } 219 /* FIXME: 220 * In theory, we would like to send the events for the arrow keys. 221 * However, the TextView can arbitrarily change the selection (i.e. 222 * long press followed by using the trackball). Therefore, we keep 223 * in sync with the TextView via onSelectionChanged. If we also 224 * send the DOM event, we lose the correct selection. 225 if (isArrowKey) { 226 // Arrow key does not change the text, but we still want to send 227 // the DOM events. 228 sendDomEvent(event); 229 } 230 */ 231 return true; 232 } 233 // Ignore the key up event for newlines. This prevents 234 // multiple newlines in the native textarea. 235 if (mGotEnterDown && !down) { 236 return true; 237 } 238 // if it is a navigation key, pass it to WebView 239 if (isArrowKey) { 240 // WebView check the trackballtime in onKeyDown to avoid calling 241 // native from both trackball and key handling. As this is called 242 // from WebTextView, we always want WebView to check with native. 243 // Reset trackballtime to ensure it. 244 mWebView.resetTrackballTime(); 245 return down ? mWebView.onKeyDown(keyCode, event) : mWebView 246 .onKeyUp(keyCode, event); 247 } 248 return false; 249 } 250 251 /** 252 * Create a fake touch up event at (x,y) with respect to this WebTextView. 253 * This is used by WebView to act as though a touch event which happened 254 * before we placed the WebTextView actually hit it, so that it can place 255 * the cursor accordingly. 256 */ 257 /* package */ void fakeTouchEvent(float x, float y) { 258 // We need to ensure that there is a Layout, since the Layout is used 259 // in determining where to place the cursor. 260 if (getLayout() == null) { 261 measure(mWidthSpec, mHeightSpec); 262 } 263 // Create a fake touch up, which is used to place the cursor. 264 MotionEvent ev = MotionEvent.obtain(0, 0, MotionEvent.ACTION_UP, 265 x, y, 0); 266 onTouchEvent(ev); 267 ev.recycle(); 268 } 269 270 /** 271 * Determine whether this WebTextView currently represents the node 272 * represented by ptr. 273 * @param ptr Pointer to a node to compare to. 274 * @return boolean Whether this WebTextView already represents the node 275 * pointed to by ptr. 276 */ 277 /* package */ boolean isSameTextField(int ptr) { 278 return ptr == mNodePointer; 279 } 280 281 @Override public InputConnection onCreateInputConnection( 282 EditorInfo outAttrs) { 283 InputConnection connection = super.onCreateInputConnection(outAttrs); 284 if (mWebView != null) { 285 // Use the name of the textfield + the url. Use backslash as an 286 // arbitrary separator. 287 outAttrs.fieldName = mWebView.nativeFocusCandidateName() + "\\" 288 + mWebView.getUrl(); 289 } 290 return connection; 291 } 292 293 @Override 294 protected void onSelectionChanged(int selStart, int selEnd) { 295 if (mWebView != null) { 296 if (DebugFlags.WEB_TEXT_VIEW) { 297 Log.v(LOGTAG, "onSelectionChanged selStart=" + selStart 298 + " selEnd=" + selEnd); 299 } 300 mWebView.setSelection(selStart, selEnd); 301 } 302 } 303 304 @Override 305 protected void onTextChanged(CharSequence s,int start,int before,int count){ 306 super.onTextChanged(s, start, before, count); 307 String postChange = s.toString(); 308 // Prevent calls to setText from invoking onTextChanged (since this will 309 // mean we are on a different textfield). Also prevent the change when 310 // going from a textfield with a string of text to one with a smaller 311 // limit on text length from registering the onTextChanged event. 312 if (mPreChange == null || mPreChange.equals(postChange) || 313 (mMaxLength > -1 && mPreChange.length() > mMaxLength && 314 mPreChange.substring(0, mMaxLength).equals(postChange))) { 315 return; 316 } 317 mPreChange = postChange; 318 // This was simply a delete or a cut, so just delete the selection. 319 if (before > 0 && 0 == count) { 320 mWebView.deleteSelection(start, start + before); 321 // For this and all changes to the text, update our cache 322 updateCachedTextfield(); 323 return; 324 } 325 // Find the last character being replaced. If it can be represented by 326 // events, we will pass them to native (after replacing the beginning 327 // of the changed text), so we can see javascript events. 328 // Otherwise, replace the text being changed (including the last 329 // character) in the textfield. 330 TextUtils.getChars(s, start + count - 1, start + count, mCharacter, 0); 331 KeyCharacterMap kmap = 332 KeyCharacterMap.load(KeyCharacterMap.BUILT_IN_KEYBOARD); 333 KeyEvent[] events = kmap.getEvents(mCharacter); 334 boolean cannotUseKeyEvents = null == events; 335 int charactersFromKeyEvents = cannotUseKeyEvents ? 0 : 1; 336 if (count > 1 || cannotUseKeyEvents) { 337 String replace = s.subSequence(start, 338 start + count - charactersFromKeyEvents).toString(); 339 mWebView.replaceTextfieldText(start, start + before, replace, 340 start + count - charactersFromKeyEvents, 341 start + count - charactersFromKeyEvents); 342 } else { 343 // This corrects the selection which may have been affected by the 344 // trackball or auto-correct. 345 if (DebugFlags.WEB_TEXT_VIEW) { 346 Log.v(LOGTAG, "onTextChanged start=" + start 347 + " start + before=" + (start + before)); 348 } 349 mWebView.setSelection(start, start + before); 350 } 351 if (!cannotUseKeyEvents) { 352 int length = events.length; 353 for (int i = 0; i < length; i++) { 354 // We never send modifier keys to native code so don't send them 355 // here either. 356 if (!KeyEvent.isModifierKey(events[i].getKeyCode())) { 357 sendDomEvent(events[i]); 358 } 359 } 360 } 361 updateCachedTextfield(); 362 } 363 364 @Override 365 public boolean onTrackballEvent(MotionEvent event) { 366 if (isPopupShowing()) { 367 return super.onTrackballEvent(event); 368 } 369 if (event.getAction() != MotionEvent.ACTION_MOVE) { 370 return false; 371 } 372 // If the Cursor is not on the text input, webview should handle the 373 // trackball 374 if (!mWebView.nativeCursorMatchesFocus()) { 375 return mWebView.onTrackballEvent(event); 376 } 377 Spannable text = (Spannable) getText(); 378 MovementMethod move = getMovementMethod(); 379 if (move != null && getLayout() != null && 380 move.onTrackballEvent(this, text, event)) { 381 // Selection is changed in onSelectionChanged 382 return true; 383 } 384 return false; 385 } 386 387 /** 388 * Remove this WebTextView from its host WebView, and return 389 * focus to the host. 390 */ 391 /* package */ void remove() { 392 // hide the soft keyboard when the edit text is out of focus 393 InputMethodManager.getInstance(mContext).hideSoftInputFromWindow( 394 getWindowToken(), 0); 395 mWebView.removeView(this); 396 mWebView.requestFocus(); 397 } 398 399 /* package */ void bringIntoView() { 400 if (getLayout() != null) { 401 bringPointIntoView(Selection.getSelectionEnd(getText())); 402 } 403 } 404 405 /** 406 * Send the DOM events for the specified event. 407 * @param event KeyEvent to be translated into a DOM event. 408 */ 409 private void sendDomEvent(KeyEvent event) { 410 mWebView.passToJavaScript(getText().toString(), event); 411 } 412 413 /** 414 * Always use this instead of setAdapter, as this has features specific to 415 * the WebTextView. 416 */ 417 public void setAdapterCustom(AutoCompleteAdapter adapter) { 418 if (adapter != null) { 419 setInputType(EditorInfo.TYPE_TEXT_FLAG_AUTO_COMPLETE); 420 adapter.setTextView(this); 421 } 422 super.setAdapter(adapter); 423 } 424 425 /** 426 * This is a special version of ArrayAdapter which changes its text size 427 * to match the text size of its host TextView. 428 */ 429 public static class AutoCompleteAdapter extends ArrayAdapter<String> { 430 private TextView mTextView; 431 432 public AutoCompleteAdapter(Context context, ArrayList<String> entries) { 433 super(context, com.android.internal.R.layout 434 .search_dropdown_item_1line, entries); 435 } 436 437 /** 438 * {@inheritDoc} 439 */ 440 public View getView(int position, View convertView, ViewGroup parent) { 441 TextView tv = 442 (TextView) super.getView(position, convertView, parent); 443 if (tv != null && mTextView != null) { 444 tv.setTextSize(mTextView.getTextSize()); 445 } 446 return tv; 447 } 448 449 /** 450 * Set the TextView so we can match its text size. 451 */ 452 private void setTextView(TextView tv) { 453 mTextView = tv; 454 } 455 } 456 457 /** 458 * Determine whether to use the system-wide password disguising method, 459 * or to use none. 460 * @param inPassword True if the textfield is a password field. 461 */ 462 /* package */ void setInPassword(boolean inPassword) { 463 if (inPassword) { 464 setInputType(EditorInfo.TYPE_CLASS_TEXT | EditorInfo. 465 TYPE_TEXT_VARIATION_PASSWORD); 466 createBackground(); 467 } 468 // For password fields, draw the WebTextView. For others, just show 469 // webkit's drawing. 470 setWillNotDraw(!inPassword); 471 setBackgroundDrawable(inPassword ? mBackground : null); 472 // For non-password fields, avoid the invals from TextView's blinking 473 // cursor 474 setCursorVisible(inPassword); 475 } 476 477 /** 478 * Private class used for the background of a password textfield. 479 */ 480 private static class OutlineDrawable extends Drawable { 481 public void draw(Canvas canvas) { 482 Rect bounds = getBounds(); 483 Paint paint = new Paint(); 484 paint.setAntiAlias(true); 485 // Draw the background. 486 paint.setColor(Color.WHITE); 487 canvas.drawRect(bounds, paint); 488 // Draw the outline. 489 paint.setStyle(Paint.Style.STROKE); 490 paint.setColor(Color.BLACK); 491 canvas.drawRect(bounds, paint); 492 } 493 // Always want it to be opaque. 494 public int getOpacity() { 495 return PixelFormat.OPAQUE; 496 } 497 // These are needed because they are abstract in Drawable. 498 public void setAlpha(int alpha) { } 499 public void setColorFilter(ColorFilter cf) { } 500 } 501 502 /** 503 * Create a background for the WebTextView and set up the paint for drawing 504 * the text. This way, we can see the password transformation of the 505 * system, which (optionally) shows the actual text before changing to dots. 506 * The background is necessary to hide the webkit-drawn text beneath. 507 */ 508 private void createBackground() { 509 if (mBackground != null) { 510 return; 511 } 512 mBackground = new OutlineDrawable(); 513 514 setGravity(Gravity.CENTER_VERTICAL); 515 // Turn on subpixel text, and turn off kerning, so it better matches 516 // the text in webkit. 517 TextPaint paint = getPaint(); 518 int flags = paint.getFlags() | Paint.SUBPIXEL_TEXT_FLAG | 519 Paint.ANTI_ALIAS_FLAG & ~Paint.DEV_KERN_TEXT_FLAG; 520 paint.setFlags(flags); 521 // Set the text color to black, regardless of the theme. This ensures 522 // that other applications that use embedded WebViews will properly 523 // display the text in password textfields. 524 setTextColor(Color.BLACK); 525 } 526 527 /* package */ void setMaxLength(int maxLength) { 528 mMaxLength = maxLength; 529 if (-1 == maxLength) { 530 setFilters(NO_FILTERS); 531 } else { 532 setFilters(new InputFilter[] { 533 new InputFilter.LengthFilter(maxLength) }); 534 } 535 } 536 537 /** 538 * Set the pointer for this node so it can be determined which node this 539 * WebTextView represents. 540 * @param ptr Integer representing the pointer to the node which this 541 * WebTextView represents. 542 */ 543 /* package */ void setNodePointer(int ptr) { 544 mNodePointer = ptr; 545 } 546 547 /** 548 * Determine the position and size of WebTextView, and add it to the 549 * WebView's view heirarchy. All parameters are presumed to be in 550 * view coordinates. Also requests Focus and sets the cursor to not 551 * request to be in view. 552 * @param x x-position of the textfield. 553 * @param y y-position of the textfield. 554 * @param width width of the textfield. 555 * @param height height of the textfield. 556 */ 557 /* package */ void setRect(int x, int y, int width, int height) { 558 LayoutParams lp = (LayoutParams) getLayoutParams(); 559 if (null == lp) { 560 lp = new LayoutParams(width, height, x, y); 561 } else { 562 lp.x = x; 563 lp.y = y; 564 lp.width = width; 565 lp.height = height; 566 } 567 if (getParent() == null) { 568 mWebView.addView(this, lp); 569 } else { 570 setLayoutParams(lp); 571 } 572 // Set up a measure spec so a layout can always be recreated. 573 mWidthSpec = MeasureSpec.makeMeasureSpec(width, MeasureSpec.EXACTLY); 574 mHeightSpec = MeasureSpec.makeMeasureSpec(height, MeasureSpec.EXACTLY); 575 requestFocus(); 576 } 577 578 /** 579 * Set whether this is a single-line textfield or a multi-line textarea. 580 * Textfields scroll horizontally, and do not handle the enter key. 581 * Textareas behave oppositely. 582 * Do NOT call this after calling setInPassword(true). This will result in 583 * removing the password input type. 584 */ 585 public void setSingleLine(boolean single) { 586 int inputType = EditorInfo.TYPE_CLASS_TEXT 587 | EditorInfo.TYPE_TEXT_VARIATION_WEB_EDIT_TEXT; 588 if (!single) { 589 inputType |= EditorInfo.TYPE_TEXT_FLAG_MULTI_LINE 590 | EditorInfo.TYPE_TEXT_FLAG_CAP_SENTENCES 591 | EditorInfo.TYPE_TEXT_FLAG_AUTO_CORRECT; 592 } 593 mSingle = single; 594 setHorizontallyScrolling(single); 595 setInputType(inputType); 596 } 597 598 /** 599 * Set the text for this WebTextView, and set the selection to (start, end) 600 * @param text Text to go into this WebTextView. 601 * @param start Beginning of the selection. 602 * @param end End of the selection. 603 */ 604 /* package */ void setText(CharSequence text, int start, int end) { 605 mPreChange = text.toString(); 606 setText(text); 607 Spannable span = (Spannable) getText(); 608 int length = span.length(); 609 if (end > length) { 610 end = length; 611 } 612 if (start < 0) { 613 start = 0; 614 } else if (start > length) { 615 start = length; 616 } 617 if (DebugFlags.WEB_TEXT_VIEW) { 618 Log.v(LOGTAG, "setText start=" + start 619 + " end=" + end); 620 } 621 Selection.setSelection(span, start, end); 622 } 623 624 /** 625 * Set the text to the new string, but use the old selection, making sure 626 * to keep it within the new string. 627 * @param text The new text to place in the textfield. 628 */ 629 /* package */ void setTextAndKeepSelection(String text) { 630 mPreChange = text.toString(); 631 Editable edit = (Editable) getText(); 632 edit.replace(0, edit.length(), text); 633 updateCachedTextfield(); 634 } 635 636 /** 637 * Update the cache to reflect the current text. 638 */ 639 /* package */ void updateCachedTextfield() { 640 mWebView.updateCachedTextfield(getText().toString()); 641 } 642} 643