ArrowKeyMovementMethod.java revision 39f0efba92a4420f77e3abc53c367ea3cacde3cf
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 if (cap) { 251 int x = (int) event.getX(); 252 int y = (int) event.getY(); 253 int offset = getOffset(x, y, widget); 254 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 } 264 } else if (event.getAction() == MotionEvent.ACTION_MOVE ) { 265 boolean cap = (MetaKeyKeyListener.getMetaState(buffer, 266 KeyEvent.META_SHIFT_ON) == 1) || 267 (MetaKeyKeyListener.getMetaState(buffer, 268 MetaKeyKeyListener.META_SELECTING) != 0); 269 270 if (cap & handled) { 271 // Before selecting, make sure we've moved out of the "slop". 272 // handled will be true, if we're in select mode AND we're 273 // OUT of the slop 274 275 // Turn long press off while we're selecting. User needs to 276 // re-tap on the selection to enable longpress 277 widget.cancelLongPress(); 278 279 // Update selection as we're moving the selection area. 280 281 // Get the current touch position 282 int x = (int) event.getX(); 283 int y = (int) event.getY(); 284 int offset = getOffset(x, y, widget); 285 286 // Get the last down touch position (the position at which the 287 // user started the selection) 288 int lastDownOffset = buffer.getSpanStart(LAST_TAP_DOWN); 289 290 // Compute the selection boundries 291 int spanstart; 292 int spanend; 293 if (offset >= lastDownOffset) { 294 // Expand from word start of the original tap to new word 295 // end, since we are selecting "forwards" 296 spanstart = findWordStart(buffer, lastDownOffset); 297 spanend = findWordEnd(buffer, offset); 298 } else { 299 // Expand to from new word start to word end of the original 300 // tap since we are selecting "backwards". 301 // The spanend will always need to be associated with the touch 302 // up position, so that refining the selection with the 303 // trackball will work as expected. 304 spanstart = findWordEnd(buffer, lastDownOffset); 305 spanend = findWordStart(buffer, offset); 306 } 307 Selection.setSelection(buffer, spanstart, spanend); 308 return true; 309 } 310 } else if (event.getAction() == MotionEvent.ACTION_UP) { 311 // If we have scrolled, then the up shouldn't move the cursor, 312 // but we do need to make sure the cursor is still visible at 313 // the current scroll offset to avoid the scroll jumping later 314 // to show it. 315 if ((initialScrollY >= 0 && initialScrollY != widget.getScrollY()) || 316 (initialScrollX >= 0 && initialScrollX != widget.getScrollX())) { 317 widget.moveCursorToVisibleOffset(); 318 return true; 319 } 320 321 int x = (int) event.getX(); 322 int y = (int) event.getY(); 323 int off = getOffset(x, y, widget); 324 325 // XXX should do the same adjust for x as we do for the line. 326 327 boolean cap = (MetaKeyKeyListener.getMetaState(buffer, 328 KeyEvent.META_SHIFT_ON) == 1) || 329 (MetaKeyKeyListener.getMetaState(buffer, 330 MetaKeyKeyListener.META_SELECTING) != 0); 331 332 DoubleTapState[] tap = buffer.getSpans(0, buffer.length(), 333 DoubleTapState.class); 334 boolean doubletap = false; 335 336 if (tap.length > 0) { 337 if (event.getEventTime() - tap[0].mWhen <= 338 ViewConfiguration.getDoubleTapTimeout()) { 339 if (sameWord(buffer, off, Selection.getSelectionEnd(buffer))) { 340 doubletap = true; 341 } 342 } 343 344 tap[0].mWhen = event.getEventTime(); 345 } else { 346 DoubleTapState newtap = new DoubleTapState(); 347 newtap.mWhen = event.getEventTime(); 348 buffer.setSpan(newtap, 0, buffer.length(), 349 Spannable.SPAN_INCLUSIVE_INCLUSIVE); 350 } 351 352 if (cap) { 353 buffer.removeSpan(LAST_TAP_DOWN); 354 } else if (doubletap) { 355 Selection.setSelection(buffer, 356 findWordStart(buffer, off), 357 findWordEnd(buffer, off)); 358 } else { 359 Selection.setSelection(buffer, off); 360 } 361 362 MetaKeyKeyListener.adjustMetaAfterKeypress(buffer); 363 MetaKeyKeyListener.resetLockedMeta(buffer); 364 365 return true; 366 } 367 } 368 369 return handled; 370 } 371 372 private static class DoubleTapState implements NoCopySpan { 373 long mWhen; 374 } 375 376 private static boolean sameWord(CharSequence text, int one, int two) { 377 int start = findWordStart(text, one); 378 int end = findWordEnd(text, one); 379 380 if (end == start) { 381 return false; 382 } 383 384 return start == findWordStart(text, two) && 385 end == findWordEnd(text, two); 386 } 387 388 // TODO: Unify with TextView.getWordForDictionary() 389 private static int findWordStart(CharSequence text, int start) { 390 for (; start > 0; start--) { 391 char c = text.charAt(start - 1); 392 int type = Character.getType(c); 393 394 if (c != '\'' && 395 type != Character.UPPERCASE_LETTER && 396 type != Character.LOWERCASE_LETTER && 397 type != Character.TITLECASE_LETTER && 398 type != Character.MODIFIER_LETTER && 399 type != Character.DECIMAL_DIGIT_NUMBER) { 400 break; 401 } 402 } 403 404 return start; 405 } 406 407 // TODO: Unify with TextView.getWordForDictionary() 408 private static int findWordEnd(CharSequence text, int end) { 409 int len = text.length(); 410 411 for (; end < len; end++) { 412 char c = text.charAt(end); 413 int type = Character.getType(c); 414 415 if (c != '\'' && 416 type != Character.UPPERCASE_LETTER && 417 type != Character.LOWERCASE_LETTER && 418 type != Character.TITLECASE_LETTER && 419 type != Character.MODIFIER_LETTER && 420 type != Character.DECIMAL_DIGIT_NUMBER) { 421 break; 422 } 423 } 424 425 return end; 426 } 427 428 public boolean canSelectArbitrarily() { 429 return true; 430 } 431 432 public void initialize(TextView widget, Spannable text) { 433 Selection.setSelection(text, 0); 434 } 435 436 public void onTakeFocus(TextView view, Spannable text, int dir) { 437 if ((dir & (View.FOCUS_FORWARD | View.FOCUS_DOWN)) != 0) { 438 Layout layout = view.getLayout(); 439 440 if (layout == null) { 441 /* 442 * This shouldn't be null, but do something sensible if it is. 443 */ 444 Selection.setSelection(text, text.length()); 445 } else { 446 /* 447 * Put the cursor at the end of the first line, which is 448 * either the last offset if there is only one line, or the 449 * offset before the first character of the second line 450 * if there is more than one line. 451 */ 452 if (layout.getLineCount() == 1) { 453 Selection.setSelection(text, text.length()); 454 } else { 455 Selection.setSelection(text, layout.getLineStart(1) - 1); 456 } 457 } 458 } else { 459 Selection.setSelection(text, text.length()); 460 } 461 } 462 463 public static MovementMethod getInstance() { 464 if (sInstance == null) 465 sInstance = new ArrowKeyMovementMethod(); 466 467 return sInstance; 468 } 469 470 471 private static final Object LAST_TAP_DOWN = new Object(); 472 private static ArrowKeyMovementMethod sInstance; 473} 474