ArrowKeyMovementMethod.java revision 62c4ad3b6ba162540c3fb57fcacb375ccfa53454
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 256 buffer.setSpan(LAST_TAP_DOWN, offset, offset, 257 Spannable.SPAN_POINT_POINT); 258 259 // Disallow intercepting of the touch events, so that 260 // users can scroll and select at the same time. 261 // without this, users would get booted out of select 262 // mode once the view detected it needed to scroll. 263 widget.getParent().requestDisallowInterceptTouchEvent(true); 264 } else { 265 OnePointFiveTapState[] tap = buffer.getSpans(0, buffer.length(), 266 OnePointFiveTapState.class); 267 268 if (tap.length > 0) { 269 if (event.getEventTime() - tap[0].mWhen <= 270 ViewConfiguration.getDoubleTapTimeout() && 271 sameWord(buffer, offset, Selection.getSelectionEnd(buffer))) { 272 273 tap[0].active = true; 274 MetaKeyKeyListener.startSelecting(widget, buffer); 275 widget.getParent().requestDisallowInterceptTouchEvent(true); 276 buffer.setSpan(LAST_TAP_DOWN, offset, offset, 277 Spannable.SPAN_POINT_POINT); 278 } 279 280 tap[0].mWhen = event.getEventTime(); 281 } else { 282 OnePointFiveTapState newtap = new OnePointFiveTapState(); 283 newtap.mWhen = event.getEventTime(); 284 newtap.active = false; 285 buffer.setSpan(newtap, 0, buffer.length(), 286 Spannable.SPAN_INCLUSIVE_INCLUSIVE); 287 } 288 } 289 } else if (event.getAction() == MotionEvent.ACTION_MOVE ) { 290 boolean cap = (MetaKeyKeyListener.getMetaState(buffer, 291 KeyEvent.META_SHIFT_ON) == 1) || 292 (MetaKeyKeyListener.getMetaState(buffer, 293 MetaKeyKeyListener.META_SELECTING) != 0); 294 295 if (cap & handled) { 296 // Before selecting, make sure we've moved out of the "slop". 297 // handled will be true, if we're in select mode AND we're 298 // OUT of the slop 299 300 // Turn long press off while we're selecting. User needs to 301 // re-tap on the selection to enable longpress 302 widget.cancelLongPress(); 303 304 // Update selection as we're moving the selection area. 305 306 // Get the current touch position 307 int x = (int) event.getX(); 308 int y = (int) event.getY(); 309 int offset = getOffset(x, y, widget); 310 311 // Get the last down touch position (the position at which the 312 // user started the selection) 313 int lastDownOffset = buffer.getSpanStart(LAST_TAP_DOWN); 314 315 // Compute the selection boundaries 316 int spanstart; 317 int spanend; 318 if (offset >= lastDownOffset) { 319 // Expand from word start of the original tap to new word 320 // end, since we are selecting "forwards" 321 spanstart = findWordStart(buffer, lastDownOffset); 322 spanend = findWordEnd(buffer, offset); 323 } else { 324 // Expand to from new word start to word end of the original 325 // tap since we are selecting "backwards". 326 // The spanend will always need to be associated with the touch 327 // up position, so that refining the selection with the 328 // trackball will work as expected. 329 spanstart = findWordEnd(buffer, lastDownOffset); 330 spanend = findWordStart(buffer, offset); 331 } 332 Selection.setSelection(buffer, spanstart, spanend); 333 return true; 334 } 335 } else if (event.getAction() == MotionEvent.ACTION_UP) { 336 // If we have scrolled, then the up shouldn't move the cursor, 337 // but we do need to make sure the cursor is still visible at 338 // the current scroll offset to avoid the scroll jumping later 339 // to show it. 340 if ((initialScrollY >= 0 && initialScrollY != widget.getScrollY()) || 341 (initialScrollX >= 0 && initialScrollX != widget.getScrollX())) { 342 widget.moveCursorToVisibleOffset(); 343 return true; 344 } 345 346 int x = (int) event.getX(); 347 int y = (int) event.getY(); 348 int off = getOffset(x, y, widget); 349 350 // XXX should do the same adjust for x as we do for the line. 351 352 OnePointFiveTapState[] onepointfivetap = buffer.getSpans(0, buffer.length(), 353 OnePointFiveTapState.class); 354 if (onepointfivetap.length > 0 && onepointfivetap[0].active && 355 Selection.getSelectionStart(buffer) == Selection.getSelectionEnd(buffer)) { 356 // If we've set select mode, because there was a onepointfivetap, 357 // but there was no ensuing swipe gesture, undo the select mode 358 // and remove reference to the last onepointfivetap. 359 MetaKeyKeyListener.stopSelecting(widget, buffer); 360 for (int i=0; i < onepointfivetap.length; i++) { 361 buffer.removeSpan(onepointfivetap[i]); 362 } 363 buffer.removeSpan(LAST_TAP_DOWN); 364 } 365 boolean cap = (MetaKeyKeyListener.getMetaState(buffer, 366 KeyEvent.META_SHIFT_ON) == 1) || 367 (MetaKeyKeyListener.getMetaState(buffer, 368 MetaKeyKeyListener.META_SELECTING) != 0); 369 370 DoubleTapState[] tap = buffer.getSpans(0, buffer.length(), 371 DoubleTapState.class); 372 boolean doubletap = false; 373 374 if (tap.length > 0) { 375 if (event.getEventTime() - tap[0].mWhen <= 376 ViewConfiguration.getDoubleTapTimeout() && 377 sameWord(buffer, off, Selection.getSelectionEnd(buffer))) { 378 379 doubletap = true; 380 } 381 382 tap[0].mWhen = event.getEventTime(); 383 } else { 384 DoubleTapState newtap = new DoubleTapState(); 385 newtap.mWhen = event.getEventTime(); 386 buffer.setSpan(newtap, 0, buffer.length(), 387 Spannable.SPAN_INCLUSIVE_INCLUSIVE); 388 } 389 390 if (cap) { 391 buffer.removeSpan(LAST_TAP_DOWN); 392 if (onepointfivetap.length > 0 && onepointfivetap[0].active) { 393 // If we selecting something with the onepointfivetap-and 394 // swipe gesture, stop it on finger up. 395 MetaKeyKeyListener.stopSelecting(widget, buffer); 396 } 397 } else if (doubletap) { 398 Selection.setSelection(buffer, 399 findWordStart(buffer, off), 400 findWordEnd(buffer, off)); 401 } else { 402 Selection.setSelection(buffer, off); 403 } 404 405 MetaKeyKeyListener.adjustMetaAfterKeypress(buffer); 406 MetaKeyKeyListener.resetLockedMeta(buffer); 407 408 return true; 409 } 410 } 411 412 return handled; 413 } 414 415 private static class DoubleTapState implements NoCopySpan { 416 long mWhen; 417 } 418 419 /* We check for a onepointfive tap. This is similar to 420 * doubletap gesture (where a finger goes down, up, down, up, in a short 421 * time period), except in the onepointfive tap, a users finger only needs 422 * to go down, up, down in a short time period. We detect this type of tap 423 * to implement the onepointfivetap-and-swipe selection gesture. 424 * This gesture allows users to select a segment of text without going 425 * through the "select text" option in the context menu. 426 */ 427 private static class OnePointFiveTapState implements NoCopySpan { 428 long mWhen; 429 boolean active; 430 } 431 432 private static boolean sameWord(CharSequence text, int one, int two) { 433 int start = findWordStart(text, one); 434 int end = findWordEnd(text, one); 435 436 if (end == start) { 437 return false; 438 } 439 440 return start == findWordStart(text, two) && 441 end == findWordEnd(text, two); 442 } 443 444 // TODO: Unify with TextView.getWordForDictionary() 445 private static int findWordStart(CharSequence text, int start) { 446 for (; start > 0; start--) { 447 char c = text.charAt(start - 1); 448 int type = Character.getType(c); 449 450 if (c != '\'' && 451 type != Character.UPPERCASE_LETTER && 452 type != Character.LOWERCASE_LETTER && 453 type != Character.TITLECASE_LETTER && 454 type != Character.MODIFIER_LETTER && 455 type != Character.DECIMAL_DIGIT_NUMBER) { 456 break; 457 } 458 } 459 460 return start; 461 } 462 463 // TODO: Unify with TextView.getWordForDictionary() 464 private static int findWordEnd(CharSequence text, int end) { 465 int len = text.length(); 466 467 for (; end < len; end++) { 468 char c = text.charAt(end); 469 int type = Character.getType(c); 470 471 if (c != '\'' && 472 type != Character.UPPERCASE_LETTER && 473 type != Character.LOWERCASE_LETTER && 474 type != Character.TITLECASE_LETTER && 475 type != Character.MODIFIER_LETTER && 476 type != Character.DECIMAL_DIGIT_NUMBER) { 477 break; 478 } 479 } 480 481 return end; 482 } 483 484 public boolean canSelectArbitrarily() { 485 return true; 486 } 487 488 public void initialize(TextView widget, Spannable text) { 489 Selection.setSelection(text, 0); 490 } 491 492 public void onTakeFocus(TextView view, Spannable text, int dir) { 493 if ((dir & (View.FOCUS_FORWARD | View.FOCUS_DOWN)) != 0) { 494 Layout layout = view.getLayout(); 495 496 if (layout == null) { 497 /* 498 * This shouldn't be null, but do something sensible if it is. 499 */ 500 Selection.setSelection(text, text.length()); 501 } else { 502 /* 503 * Put the cursor at the end of the first line, which is 504 * either the last offset if there is only one line, or the 505 * offset before the first character of the second line 506 * if there is more than one line. 507 */ 508 if (layout.getLineCount() == 1) { 509 Selection.setSelection(text, text.length()); 510 } else { 511 Selection.setSelection(text, layout.getLineStart(1) - 1); 512 } 513 } 514 } else { 515 Selection.setSelection(text, text.length()); 516 } 517 } 518 519 public static MovementMethod getInstance() { 520 if (sInstance == null) 521 sInstance = new ArrowKeyMovementMethod(); 522 523 return sInstance; 524 } 525 526 527 private static final Object LAST_TAP_DOWN = new Object(); 528 private static ArrowKeyMovementMethod sInstance; 529} 530