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