BaseMovementMethod.java revision 8f34567c71003505456a9b1a0d461a4e62883d70
1/* 2 * Copyright (C) 2010 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.text.method; 18 19import android.text.Layout; 20import android.text.Spannable; 21import android.view.InputDevice; 22import android.view.KeyEvent; 23import android.view.MotionEvent; 24import android.widget.TextView; 25 26/** 27 * Base classes for movement methods. 28 */ 29public class BaseMovementMethod implements MovementMethod { 30 @Override 31 public boolean canSelectArbitrarily() { 32 return false; 33 } 34 35 @Override 36 public void initialize(TextView widget, Spannable text) { 37 } 38 39 @Override 40 public boolean onKeyDown(TextView widget, Spannable text, int keyCode, KeyEvent event) { 41 final int movementMetaState = getMovementMetaState(text, event); 42 boolean handled = handleMovementKey(widget, text, keyCode, movementMetaState, event); 43 if (handled) { 44 MetaKeyKeyListener.adjustMetaAfterKeypress(text); 45 MetaKeyKeyListener.resetLockedMeta(text); 46 } 47 return handled; 48 } 49 50 @Override 51 public boolean onKeyOther(TextView widget, Spannable text, KeyEvent event) { 52 final int movementMetaState = getMovementMetaState(text, event); 53 final int keyCode = event.getKeyCode(); 54 if (keyCode != KeyEvent.KEYCODE_UNKNOWN 55 && event.getAction() == KeyEvent.ACTION_MULTIPLE) { 56 final int repeat = event.getRepeatCount(); 57 boolean handled = false; 58 for (int i = 0; i < repeat; i++) { 59 if (!handleMovementKey(widget, text, keyCode, movementMetaState, event)) { 60 break; 61 } 62 handled = true; 63 } 64 if (handled) { 65 MetaKeyKeyListener.adjustMetaAfterKeypress(text); 66 MetaKeyKeyListener.resetLockedMeta(text); 67 } 68 return handled; 69 } 70 return false; 71 } 72 73 @Override 74 public boolean onKeyUp(TextView widget, Spannable text, int keyCode, KeyEvent event) { 75 return false; 76 } 77 78 @Override 79 public void onTakeFocus(TextView widget, Spannable text, int direction) { 80 } 81 82 @Override 83 public boolean onTouchEvent(TextView widget, Spannable text, MotionEvent event) { 84 return false; 85 } 86 87 @Override 88 public boolean onTrackballEvent(TextView widget, Spannable text, MotionEvent event) { 89 return false; 90 } 91 92 @Override 93 public boolean onGenericMotionEvent(TextView widget, Spannable text, MotionEvent event) { 94 if ((event.getSource() & InputDevice.SOURCE_CLASS_POINTER) != 0) { 95 switch (event.getAction()) { 96 case MotionEvent.ACTION_SCROLL: { 97 final float vscroll; 98 final float hscroll; 99 if ((event.getMetaState() & KeyEvent.META_SHIFT_ON) != 0) { 100 vscroll = 0; 101 hscroll = event.getAxisValue(MotionEvent.AXIS_VSCROLL); 102 } else { 103 vscroll = -event.getAxisValue(MotionEvent.AXIS_VSCROLL); 104 hscroll = event.getAxisValue(MotionEvent.AXIS_HSCROLL); 105 } 106 107 boolean handled = false; 108 if (hscroll < 0) { 109 handled |= scrollLeft(widget, text, (int)Math.ceil(-hscroll)); 110 } else if (hscroll > 0) { 111 handled |= scrollRight(widget, text, (int)Math.ceil(hscroll)); 112 } 113 if (vscroll < 0) { 114 handled |= scrollUp(widget, text, (int)Math.ceil(-vscroll)); 115 } else if (vscroll > 0) { 116 handled |= scrollDown(widget, text, (int)Math.ceil(vscroll)); 117 } 118 return handled; 119 } 120 } 121 } 122 return false; 123 } 124 125 /** 126 * Gets the meta state used for movement using the modifiers tracked by the text 127 * buffer as well as those present in the key event. 128 * 129 * The movement meta state excludes the state of locked modifiers or the SHIFT key 130 * since they are not used by movement actions (but they may be used for selection). 131 * 132 * @param buffer The text buffer. 133 * @param event The key event. 134 * @return The keyboard meta states used for movement. 135 */ 136 protected int getMovementMetaState(Spannable buffer, KeyEvent event) { 137 // We ignore locked modifiers and SHIFT. 138 int metaState = (event.getMetaState() | MetaKeyKeyListener.getMetaState(buffer)) 139 & ~(MetaKeyKeyListener.META_ALT_LOCKED | MetaKeyKeyListener.META_SYM_LOCKED); 140 return KeyEvent.normalizeMetaState(metaState) & ~KeyEvent.META_SHIFT_MASK; 141 } 142 143 /** 144 * Performs a movement key action. 145 * The default implementation decodes the key down and invokes movement actions 146 * such as {@link #down} and {@link #up}. 147 * {@link #onKeyDown(TextView, Spannable, int, KeyEvent)} calls this method once 148 * to handle an {@link KeyEvent#ACTION_DOWN}. 149 * {@link #onKeyOther(TextView, Spannable, KeyEvent)} calls this method repeatedly 150 * to handle each repetition of an {@link KeyEvent#ACTION_MULTIPLE}. 151 * 152 * @param widget The text view. 153 * @param buffer The text buffer. 154 * @param event The key event. 155 * @param keyCode The key code. 156 * @param movementMetaState The keyboard meta states used for movement. 157 * @param event The key event. 158 * @return True if the event was handled. 159 */ 160 protected boolean handleMovementKey(TextView widget, Spannable buffer, 161 int keyCode, int movementMetaState, KeyEvent event) { 162 switch (keyCode) { 163 case KeyEvent.KEYCODE_DPAD_LEFT: 164 if (KeyEvent.metaStateHasNoModifiers(movementMetaState)) { 165 return left(widget, buffer); 166 } else if (KeyEvent.metaStateHasModifiers(movementMetaState, 167 KeyEvent.META_ALT_ON)) { 168 return lineStart(widget, buffer); 169 } 170 break; 171 172 case KeyEvent.KEYCODE_DPAD_RIGHT: 173 if (KeyEvent.metaStateHasNoModifiers(movementMetaState)) { 174 return right(widget, buffer); 175 } else if (KeyEvent.metaStateHasModifiers(movementMetaState, 176 KeyEvent.META_ALT_ON)) { 177 return lineEnd(widget, buffer); 178 } 179 break; 180 181 case KeyEvent.KEYCODE_DPAD_UP: 182 if (KeyEvent.metaStateHasNoModifiers(movementMetaState)) { 183 return up(widget, buffer); 184 } else if (KeyEvent.metaStateHasModifiers(movementMetaState, 185 KeyEvent.META_ALT_ON)) { 186 return top(widget, buffer); 187 } 188 break; 189 190 case KeyEvent.KEYCODE_DPAD_DOWN: 191 if (KeyEvent.metaStateHasNoModifiers(movementMetaState)) { 192 return down(widget, buffer); 193 } else if (KeyEvent.metaStateHasModifiers(movementMetaState, 194 KeyEvent.META_ALT_ON)) { 195 return bottom(widget, buffer); 196 } 197 break; 198 199 case KeyEvent.KEYCODE_PAGE_UP: 200 if (KeyEvent.metaStateHasNoModifiers(movementMetaState)) { 201 return pageUp(widget, buffer); 202 } else if (KeyEvent.metaStateHasModifiers(movementMetaState, 203 KeyEvent.META_ALT_ON)) { 204 return top(widget, buffer); 205 } 206 break; 207 208 case KeyEvent.KEYCODE_PAGE_DOWN: 209 if (KeyEvent.metaStateHasNoModifiers(movementMetaState)) { 210 return pageDown(widget, buffer); 211 } else if (KeyEvent.metaStateHasModifiers(movementMetaState, 212 KeyEvent.META_ALT_ON)) { 213 return bottom(widget, buffer); 214 } 215 break; 216 217 case KeyEvent.KEYCODE_MOVE_HOME: 218 if (KeyEvent.metaStateHasNoModifiers(movementMetaState)) { 219 return home(widget, buffer); 220 } 221 break; 222 223 case KeyEvent.KEYCODE_MOVE_END: 224 if (KeyEvent.metaStateHasNoModifiers(movementMetaState)) { 225 return end(widget, buffer); 226 } 227 break; 228 } 229 return false; 230 } 231 232 /** 233 * Performs a left movement action. 234 * Moves the cursor or scrolls left by one character. 235 * 236 * @param widget The text view. 237 * @param buffer The text buffer. 238 * @return True if the event was handled. 239 */ 240 protected boolean left(TextView widget, Spannable buffer) { 241 return false; 242 } 243 244 /** 245 * Performs a right movement action. 246 * Moves the cursor or scrolls right by one character. 247 * 248 * @param widget The text view. 249 * @param buffer The text buffer. 250 * @return True if the event was handled. 251 */ 252 protected boolean right(TextView widget, Spannable buffer) { 253 return false; 254 } 255 256 /** 257 * Performs an up movement action. 258 * Moves the cursor or scrolls up by one line. 259 * 260 * @param widget The text view. 261 * @param buffer The text buffer. 262 * @return True if the event was handled. 263 */ 264 protected boolean up(TextView widget, Spannable buffer) { 265 return false; 266 } 267 268 /** 269 * Performs a down movement action. 270 * Moves the cursor or scrolls down by one line. 271 * 272 * @param widget The text view. 273 * @param buffer The text buffer. 274 * @return True if the event was handled. 275 */ 276 protected boolean down(TextView widget, Spannable buffer) { 277 return false; 278 } 279 280 /** 281 * Performs a page-up movement action. 282 * Moves the cursor or scrolls up by one page. 283 * 284 * @param widget The text view. 285 * @param buffer The text buffer. 286 * @return True if the event was handled. 287 */ 288 protected boolean pageUp(TextView widget, Spannable buffer) { 289 return false; 290 } 291 292 /** 293 * Performs a page-down movement action. 294 * Moves the cursor or scrolls down by one page. 295 * 296 * @param widget The text view. 297 * @param buffer The text buffer. 298 * @return True if the event was handled. 299 */ 300 protected boolean pageDown(TextView widget, Spannable buffer) { 301 return false; 302 } 303 304 /** 305 * Performs a top movement action. 306 * Moves the cursor or scrolls to the top of the buffer. 307 * 308 * @param widget The text view. 309 * @param buffer The text buffer. 310 * @return True if the event was handled. 311 */ 312 protected boolean top(TextView widget, Spannable buffer) { 313 return false; 314 } 315 316 /** 317 * Performs a bottom movement action. 318 * Moves the cursor or scrolls to the bottom of the buffer. 319 * 320 * @param widget The text view. 321 * @param buffer The text buffer. 322 * @return True if the event was handled. 323 */ 324 protected boolean bottom(TextView widget, Spannable buffer) { 325 return false; 326 } 327 328 /** 329 * Performs a line-start movement action. 330 * Moves the cursor or scrolls to the start of the line. 331 * 332 * @param widget The text view. 333 * @param buffer The text buffer. 334 * @return True if the event was handled. 335 */ 336 protected boolean lineStart(TextView widget, Spannable buffer) { 337 return false; 338 } 339 340 /** 341 * Performs an line-end movement action. 342 * Moves the cursor or scrolls to the end of the line. 343 * 344 * @param widget The text view. 345 * @param buffer The text buffer. 346 * @return True if the event was handled. 347 */ 348 protected boolean lineEnd(TextView widget, Spannable buffer) { 349 return false; 350 } 351 352 /** 353 * Performs a home movement action. 354 * Moves the cursor or scrolls to the start of the line or to the top of the 355 * document depending on whether the insertion point is being moved or 356 * the document is being scrolled. 357 * 358 * @param widget The text view. 359 * @param buffer The text buffer. 360 * @return True if the event was handled. 361 */ 362 protected boolean home(TextView widget, Spannable buffer) { 363 return false; 364 } 365 366 /** 367 * Performs an end movement action. 368 * Moves the cursor or scrolls to the start of the line or to the top of the 369 * document depending on whether the insertion point is being moved or 370 * the document is being scrolled. 371 * 372 * @param widget The text view. 373 * @param buffer The text buffer. 374 * @return True if the event was handled. 375 */ 376 protected boolean end(TextView widget, Spannable buffer) { 377 return false; 378 } 379 380 private int getTopLine(TextView widget) { 381 return widget.getLayout().getLineForVertical(widget.getScrollY()); 382 } 383 384 private int getBottomLine(TextView widget) { 385 return widget.getLayout().getLineForVertical(widget.getScrollY() + getInnerHeight(widget)); 386 } 387 388 private int getInnerWidth(TextView widget) { 389 return widget.getWidth() - widget.getTotalPaddingLeft() - widget.getTotalPaddingRight(); 390 } 391 392 private int getInnerHeight(TextView widget) { 393 return widget.getHeight() - widget.getTotalPaddingTop() - widget.getTotalPaddingBottom(); 394 } 395 396 private int getCharacterWidth(TextView widget) { 397 return (int) Math.ceil(widget.getPaint().getFontSpacing()); 398 } 399 400 private int getScrollBoundsLeft(TextView widget) { 401 final Layout layout = widget.getLayout(); 402 final int topLine = getTopLine(widget); 403 final int bottomLine = getBottomLine(widget); 404 if (topLine > bottomLine) { 405 return 0; 406 } 407 int left = Integer.MAX_VALUE; 408 for (int line = topLine; line <= bottomLine; line++) { 409 final int lineLeft = (int) Math.floor(layout.getLineLeft(line)); 410 if (lineLeft < left) { 411 left = lineLeft; 412 } 413 } 414 return left; 415 } 416 417 private int getScrollBoundsRight(TextView widget) { 418 final Layout layout = widget.getLayout(); 419 final int topLine = getTopLine(widget); 420 final int bottomLine = getBottomLine(widget); 421 if (topLine > bottomLine) { 422 return 0; 423 } 424 int right = Integer.MIN_VALUE; 425 for (int line = topLine; line <= bottomLine; line++) { 426 final int lineRight = (int) Math.ceil(layout.getLineRight(line)); 427 if (lineRight > right) { 428 right = lineRight; 429 } 430 } 431 return right; 432 } 433 434 /** 435 * Performs a scroll left action. 436 * Scrolls left by the specified number of characters. 437 * 438 * @param widget The text view. 439 * @param buffer The text buffer. 440 * @param amount The number of characters to scroll by. Must be at least 1. 441 * @return True if the event was handled. 442 * @hide 443 */ 444 protected boolean scrollLeft(TextView widget, Spannable buffer, int amount) { 445 final int minScrollX = getScrollBoundsLeft(widget); 446 int scrollX = widget.getScrollX(); 447 if (scrollX > minScrollX) { 448 scrollX = Math.max(scrollX - getCharacterWidth(widget) * amount, minScrollX); 449 widget.scrollTo(scrollX, widget.getScrollY()); 450 return true; 451 } 452 return false; 453 } 454 455 /** 456 * Performs a scroll right action. 457 * Scrolls right by the specified number of characters. 458 * 459 * @param widget The text view. 460 * @param buffer The text buffer. 461 * @param amount The number of characters to scroll by. Must be at least 1. 462 * @return True if the event was handled. 463 * @hide 464 */ 465 protected boolean scrollRight(TextView widget, Spannable buffer, int amount) { 466 final int maxScrollX = getScrollBoundsRight(widget) - getInnerWidth(widget); 467 int scrollX = widget.getScrollX(); 468 if (scrollX < maxScrollX) { 469 scrollX = Math.min(scrollX + getCharacterWidth(widget) * amount, maxScrollX); 470 widget.scrollTo(scrollX, widget.getScrollY()); 471 return true; 472 } 473 return false; 474 } 475 476 /** 477 * Performs a scroll up action. 478 * Scrolls up by the specified number of lines. 479 * 480 * @param widget The text view. 481 * @param buffer The text buffer. 482 * @param amount The number of lines to scroll by. Must be at least 1. 483 * @return True if the event was handled. 484 * @hide 485 */ 486 protected boolean scrollUp(TextView widget, Spannable buffer, int amount) { 487 final Layout layout = widget.getLayout(); 488 final int top = widget.getScrollY(); 489 int topLine = layout.getLineForVertical(top); 490 if (layout.getLineTop(topLine) == top) { 491 // If the top line is partially visible, bring it all the way 492 // into view; otherwise, bring the previous line into view. 493 topLine -= 1; 494 } 495 if (topLine >= 0) { 496 topLine = Math.max(topLine - amount + 1, 0); 497 Touch.scrollTo(widget, layout, widget.getScrollX(), layout.getLineTop(topLine)); 498 return true; 499 } 500 return false; 501 } 502 503 /** 504 * Performs a scroll down action. 505 * Scrolls down by the specified number of lines. 506 * 507 * @param widget The text view. 508 * @param buffer The text buffer. 509 * @param amount The number of lines to scroll by. Must be at least 1. 510 * @return True if the event was handled. 511 * @hide 512 */ 513 protected boolean scrollDown(TextView widget, Spannable buffer, int amount) { 514 final Layout layout = widget.getLayout(); 515 final int innerHeight = getInnerHeight(widget); 516 final int bottom = widget.getScrollY() + innerHeight; 517 int bottomLine = layout.getLineForVertical(bottom); 518 if (layout.getLineTop(bottomLine + 1) < bottom + 1) { 519 // Less than a pixel of this line is out of view, 520 // so we must have tried to make it entirely in view 521 // and now want the next line to be in view instead. 522 bottomLine += 1; 523 } 524 final int limit = layout.getLineCount() - 1; 525 if (bottomLine <= limit) { 526 bottomLine = Math.min(bottomLine + amount - 1, limit); 527 Touch.scrollTo(widget, layout, widget.getScrollX(), 528 layout.getLineTop(bottomLine + 1) - innerHeight); 529 return true; 530 } 531 return false; 532 } 533 534 /** 535 * Performs a scroll page up action. 536 * Scrolls up by one page. 537 * 538 * @param widget The text view. 539 * @param buffer The text buffer. 540 * @return True if the event was handled. 541 * @hide 542 */ 543 protected boolean scrollPageUp(TextView widget, Spannable buffer) { 544 final Layout layout = widget.getLayout(); 545 final int top = widget.getScrollY() - getInnerHeight(widget); 546 int topLine = layout.getLineForVertical(top); 547 if (topLine >= 0) { 548 Touch.scrollTo(widget, layout, widget.getScrollX(), layout.getLineTop(topLine)); 549 return true; 550 } 551 return false; 552 } 553 554 /** 555 * Performs a scroll page up action. 556 * Scrolls down by one page. 557 * 558 * @param widget The text view. 559 * @param buffer The text buffer. 560 * @return True if the event was handled. 561 * @hide 562 */ 563 protected boolean scrollPageDown(TextView widget, Spannable buffer) { 564 final Layout layout = widget.getLayout(); 565 final int innerHeight = getInnerHeight(widget); 566 final int bottom = widget.getScrollY() + innerHeight + innerHeight; 567 int bottomLine = layout.getLineForVertical(bottom); 568 if (bottomLine <= layout.getLineCount() - 1) { 569 Touch.scrollTo(widget, layout, widget.getScrollX(), 570 layout.getLineTop(bottomLine + 1) - innerHeight); 571 return true; 572 } 573 return false; 574 } 575 576 /** 577 * Performs a scroll to top action. 578 * Scrolls to the top of the document. 579 * 580 * @param widget The text view. 581 * @param buffer The text buffer. 582 * @return True if the event was handled. 583 * @hide 584 */ 585 protected boolean scrollTop(TextView widget, Spannable buffer) { 586 final Layout layout = widget.getLayout(); 587 if (getTopLine(widget) >= 0) { 588 Touch.scrollTo(widget, layout, widget.getScrollX(), layout.getLineTop(0)); 589 return true; 590 } 591 return false; 592 } 593 594 /** 595 * Performs a scroll to bottom action. 596 * Scrolls to the bottom of the document. 597 * 598 * @param widget The text view. 599 * @param buffer The text buffer. 600 * @return True if the event was handled. 601 * @hide 602 */ 603 protected boolean scrollBottom(TextView widget, Spannable buffer) { 604 final Layout layout = widget.getLayout(); 605 final int lineCount = layout.getLineCount(); 606 if (getBottomLine(widget) <= lineCount - 1) { 607 Touch.scrollTo(widget, layout, widget.getScrollX(), 608 layout.getLineTop(lineCount) - getInnerHeight(widget)); 609 return true; 610 } 611 return false; 612 } 613 614 /** 615 * Performs a scroll to line start action. 616 * Scrolls to the start of the line. 617 * 618 * @param widget The text view. 619 * @param buffer The text buffer. 620 * @return True if the event was handled. 621 * @hide 622 */ 623 protected boolean scrollLineStart(TextView widget, Spannable buffer) { 624 final int minScrollX = getScrollBoundsLeft(widget); 625 int scrollX = widget.getScrollX(); 626 if (scrollX > minScrollX) { 627 widget.scrollTo(minScrollX, widget.getScrollY()); 628 return true; 629 } 630 return false; 631 } 632 633 /** 634 * Performs a scroll to line end action. 635 * Scrolls to the end of the line. 636 * 637 * @param widget The text view. 638 * @param buffer The text buffer. 639 * @return True if the event was handled. 640 * @hide 641 */ 642 protected boolean scrollLineEnd(TextView widget, Spannable buffer) { 643 final int maxScrollX = getScrollBoundsRight(widget) - getInnerWidth(widget); 644 int scrollX = widget.getScrollX(); 645 if (scrollX < maxScrollX) { 646 widget.scrollTo(maxScrollX, widget.getScrollY()); 647 return true; 648 } 649 return false; 650 } 651} 652