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