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