ArrowKeyMovementMethod.java revision daa4a95a54909d53a325eb06ca22130743b5be04
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.graphics.Rect; 20import android.text.CharSequenceIterator; 21import android.text.Editable; 22import android.text.Layout; 23import android.text.Selection; 24import android.text.Spannable; 25import android.text.Spanned; 26import android.text.TextWatcher; 27import android.util.Log; 28import android.util.MathUtils; 29import android.view.KeyEvent; 30import android.view.MotionEvent; 31import android.view.View; 32import android.widget.TextView; 33 34import java.text.BreakIterator; 35import java.text.CharacterIterator; 36 37/** 38 * A movement method that provides cursor movement and selection. 39 * Supports displaying the context menu on DPad Center. 40 */ 41public class ArrowKeyMovementMethod extends BaseMovementMethod implements MovementMethod { 42 private static boolean isSelecting(Spannable buffer) { 43 return ((MetaKeyKeyListener.getMetaState(buffer, MetaKeyKeyListener.META_SHIFT_ON) == 1) || 44 (MetaKeyKeyListener.getMetaState(buffer, MetaKeyKeyListener.META_SELECTING) != 0)); 45 } 46 47 private int getCurrentLineTop(Spannable buffer, Layout layout) { 48 return layout.getLineTop(layout.getLineForOffset(Selection.getSelectionEnd(buffer))); 49 } 50 51 private int getPageHeight(TextView widget) { 52 // This calculation does not take into account the view transformations that 53 // may have been applied to the child or its containers. In case of scaling or 54 // rotation, the calculated page height may be incorrect. 55 final Rect rect = new Rect(); 56 return widget.getGlobalVisibleRect(rect) ? rect.height() : 0; 57 } 58 59 @Override 60 protected boolean handleMovementKey(TextView widget, Spannable buffer, int keyCode, 61 int movementMetaState, KeyEvent event) { 62 switch (keyCode) { 63 case KeyEvent.KEYCODE_DPAD_CENTER: 64 if (KeyEvent.metaStateHasNoModifiers(movementMetaState)) { 65 if (event.getAction() == KeyEvent.ACTION_DOWN 66 && event.getRepeatCount() == 0 67 && MetaKeyKeyListener.getMetaState(buffer, 68 MetaKeyKeyListener.META_SELECTING) != 0) { 69 return widget.showContextMenu(); 70 } 71 } 72 break; 73 } 74 return super.handleMovementKey(widget, buffer, keyCode, movementMetaState, event); 75 } 76 77 @Override 78 protected boolean left(TextView widget, Spannable buffer) { 79 final Layout layout = widget.getLayout(); 80 if (isSelecting(buffer)) { 81 return Selection.extendLeft(buffer, layout); 82 } else { 83 return Selection.moveLeft(buffer, layout); 84 } 85 } 86 87 @Override 88 protected boolean right(TextView widget, Spannable buffer) { 89 final Layout layout = widget.getLayout(); 90 if (isSelecting(buffer)) { 91 return Selection.extendRight(buffer, layout); 92 } else { 93 return Selection.moveRight(buffer, layout); 94 } 95 } 96 97 @Override 98 protected boolean up(TextView widget, Spannable buffer) { 99 final Layout layout = widget.getLayout(); 100 if (isSelecting(buffer)) { 101 return Selection.extendUp(buffer, layout); 102 } else { 103 return Selection.moveUp(buffer, layout); 104 } 105 } 106 107 @Override 108 protected boolean down(TextView widget, Spannable buffer) { 109 final Layout layout = widget.getLayout(); 110 if (isSelecting(buffer)) { 111 return Selection.extendDown(buffer, layout); 112 } else { 113 return Selection.moveDown(buffer, layout); 114 } 115 } 116 117 @Override 118 protected boolean pageUp(TextView widget, Spannable buffer) { 119 final Layout layout = widget.getLayout(); 120 final boolean selecting = isSelecting(buffer); 121 final int targetY = getCurrentLineTop(buffer, layout) - getPageHeight(widget); 122 boolean handled = false; 123 for (;;) { 124 final int previousSelectionEnd = Selection.getSelectionEnd(buffer); 125 if (selecting) { 126 Selection.extendUp(buffer, layout); 127 } else { 128 Selection.moveUp(buffer, layout); 129 } 130 if (Selection.getSelectionEnd(buffer) == previousSelectionEnd) { 131 break; 132 } 133 handled = true; 134 if (getCurrentLineTop(buffer, layout) <= targetY) { 135 break; 136 } 137 } 138 return handled; 139 } 140 141 @Override 142 protected boolean pageDown(TextView widget, Spannable buffer) { 143 final Layout layout = widget.getLayout(); 144 final boolean selecting = isSelecting(buffer); 145 final int targetY = getCurrentLineTop(buffer, layout) + getPageHeight(widget); 146 boolean handled = false; 147 for (;;) { 148 final int previousSelectionEnd = Selection.getSelectionEnd(buffer); 149 if (selecting) { 150 Selection.extendDown(buffer, layout); 151 } else { 152 Selection.moveDown(buffer, layout); 153 } 154 if (Selection.getSelectionEnd(buffer) == previousSelectionEnd) { 155 break; 156 } 157 handled = true; 158 if (getCurrentLineTop(buffer, layout) >= targetY) { 159 break; 160 } 161 } 162 return handled; 163 } 164 165 @Override 166 protected boolean top(TextView widget, Spannable buffer) { 167 if (isSelecting(buffer)) { 168 Selection.extendSelection(buffer, 0); 169 } else { 170 Selection.setSelection(buffer, 0); 171 } 172 return true; 173 } 174 175 @Override 176 protected boolean bottom(TextView widget, Spannable buffer) { 177 if (isSelecting(buffer)) { 178 Selection.extendSelection(buffer, buffer.length()); 179 } else { 180 Selection.setSelection(buffer, buffer.length()); 181 } 182 return true; 183 } 184 185 @Override 186 protected boolean lineStart(TextView widget, Spannable buffer) { 187 final Layout layout = widget.getLayout(); 188 if (isSelecting(buffer)) { 189 return Selection.extendToLeftEdge(buffer, layout); 190 } else { 191 return Selection.moveToLeftEdge(buffer, layout); 192 } 193 } 194 195 @Override 196 protected boolean lineEnd(TextView widget, Spannable buffer) { 197 final Layout layout = widget.getLayout(); 198 if (isSelecting(buffer)) { 199 return Selection.extendToRightEdge(buffer, layout); 200 } else { 201 return Selection.moveToRightEdge(buffer, layout); 202 } 203 } 204 205 /** {@hide} */ 206 @Override 207 protected boolean leftWord(TextView widget, Spannable buffer) { 208 mWordIterator.setCharSequence(buffer); 209 return Selection.moveToPreceding(buffer, mWordIterator, isSelecting(buffer)); 210 } 211 212 /** {@hide} */ 213 @Override 214 protected boolean rightWord(TextView widget, Spannable buffer) { 215 mWordIterator.setCharSequence(buffer); 216 return Selection.moveToFollowing(buffer, mWordIterator, isSelecting(buffer)); 217 } 218 219 @Override 220 protected boolean home(TextView widget, Spannable buffer) { 221 return lineStart(widget, buffer); 222 } 223 224 @Override 225 protected boolean end(TextView widget, Spannable buffer) { 226 return lineEnd(widget, buffer); 227 } 228 229 @Override 230 public boolean onTouchEvent(TextView widget, Spannable buffer, MotionEvent event) { 231 int initialScrollX = -1; 232 int initialScrollY = -1; 233 final int action = event.getAction(); 234 235 if (action == MotionEvent.ACTION_UP) { 236 initialScrollX = Touch.getInitialScrollX(widget, buffer); 237 initialScrollY = Touch.getInitialScrollY(widget, buffer); 238 } 239 240 boolean handled = Touch.onTouchEvent(widget, buffer, event); 241 242 if (widget.isFocused() && !widget.didTouchFocusSelect()) { 243 if (action == MotionEvent.ACTION_DOWN) { 244 boolean cap = isSelecting(buffer); 245 if (cap) { 246 int offset = widget.getOffset((int) event.getX(), (int) event.getY()); 247 248 buffer.setSpan(LAST_TAP_DOWN, offset, offset, Spannable.SPAN_POINT_POINT); 249 250 // Disallow intercepting of the touch events, so that 251 // users can scroll and select at the same time. 252 // without this, users would get booted out of select 253 // mode once the view detected it needed to scroll. 254 widget.getParent().requestDisallowInterceptTouchEvent(true); 255 } 256 } else if (action == MotionEvent.ACTION_MOVE) { 257 boolean cap = isSelecting(buffer); 258 259 if (cap && handled) { 260 // Before selecting, make sure we've moved out of the "slop". 261 // handled will be true, if we're in select mode AND we're 262 // OUT of the slop 263 264 // Turn long press off while we're selecting. User needs to 265 // re-tap on the selection to enable long press 266 widget.cancelLongPress(); 267 268 // Update selection as we're moving the selection area. 269 270 // Get the current touch position 271 int offset = widget.getOffset((int) event.getX(), (int) event.getY()); 272 273 Selection.extendSelection(buffer, offset); 274 return true; 275 } 276 } else if (action == MotionEvent.ACTION_UP) { 277 // If we have scrolled, then the up shouldn't move the cursor, 278 // but we do need to make sure the cursor is still visible at 279 // the current scroll offset to avoid the scroll jumping later 280 // to show it. 281 if ((initialScrollY >= 0 && initialScrollY != widget.getScrollY()) || 282 (initialScrollX >= 0 && initialScrollX != widget.getScrollX())) { 283 widget.moveCursorToVisibleOffset(); 284 return true; 285 } 286 287 int offset = widget.getOffset((int) event.getX(), (int) event.getY()); 288 if (isSelecting(buffer)) { 289 buffer.removeSpan(LAST_TAP_DOWN); 290 Selection.extendSelection(buffer, offset); 291 } else { 292 Selection.setSelection(buffer, offset); 293 } 294 295 MetaKeyKeyListener.adjustMetaAfterKeypress(buffer); 296 MetaKeyKeyListener.resetLockedMeta(buffer); 297 298 return true; 299 } 300 } 301 302 return handled; 303 } 304 305 @Override 306 public boolean canSelectArbitrarily() { 307 return true; 308 } 309 310 @Override 311 public void initialize(TextView widget, Spannable text) { 312 Selection.setSelection(text, 0); 313 } 314 315 @Override 316 public void onTakeFocus(TextView view, Spannable text, int dir) { 317 if ((dir & (View.FOCUS_FORWARD | View.FOCUS_DOWN)) != 0) { 318 if (view.getLayout() == null) { 319 // This shouldn't be null, but do something sensible if it is. 320 Selection.setSelection(text, text.length()); 321 } 322 } else { 323 Selection.setSelection(text, text.length()); 324 } 325 } 326 327 public static MovementMethod getInstance() { 328 if (sInstance == null) { 329 sInstance = new ArrowKeyMovementMethod(); 330 } 331 332 return sInstance; 333 } 334 335 /** 336 * Walks through cursor positions at word boundaries. Internally uses 337 * {@link BreakIterator#getWordInstance()}, and caches {@link CharSequence} 338 * for performance reasons. 339 */ 340 private static class WordIterator implements Selection.PositionIterator { 341 private CharSequence mCurrent; 342 private boolean mCurrentDirty = false; 343 344 private BreakIterator mIterator; 345 346 private TextWatcher mWatcher = new TextWatcher() { 347 /** {@inheritDoc} */ 348 public void beforeTextChanged(CharSequence s, int start, int count, int after) { 349 // ignored 350 } 351 352 /** {@inheritDoc} */ 353 public void onTextChanged(CharSequence s, int start, int before, int count) { 354 mCurrentDirty = true; 355 } 356 357 /** {@inheritDoc} */ 358 public void afterTextChanged(Editable s) { 359 // ignored 360 } 361 }; 362 363 public void setCharSequence(CharSequence incoming) { 364 if (mIterator == null) { 365 mIterator = BreakIterator.getWordInstance(); 366 } 367 368 // when incoming is different object, move listeners to new sequence 369 // and mark as dirty so we reload contents. 370 if (mCurrent != incoming) { 371 if (mCurrent instanceof Editable) { 372 ((Editable) mCurrent).removeSpan(mWatcher); 373 } 374 375 if (incoming instanceof Editable) { 376 ((Editable) incoming).setSpan( 377 mWatcher, 0, incoming.length(), Spanned.SPAN_INCLUSIVE_INCLUSIVE); 378 } 379 380 mCurrent = incoming; 381 mCurrentDirty = true; 382 } 383 384 if (mCurrentDirty) { 385 final CharacterIterator charIterator = new CharSequenceIterator(mCurrent); 386 mIterator.setText(charIterator); 387 388 mCurrentDirty = false; 389 } 390 } 391 392 private boolean isValidOffset(int offset) { 393 return offset >= 0 && offset <= mCurrent.length(); 394 } 395 396 private boolean isLetterOrDigit(int offset) { 397 if (isValidOffset(offset)) { 398 return Character.isLetterOrDigit(mCurrent.charAt(offset)); 399 } else { 400 return false; 401 } 402 } 403 404 /** {@inheritDoc} */ 405 public int preceding(int offset) { 406 // always round cursor index into valid string index 407 offset = MathUtils.constrain(offset, 0, mCurrent.length()); 408 409 do { 410 offset = mIterator.preceding(offset); 411 if (isLetterOrDigit(offset)) break; 412 } while (isValidOffset(offset)); 413 414 return offset; 415 } 416 417 /** {@inheritDoc} */ 418 public int following(int offset) { 419 // always round cursor index into valid string index 420 offset = MathUtils.constrain(offset, 0, mCurrent.length()); 421 422 do { 423 offset = mIterator.following(offset); 424 if (isLetterOrDigit(offset - 1)) break; 425 } while (isValidOffset(offset)); 426 427 return offset; 428 } 429 } 430 431 private WordIterator mWordIterator = new WordIterator(); 432 433 private static final Object LAST_TAP_DOWN = new Object(); 434 private static ArrowKeyMovementMethod sInstance; 435} 436