ArrowKeyMovementMethod.java revision 8cdb684163051c12f37e8a5f9031f17efd9d0fa4
1/* 2 * Copyright (C) 2006 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.util.Log; 20import android.view.KeyEvent; 21import android.graphics.Rect; 22import android.text.*; 23import android.widget.TextView; 24import android.view.View; 25import android.view.ViewConfiguration; 26import android.view.MotionEvent; 27 28// XXX this doesn't extend MetaKeyKeyListener because the signatures 29// don't match. Need to figure that out. Meanwhile the meta keys 30// won't work in fields that don't take input. 31 32public class 33ArrowKeyMovementMethod 34implements MovementMethod 35{ 36 private boolean up(TextView widget, Spannable buffer) { 37 boolean cap = (MetaKeyKeyListener.getMetaState(buffer, 38 KeyEvent.META_SHIFT_ON) == 1) || 39 (MetaKeyKeyListener.getMetaState(buffer, 40 MetaKeyKeyListener.META_SELECTING) != 0); 41 boolean alt = MetaKeyKeyListener.getMetaState(buffer, 42 KeyEvent.META_ALT_ON) == 1; 43 Layout layout = widget.getLayout(); 44 45 if (cap) { 46 if (alt) { 47 Selection.extendSelection(buffer, 0); 48 return true; 49 } else { 50 return Selection.extendUp(buffer, layout); 51 } 52 } else { 53 if (alt) { 54 Selection.setSelection(buffer, 0); 55 return true; 56 } else { 57 return Selection.moveUp(buffer, layout); 58 } 59 } 60 } 61 62 private boolean down(TextView widget, Spannable buffer) { 63 boolean cap = (MetaKeyKeyListener.getMetaState(buffer, 64 KeyEvent.META_SHIFT_ON) == 1) || 65 (MetaKeyKeyListener.getMetaState(buffer, 66 MetaKeyKeyListener.META_SELECTING) != 0); 67 boolean alt = MetaKeyKeyListener.getMetaState(buffer, 68 KeyEvent.META_ALT_ON) == 1; 69 Layout layout = widget.getLayout(); 70 71 if (cap) { 72 if (alt) { 73 Selection.extendSelection(buffer, buffer.length()); 74 return true; 75 } else { 76 return Selection.extendDown(buffer, layout); 77 } 78 } else { 79 if (alt) { 80 Selection.setSelection(buffer, buffer.length()); 81 return true; 82 } else { 83 return Selection.moveDown(buffer, layout); 84 } 85 } 86 } 87 88 private boolean left(TextView widget, Spannable buffer) { 89 boolean cap = (MetaKeyKeyListener.getMetaState(buffer, 90 KeyEvent.META_SHIFT_ON) == 1) || 91 (MetaKeyKeyListener.getMetaState(buffer, 92 MetaKeyKeyListener.META_SELECTING) != 0); 93 boolean alt = MetaKeyKeyListener.getMetaState(buffer, 94 KeyEvent.META_ALT_ON) == 1; 95 Layout layout = widget.getLayout(); 96 97 if (cap) { 98 if (alt) { 99 return Selection.extendToLeftEdge(buffer, layout); 100 } else { 101 return Selection.extendLeft(buffer, layout); 102 } 103 } else { 104 if (alt) { 105 return Selection.moveToLeftEdge(buffer, layout); 106 } else { 107 return Selection.moveLeft(buffer, layout); 108 } 109 } 110 } 111 112 private boolean right(TextView widget, Spannable buffer) { 113 boolean cap = (MetaKeyKeyListener.getMetaState(buffer, 114 KeyEvent.META_SHIFT_ON) == 1) || 115 (MetaKeyKeyListener.getMetaState(buffer, 116 MetaKeyKeyListener.META_SELECTING) != 0); 117 boolean alt = MetaKeyKeyListener.getMetaState(buffer, 118 KeyEvent.META_ALT_ON) == 1; 119 Layout layout = widget.getLayout(); 120 121 if (cap) { 122 if (alt) { 123 return Selection.extendToRightEdge(buffer, layout); 124 } else { 125 return Selection.extendRight(buffer, layout); 126 } 127 } else { 128 if (alt) { 129 return Selection.moveToRightEdge(buffer, layout); 130 } else { 131 return Selection.moveRight(buffer, layout); 132 } 133 } 134 } 135 136 private int getOffset(int x, int y, TextView widget){ 137 // Converts the absolute X,Y coordinates to the character offset for the 138 // character whose position is closest to the specified 139 // horizontal position. 140 x -= widget.getTotalPaddingLeft(); 141 y -= widget.getTotalPaddingTop(); 142 143 // Clamp the position to inside of the view. 144 if (x < 0) { 145 x = 0; 146 } else if (x >= (widget.getWidth()-widget.getTotalPaddingRight())) { 147 x = widget.getWidth()-widget.getTotalPaddingRight() - 1; 148 } 149 if (y < 0) { 150 y = 0; 151 } else if (y >= (widget.getHeight()-widget.getTotalPaddingBottom())) { 152 y = widget.getHeight()-widget.getTotalPaddingBottom() - 1; 153 } 154 155 x += widget.getScrollX(); 156 y += widget.getScrollY(); 157 158 Layout layout = widget.getLayout(); 159 int line = layout.getLineForVertical(y); 160 161 int offset = layout.getOffsetForHorizontal(line, x); 162 return offset; 163 } 164 165 public boolean onKeyDown(TextView widget, Spannable buffer, int keyCode, KeyEvent event) { 166 if (executeDown(widget, buffer, keyCode)) { 167 MetaKeyKeyListener.adjustMetaAfterKeypress(buffer); 168 MetaKeyKeyListener.resetLockedMeta(buffer); 169 return true; 170 } 171 172 return false; 173 } 174 175 private boolean executeDown(TextView widget, Spannable buffer, int keyCode) { 176 boolean handled = false; 177 178 switch (keyCode) { 179 case KeyEvent.KEYCODE_DPAD_UP: 180 handled |= up(widget, buffer); 181 break; 182 183 case KeyEvent.KEYCODE_DPAD_DOWN: 184 handled |= down(widget, buffer); 185 break; 186 187 case KeyEvent.KEYCODE_DPAD_LEFT: 188 handled |= left(widget, buffer); 189 break; 190 191 case KeyEvent.KEYCODE_DPAD_RIGHT: 192 handled |= right(widget, buffer); 193 break; 194 195 case KeyEvent.KEYCODE_DPAD_CENTER: 196 if (MetaKeyKeyListener.getMetaState(buffer, MetaKeyKeyListener.META_SELECTING) != 0) { 197 if (widget.showContextMenu()) { 198 handled = true; 199 } 200 } 201 } 202 203 if (handled) { 204 MetaKeyKeyListener.adjustMetaAfterKeypress(buffer); 205 MetaKeyKeyListener.resetLockedMeta(buffer); 206 } 207 208 return handled; 209 } 210 211 public boolean onKeyUp(TextView widget, Spannable buffer, int keyCode, KeyEvent event) { 212 return false; 213 } 214 215 public boolean onKeyOther(TextView view, Spannable text, KeyEvent event) { 216 int code = event.getKeyCode(); 217 if (code != KeyEvent.KEYCODE_UNKNOWN 218 && event.getAction() == KeyEvent.ACTION_MULTIPLE) { 219 int repeat = event.getRepeatCount(); 220 boolean handled = false; 221 while ((--repeat) > 0) { 222 handled |= executeDown(view, text, code); 223 } 224 return handled; 225 } 226 return false; 227 } 228 229 public boolean onTrackballEvent(TextView widget, Spannable text, 230 MotionEvent event) { 231 return false; 232 } 233 234 public boolean onTouchEvent(TextView widget, Spannable buffer, 235 MotionEvent event) { 236 int initialScrollX = -1, initialScrollY = -1; 237 if (event.getAction() == MotionEvent.ACTION_UP) { 238 initialScrollX = Touch.getInitialScrollX(widget, buffer); 239 initialScrollY = Touch.getInitialScrollY(widget, buffer); 240 } 241 242 boolean handled = Touch.onTouchEvent(widget, buffer, event); 243 244 if (widget.isFocused() && !widget.didTouchFocusSelect()) { 245 if (event.getAction() == MotionEvent.ACTION_DOWN) { 246 boolean cap = (MetaKeyKeyListener.getMetaState(buffer, 247 KeyEvent.META_SHIFT_ON) == 1) || 248 (MetaKeyKeyListener.getMetaState(buffer, 249 MetaKeyKeyListener.META_SELECTING) != 0); 250 int x = (int) event.getX(); 251 int y = (int) event.getY(); 252 int offset = getOffset(x, y, widget); 253 254 if (cap) { 255 buffer.setSpan(LAST_TAP_DOWN, offset, offset, 256 Spannable.SPAN_POINT_POINT); 257 258 // Disallow intercepting of the touch events, so that 259 // users can scroll and select at the same time. 260 // without this, users would get booted out of select 261 // mode once the view detected it needed to scroll. 262 widget.getParent().requestDisallowInterceptTouchEvent(true); 263 } else { 264 OnePointFiveTapState[] tap = buffer.getSpans(0, buffer.length(), 265 OnePointFiveTapState.class); 266 267 if (tap.length > 0) { 268 if (event.getEventTime() - tap[0].mWhen <= 269 ViewConfiguration.getDoubleTapTimeout() && 270 sameWord(buffer, offset, Selection.getSelectionEnd(buffer))) { 271 272 tap[0].active = true; 273 MetaKeyKeyListener.startSelecting(widget, buffer); 274 widget.getParent().requestDisallowInterceptTouchEvent(true); 275 buffer.setSpan(LAST_TAP_DOWN, offset, offset, 276 Spannable.SPAN_POINT_POINT); 277 } 278 279 tap[0].mWhen = event.getEventTime(); 280 } else { 281 OnePointFiveTapState newtap = new OnePointFiveTapState(); 282 newtap.mWhen = event.getEventTime(); 283 newtap.active = false; 284 buffer.setSpan(newtap, 0, buffer.length(), 285 Spannable.SPAN_INCLUSIVE_INCLUSIVE); 286 } 287 } 288 } else if (event.getAction() == MotionEvent.ACTION_MOVE) { 289 boolean cap = (MetaKeyKeyListener.getMetaState(buffer, 290 KeyEvent.META_SHIFT_ON) == 1) || 291 (MetaKeyKeyListener.getMetaState(buffer, 292 MetaKeyKeyListener.META_SELECTING) != 0); 293 294 if (cap && handled) { 295 // Before selecting, make sure we've moved out of the "slop". 296 // handled will be true, if we're in select mode AND we're 297 // OUT of the slop 298 299 // Turn long press off while we're selecting. User needs to 300 // re-tap on the selection to enable longpress 301 widget.cancelLongPress(); 302 303 // Update selection as we're moving the selection area. 304 305 // Get the current touch position 306 int x = (int) event.getX(); 307 int y = (int) event.getY(); 308 int offset = getOffset(x, y, widget); 309 310 final OnePointFiveTapState[] tap = buffer.getSpans(0, buffer.length(), 311 OnePointFiveTapState.class); 312 313 if (tap.length > 0 && tap[0].active) { 314 // Get the last down touch position (the position at which the 315 // user started the selection) 316 int lastDownOffset = buffer.getSpanStart(LAST_TAP_DOWN); 317 318 // Compute the selection boundaries 319 int spanstart; 320 int spanend; 321 if (offset >= lastDownOffset) { 322 // Expand from word start of the original tap to new word 323 // end, since we are selecting "forwards" 324 spanstart = findWordStart(buffer, lastDownOffset); 325 spanend = findWordEnd(buffer, offset); 326 } else { 327 // Expand to from new word start to word end of the original 328 // tap since we are selecting "backwards". 329 // The spanend will always need to be associated with the touch 330 // up position, so that refining the selection with the 331 // trackball will work as expected. 332 spanstart = findWordEnd(buffer, lastDownOffset); 333 spanend = findWordStart(buffer, offset); 334 } 335 Selection.setSelection(buffer, spanstart, spanend); 336 } else { 337 Selection.extendSelection(buffer, offset); 338 } 339 return true; 340 } 341 } else if (event.getAction() == MotionEvent.ACTION_UP) { 342 // If we have scrolled, then the up shouldn't move the cursor, 343 // but we do need to make sure the cursor is still visible at 344 // the current scroll offset to avoid the scroll jumping later 345 // to show it. 346 if ((initialScrollY >= 0 && initialScrollY != widget.getScrollY()) || 347 (initialScrollX >= 0 && initialScrollX != widget.getScrollX())) { 348 widget.moveCursorToVisibleOffset(); 349 return true; 350 } 351 352 int x = (int) event.getX(); 353 int y = (int) event.getY(); 354 int off = getOffset(x, y, widget); 355 356 // XXX should do the same adjust for x as we do for the line. 357 358 OnePointFiveTapState[] onepointfivetap = buffer.getSpans(0, buffer.length(), 359 OnePointFiveTapState.class); 360 if (onepointfivetap.length > 0 && onepointfivetap[0].active && 361 Selection.getSelectionStart(buffer) == Selection.getSelectionEnd(buffer)) { 362 // If we've set select mode, because there was a onepointfivetap, 363 // but there was no ensuing swipe gesture, undo the select mode 364 // and remove reference to the last onepointfivetap. 365 MetaKeyKeyListener.stopSelecting(widget, buffer); 366 for (int i=0; i < onepointfivetap.length; i++) { 367 buffer.removeSpan(onepointfivetap[i]); 368 } 369 buffer.removeSpan(LAST_TAP_DOWN); 370 } 371 boolean cap = (MetaKeyKeyListener.getMetaState(buffer, 372 KeyEvent.META_SHIFT_ON) == 1) || 373 (MetaKeyKeyListener.getMetaState(buffer, 374 MetaKeyKeyListener.META_SELECTING) != 0); 375 376 DoubleTapState[] tap = buffer.getSpans(0, buffer.length(), 377 DoubleTapState.class); 378 boolean doubletap = false; 379 380 if (tap.length > 0) { 381 if (event.getEventTime() - tap[0].mWhen <= 382 ViewConfiguration.getDoubleTapTimeout() && 383 sameWord(buffer, off, Selection.getSelectionEnd(buffer))) { 384 385 doubletap = true; 386 } 387 388 tap[0].mWhen = event.getEventTime(); 389 } else { 390 DoubleTapState newtap = new DoubleTapState(); 391 newtap.mWhen = event.getEventTime(); 392 buffer.setSpan(newtap, 0, buffer.length(), 393 Spannable.SPAN_INCLUSIVE_INCLUSIVE); 394 } 395 396 if (cap) { 397 buffer.removeSpan(LAST_TAP_DOWN); 398 if (onepointfivetap.length > 0 && onepointfivetap[0].active) { 399 // If we selecting something with the onepointfivetap-and 400 // swipe gesture, stop it on finger up. 401 MetaKeyKeyListener.stopSelecting(widget, buffer); 402 } else { 403 Selection.extendSelection(buffer, off); 404 } 405 } else if (doubletap) { 406 Selection.setSelection(buffer, 407 findWordStart(buffer, off), 408 findWordEnd(buffer, off)); 409 } else { 410 Selection.setSelection(buffer, off); 411 } 412 413 MetaKeyKeyListener.adjustMetaAfterKeypress(buffer); 414 MetaKeyKeyListener.resetLockedMeta(buffer); 415 416 return true; 417 } 418 } 419 420 return handled; 421 } 422 423 private static class DoubleTapState implements NoCopySpan { 424 long mWhen; 425 } 426 427 /* We check for a onepointfive tap. This is similar to 428 * doubletap gesture (where a finger goes down, up, down, up, in a short 429 * time period), except in the onepointfive tap, a users finger only needs 430 * to go down, up, down in a short time period. We detect this type of tap 431 * to implement the onepointfivetap-and-swipe selection gesture. 432 * This gesture allows users to select a segment of text without going 433 * through the "select text" option in the context menu. 434 */ 435 private static class OnePointFiveTapState implements NoCopySpan { 436 long mWhen; 437 boolean active; 438 } 439 440 private static boolean sameWord(CharSequence text, int one, int two) { 441 int start = findWordStart(text, one); 442 int end = findWordEnd(text, one); 443 444 if (end == start) { 445 return false; 446 } 447 448 return start == findWordStart(text, two) && 449 end == findWordEnd(text, two); 450 } 451 452 // TODO: Unify with TextView.getWordForDictionary() 453 private static int findWordStart(CharSequence text, int start) { 454 for (; start > 0; start--) { 455 char c = text.charAt(start - 1); 456 int type = Character.getType(c); 457 458 if (c != '\'' && 459 type != Character.UPPERCASE_LETTER && 460 type != Character.LOWERCASE_LETTER && 461 type != Character.TITLECASE_LETTER && 462 type != Character.MODIFIER_LETTER && 463 type != Character.DECIMAL_DIGIT_NUMBER) { 464 break; 465 } 466 } 467 468 return start; 469 } 470 471 // TODO: Unify with TextView.getWordForDictionary() 472 private static int findWordEnd(CharSequence text, int end) { 473 int len = text.length(); 474 475 for (; end < len; end++) { 476 char c = text.charAt(end); 477 int type = Character.getType(c); 478 479 if (c != '\'' && 480 type != Character.UPPERCASE_LETTER && 481 type != Character.LOWERCASE_LETTER && 482 type != Character.TITLECASE_LETTER && 483 type != Character.MODIFIER_LETTER && 484 type != Character.DECIMAL_DIGIT_NUMBER) { 485 break; 486 } 487 } 488 489 return end; 490 } 491 492 public boolean canSelectArbitrarily() { 493 return true; 494 } 495 496 public void initialize(TextView widget, Spannable text) { 497 Selection.setSelection(text, 0); 498 } 499 500 public void onTakeFocus(TextView view, Spannable text, int dir) { 501 if ((dir & (View.FOCUS_FORWARD | View.FOCUS_DOWN)) != 0) { 502 Layout layout = view.getLayout(); 503 504 if (layout == null) { 505 /* 506 * This shouldn't be null, but do something sensible if it is. 507 */ 508 Selection.setSelection(text, text.length()); 509 } else { 510 /* 511 * Put the cursor at the end of the first line, which is 512 * either the last offset if there is only one line, or the 513 * offset before the first character of the second line 514 * if there is more than one line. 515 */ 516 if (layout.getLineCount() == 1) { 517 Selection.setSelection(text, text.length()); 518 } else { 519 Selection.setSelection(text, layout.getLineStart(1) - 1); 520 } 521 } 522 } else { 523 Selection.setSelection(text, text.length()); 524 } 525 } 526 527 public static MovementMethod getInstance() { 528 if (sInstance == null) 529 sInstance = new ArrowKeyMovementMethod(); 530 531 return sInstance; 532 } 533 534 535 private static final Object LAST_TAP_DOWN = new Object(); 536 private static ArrowKeyMovementMethod sInstance; 537} 538