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.graphics.Rect; 20 import android.text.Layout; 21 import android.text.Selection; 22 import android.text.Spannable; 23 import android.view.InputDevice; 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 final Layout layout = widget.getLayout(); 72 if (isSelecting(buffer)) { 73 return Selection.extendLeft(buffer, layout); 74 } else { 75 return Selection.moveLeft(buffer, layout); 76 } 77 } 78 79 @Override right(TextView widget, Spannable buffer)80 protected boolean right(TextView widget, Spannable buffer) { 81 final Layout layout = widget.getLayout(); 82 if (isSelecting(buffer)) { 83 return Selection.extendRight(buffer, layout); 84 } else { 85 return Selection.moveRight(buffer, layout); 86 } 87 } 88 89 @Override up(TextView widget, Spannable buffer)90 protected boolean up(TextView widget, Spannable buffer) { 91 final Layout layout = widget.getLayout(); 92 if (isSelecting(buffer)) { 93 return Selection.extendUp(buffer, layout); 94 } else { 95 return Selection.moveUp(buffer, layout); 96 } 97 } 98 99 @Override down(TextView widget, Spannable buffer)100 protected boolean down(TextView widget, Spannable buffer) { 101 final Layout layout = widget.getLayout(); 102 if (isSelecting(buffer)) { 103 return Selection.extendDown(buffer, layout); 104 } else { 105 return Selection.moveDown(buffer, layout); 106 } 107 } 108 109 @Override pageUp(TextView widget, Spannable buffer)110 protected boolean pageUp(TextView widget, Spannable buffer) { 111 final Layout layout = widget.getLayout(); 112 final boolean selecting = isSelecting(buffer); 113 final int targetY = getCurrentLineTop(buffer, layout) - getPageHeight(widget); 114 boolean handled = false; 115 for (;;) { 116 final int previousSelectionEnd = Selection.getSelectionEnd(buffer); 117 if (selecting) { 118 Selection.extendUp(buffer, layout); 119 } else { 120 Selection.moveUp(buffer, layout); 121 } 122 if (Selection.getSelectionEnd(buffer) == previousSelectionEnd) { 123 break; 124 } 125 handled = true; 126 if (getCurrentLineTop(buffer, layout) <= targetY) { 127 break; 128 } 129 } 130 return handled; 131 } 132 133 @Override pageDown(TextView widget, Spannable buffer)134 protected boolean pageDown(TextView widget, Spannable buffer) { 135 final Layout layout = widget.getLayout(); 136 final boolean selecting = isSelecting(buffer); 137 final int targetY = getCurrentLineTop(buffer, layout) + getPageHeight(widget); 138 boolean handled = false; 139 for (;;) { 140 final int previousSelectionEnd = Selection.getSelectionEnd(buffer); 141 if (selecting) { 142 Selection.extendDown(buffer, layout); 143 } else { 144 Selection.moveDown(buffer, layout); 145 } 146 if (Selection.getSelectionEnd(buffer) == previousSelectionEnd) { 147 break; 148 } 149 handled = true; 150 if (getCurrentLineTop(buffer, layout) >= targetY) { 151 break; 152 } 153 } 154 return handled; 155 } 156 157 @Override top(TextView widget, Spannable buffer)158 protected boolean top(TextView widget, Spannable buffer) { 159 if (isSelecting(buffer)) { 160 Selection.extendSelection(buffer, 0); 161 } else { 162 Selection.setSelection(buffer, 0); 163 } 164 return true; 165 } 166 167 @Override bottom(TextView widget, Spannable buffer)168 protected boolean bottom(TextView widget, Spannable buffer) { 169 if (isSelecting(buffer)) { 170 Selection.extendSelection(buffer, buffer.length()); 171 } else { 172 Selection.setSelection(buffer, buffer.length()); 173 } 174 return true; 175 } 176 177 @Override lineStart(TextView widget, Spannable buffer)178 protected boolean lineStart(TextView widget, Spannable buffer) { 179 final Layout layout = widget.getLayout(); 180 if (isSelecting(buffer)) { 181 return Selection.extendToLeftEdge(buffer, layout); 182 } else { 183 return Selection.moveToLeftEdge(buffer, layout); 184 } 185 } 186 187 @Override lineEnd(TextView widget, Spannable buffer)188 protected boolean lineEnd(TextView widget, Spannable buffer) { 189 final Layout layout = widget.getLayout(); 190 if (isSelecting(buffer)) { 191 return Selection.extendToRightEdge(buffer, layout); 192 } else { 193 return Selection.moveToRightEdge(buffer, layout); 194 } 195 } 196 197 /** {@hide} */ 198 @Override leftWord(TextView widget, Spannable buffer)199 protected boolean leftWord(TextView widget, Spannable buffer) { 200 final int selectionEnd = widget.getSelectionEnd(); 201 final WordIterator wordIterator = widget.getWordIterator(); 202 wordIterator.setCharSequence(buffer, selectionEnd, selectionEnd); 203 return Selection.moveToPreceding(buffer, wordIterator, isSelecting(buffer)); 204 } 205 206 /** {@hide} */ 207 @Override rightWord(TextView widget, Spannable buffer)208 protected boolean rightWord(TextView widget, Spannable buffer) { 209 final int selectionEnd = widget.getSelectionEnd(); 210 final WordIterator wordIterator = widget.getWordIterator(); 211 wordIterator.setCharSequence(buffer, selectionEnd, selectionEnd); 212 return Selection.moveToFollowing(buffer, wordIterator, isSelecting(buffer)); 213 } 214 215 @Override home(TextView widget, Spannable buffer)216 protected boolean home(TextView widget, Spannable buffer) { 217 return lineStart(widget, buffer); 218 } 219 220 @Override end(TextView widget, Spannable buffer)221 protected boolean end(TextView widget, Spannable buffer) { 222 return lineEnd(widget, buffer); 223 } 224 isTouchSelecting(boolean isMouse, Spannable buffer)225 private static boolean isTouchSelecting(boolean isMouse, Spannable buffer) { 226 return isMouse ? Touch.isActivelySelecting(buffer) : isSelecting(buffer); 227 } 228 229 @Override onTouchEvent(TextView widget, Spannable buffer, MotionEvent event)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 final boolean isMouse = event.isFromSource(InputDevice.SOURCE_MOUSE); 235 236 if (action == MotionEvent.ACTION_UP) { 237 initialScrollX = Touch.getInitialScrollX(widget, buffer); 238 initialScrollY = Touch.getInitialScrollY(widget, buffer); 239 } 240 241 boolean handled = Touch.onTouchEvent(widget, buffer, event); 242 243 if (widget.didTouchFocusSelect() && !isMouse) { 244 return handled; 245 } 246 if (action == MotionEvent.ACTION_DOWN) { 247 // Capture the mouse pointer down location to ensure selection starts 248 // right under the mouse (and is not influenced by cursor location). 249 // The code below needs to run for mouse events. 250 // For touch events, the code should run only when selection is active. 251 if (isMouse || isTouchSelecting(isMouse, buffer)) { 252 if (!widget.isFocused()) { 253 if (!widget.requestFocus()) { 254 return handled; 255 } 256 } 257 int offset = widget.getOffsetForPosition(event.getX(), event.getY()); 258 buffer.setSpan(LAST_TAP_DOWN, offset, offset, Spannable.SPAN_POINT_POINT); 259 // Disallow intercepting of the touch events, so that 260 // users can scroll and select at the same time. 261 // without this, users would get booted out of select 262 // mode once the view detected it needed to scroll. 263 widget.getParent().requestDisallowInterceptTouchEvent(true); 264 } 265 } else if (widget.isFocused()) { 266 if (action == MotionEvent.ACTION_MOVE) { 267 // Cursor can be active at any location in the text while mouse pointer can start 268 // selection from a totally different location. Use LAST_TAP_DOWN span to ensure 269 // text selection will start from mouse pointer location. 270 if (isMouse && Touch.isSelectionStarted(buffer)) { 271 int offset = buffer.getSpanStart(LAST_TAP_DOWN); 272 Selection.setSelection(buffer, offset); 273 } 274 275 if (isTouchSelecting(isMouse, buffer) && handled) { 276 // Before selecting, make sure we've moved out of the "slop". 277 // handled will be true, if we're in select mode AND we're 278 // OUT of the slop 279 280 // Turn long press off while we're selecting. User needs to 281 // re-tap on the selection to enable long press 282 widget.cancelLongPress(); 283 284 // Update selection as we're moving the selection area. 285 286 // Get the current touch position 287 int offset = widget.getOffsetForPosition(event.getX(), event.getY()); 288 289 Selection.extendSelection(buffer, offset); 290 return true; 291 } 292 } else if (action == MotionEvent.ACTION_UP) { 293 // If we have scrolled, then the up shouldn't move the cursor, 294 // but we do need to make sure the cursor is still visible at 295 // the current scroll offset to avoid the scroll jumping later 296 // to show it. 297 if ((initialScrollY >= 0 && initialScrollY != widget.getScrollY()) || 298 (initialScrollX >= 0 && initialScrollX != widget.getScrollX())) { 299 widget.moveCursorToVisibleOffset(); 300 return true; 301 } 302 303 int offset = widget.getOffsetForPosition(event.getX(), event.getY()); 304 if (isTouchSelecting(isMouse, buffer)) { 305 buffer.removeSpan(LAST_TAP_DOWN); 306 Selection.extendSelection(buffer, offset); 307 } 308 309 MetaKeyKeyListener.adjustMetaAfterKeypress(buffer); 310 MetaKeyKeyListener.resetLockedMeta(buffer); 311 312 return true; 313 } 314 } 315 return handled; 316 } 317 318 @Override canSelectArbitrarily()319 public boolean canSelectArbitrarily() { 320 return true; 321 } 322 323 @Override initialize(TextView widget, Spannable text)324 public void initialize(TextView widget, Spannable text) { 325 Selection.setSelection(text, 0); 326 } 327 328 @Override onTakeFocus(TextView view, Spannable text, int dir)329 public void onTakeFocus(TextView view, Spannable text, int dir) { 330 if ((dir & (View.FOCUS_FORWARD | View.FOCUS_DOWN)) != 0) { 331 if (view.getLayout() == null) { 332 // This shouldn't be null, but do something sensible if it is. 333 Selection.setSelection(text, text.length()); 334 } 335 } else { 336 Selection.setSelection(text, text.length()); 337 } 338 } 339 getInstance()340 public static MovementMethod getInstance() { 341 if (sInstance == null) { 342 sInstance = new ArrowKeyMovementMethod(); 343 } 344 345 return sInstance; 346 } 347 348 private static final Object LAST_TAP_DOWN = new Object(); 349 private static ArrowKeyMovementMethod sInstance; 350 } 351