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