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