blob: 80c010638256e4e285c4548566087b6e1f8c2335 [file] [log] [blame]
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08001/*
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
Jeff Brown67b6ab72010-12-17 18:33:02 -080019import android.graphics.Rect;
Jeff Sharkeye982dfc12011-03-21 16:40:23 -070020import android.text.CharSequenceIterator;
21import android.text.Editable;
Gilles Debunneb0d6ba12010-08-17 20:01:42 -070022import android.text.Layout;
23import android.text.Selection;
24import android.text.Spannable;
Jeff Sharkeye982dfc12011-03-21 16:40:23 -070025import android.text.Spanned;
26import android.text.TextWatcher;
27import android.util.Log;
28import android.util.MathUtils;
The Android Open Source Project9066cfe2009-03-03 19:31:44 -080029import android.view.KeyEvent;
The Android Open Source Project9066cfe2009-03-03 19:31:44 -080030import android.view.MotionEvent;
Gilles Debunneb0d6ba12010-08-17 20:01:42 -070031import android.view.View;
32import android.widget.TextView;
The Android Open Source Project9066cfe2009-03-03 19:31:44 -080033
Jeff Sharkeye982dfc12011-03-21 16:40:23 -070034import java.text.BreakIterator;
35import java.text.CharacterIterator;
36
Jeff Brown67b6ab72010-12-17 18:33:02 -080037/**
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) {
Jeff Brown497a92c2010-09-12 17:55:08 -070043 return ((MetaKeyKeyListener.getMetaState(buffer, MetaKeyKeyListener.META_SHIFT_ON) == 1) ||
Gilles Debunneb0d6ba12010-08-17 20:01:42 -070044 (MetaKeyKeyListener.getMetaState(buffer, MetaKeyKeyListener.META_SELECTING) != 0));
45 }
46
Jeff Brown67b6ab72010-12-17 18:33:02 -080047 private int getCurrentLineTop(Spannable buffer, Layout layout) {
48 return layout.getLineTop(layout.getLineForOffset(Selection.getSelectionEnd(buffer)));
Gilles Debunneb0d6ba12010-08-17 20:01:42 -070049 }
50
Jeff Brown67b6ab72010-12-17 18:33:02 -080051 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;
The Android Open Source Project9066cfe2009-03-03 19:31:44 -080057 }
58
Jeff Brown67b6ab72010-12-17 18:33:02 -080059 @Override
60 protected boolean handleMovementKey(TextView widget, Spannable buffer, int keyCode,
61 int movementMetaState, KeyEvent event) {
The Android Open Source Project9066cfe2009-03-03 19:31:44 -080062 switch (keyCode) {
Jeff Brown67b6ab72010-12-17 18:33:02 -080063 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 }
The Android Open Source Project9066cfe2009-03-03 19:31:44 -080076
Jeff Brown67b6ab72010-12-17 18:33:02 -080077 @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 }
The Android Open Source Project9066cfe2009-03-03 19:31:44 -080086
Jeff Brown67b6ab72010-12-17 18:33:02 -080087 @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 }
The Android Open Source Project9066cfe2009-03-03 19:31:44 -080096
Jeff Brown67b6ab72010-12-17 18:33:02 -080097 @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 }
The Android Open Source Project9066cfe2009-03-03 19:31:44 -0800106
Jeff Brown67b6ab72010-12-17 18:33:02 -0800107 @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;
The Android Open Source Project9066cfe2009-03-03 19:31:44 -0800136 }
137 }
The Android Open Source Project9066cfe2009-03-03 19:31:44 -0800138 return handled;
139 }
140
Jeff Brown67b6ab72010-12-17 18:33:02 -0800141 @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);
The Android Open Source Project9066cfe2009-03-03 19:31:44 -0800153 }
Jeff Brown67b6ab72010-12-17 18:33:02 -0800154 if (Selection.getSelectionEnd(buffer) == previousSelectionEnd) {
155 break;
156 }
157 handled = true;
158 if (getCurrentLineTop(buffer, layout) >= targetY) {
159 break;
160 }
The Android Open Source Project9066cfe2009-03-03 19:31:44 -0800161 }
Jeff Brown67b6ab72010-12-17 18:33:02 -0800162 return handled;
The Android Open Source Project9066cfe2009-03-03 19:31:44 -0800163 }
Maryam Garrettce083792009-12-01 14:40:03 -0500164
Jeff Brown67b6ab72010-12-17 18:33:02 -0800165 @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;
The Android Open Source Project9066cfe2009-03-03 19:31:44 -0800173 }
Maryam Garrettce083792009-12-01 14:40:03 -0500174
Jeff Brown67b6ab72010-12-17 18:33:02 -0800175 @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
Jeff Sharkeye982dfc12011-03-21 16:40:23 -0700205 /** {@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
Jeff Brown67b6ab72010-12-17 18:33:02 -0800219 @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
Gilles Debunneb0d6ba12010-08-17 20:01:42 -0700230 public boolean onTouchEvent(TextView widget, Spannable buffer, MotionEvent event) {
Jeff Sharkeye982dfc12011-03-21 16:40:23 -0700231 int initialScrollX = -1;
232 int initialScrollY = -1;
Gilles Debunne0eb704c2010-11-30 12:50:54 -0800233 final int action = event.getAction();
234
235 if (action == MotionEvent.ACTION_UP) {
Dianne Hackborn1c9aefd2009-03-24 18:18:28 -0700236 initialScrollX = Touch.getInitialScrollX(widget, buffer);
237 initialScrollY = Touch.getInitialScrollY(widget, buffer);
238 }
Maryam Garrettce083792009-12-01 14:40:03 -0500239
The Android Open Source Project9066cfe2009-03-03 19:31:44 -0800240 boolean handled = Touch.onTouchEvent(widget, buffer, event);
241
The Android Open Source Projectb2a3dd82009-03-09 11:52:12 -0700242 if (widget.isFocused() && !widget.didTouchFocusSelect()) {
Gilles Debunne0eb704c2010-11-30 12:50:54 -0800243 if (action == MotionEvent.ACTION_DOWN) {
Jeff Brown67b6ab72010-12-17 18:33:02 -0800244 boolean cap = isSelecting(buffer);
Maryam Garrettce083792009-12-01 14:40:03 -0500245 if (cap) {
Gilles Debunneb0d6ba12010-08-17 20:01:42 -0700246 int offset = widget.getOffset((int) event.getX(), (int) event.getY());
Jeff Sharkeye982dfc12011-03-21 16:40:23 -0700247
Gilles Debunneb0d6ba12010-08-17 20:01:42 -0700248 buffer.setSpan(LAST_TAP_DOWN, offset, offset, Spannable.SPAN_POINT_POINT);
Maryam Garrettab928932009-12-10 15:42:30 -0500249
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);
Maryam Garrettce083792009-12-01 14:40:03 -0500255 }
Gilles Debunne0eb704c2010-11-30 12:50:54 -0800256 } else if (action == MotionEvent.ACTION_MOVE) {
Jeff Brown67b6ab72010-12-17 18:33:02 -0800257 boolean cap = isSelecting(buffer);
Maryam Garrettce083792009-12-01 14:40:03 -0500258
Kenny Root8cdb6842010-04-05 14:27:59 -0700259 if (cap && handled) {
Maryam Garrett39f0efb2009-12-11 13:52:06 -0500260 // 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
Maryam Garrettce083792009-12-01 14:40:03 -0500263
Maryam Garrett39f0efb2009-12-11 13:52:06 -0500264 // Turn long press off while we're selecting. User needs to
Gilles Debunneb0d6ba12010-08-17 20:01:42 -0700265 // re-tap on the selection to enable long press
Maryam Garrett39f0efb2009-12-11 13:52:06 -0500266 widget.cancelLongPress();
Maryam Garrettce083792009-12-01 14:40:03 -0500267
Maryam Garrett39f0efb2009-12-11 13:52:06 -0500268 // Update selection as we're moving the selection area.
Maryam Garrettce083792009-12-01 14:40:03 -0500269
Maryam Garrett39f0efb2009-12-11 13:52:06 -0500270 // Get the current touch position
Gilles Debunneb0d6ba12010-08-17 20:01:42 -0700271 int offset = widget.getOffset((int) event.getX(), (int) event.getY());
Maryam Garrett39f0efb2009-12-11 13:52:06 -0500272
Gilles Debunneb0d6ba12010-08-17 20:01:42 -0700273 Selection.extendSelection(buffer, offset);
Maryam Garrett39f0efb2009-12-11 13:52:06 -0500274 return true;
Maryam Garrettce083792009-12-01 14:40:03 -0500275 }
Gilles Debunne0eb704c2010-11-30 12:50:54 -0800276 } else if (action == MotionEvent.ACTION_UP) {
Dianne Hackborn1c9aefd2009-03-24 18:18:28 -0700277 // 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()) ||
Gilles Debunneb0d6ba12010-08-17 20:01:42 -0700282 (initialScrollX >= 0 && initialScrollX != widget.getScrollX())) {
Dianne Hackborn1c9aefd2009-03-24 18:18:28 -0700283 widget.moveCursorToVisibleOffset();
284 return true;
285 }
Maryam Garrettce083792009-12-01 14:40:03 -0500286
Gilles Debunneb0d6ba12010-08-17 20:01:42 -0700287 int offset = widget.getOffset((int) event.getX(), (int) event.getY());
Jeff Brown67b6ab72010-12-17 18:33:02 -0800288 if (isSelecting(buffer)) {
Maryam Garrett62c4ad32010-01-06 13:06:20 -0500289 buffer.removeSpan(LAST_TAP_DOWN);
Gilles Debunneb0d6ba12010-08-17 20:01:42 -0700290 Selection.extendSelection(buffer, offset);
Eric Fischera6e50452009-07-24 18:14:35 -0700291 } else {
Gilles Debunneb0d6ba12010-08-17 20:01:42 -0700292 Selection.setSelection(buffer, offset);
The Android Open Source Project9066cfe2009-03-03 19:31:44 -0800293 }
294
295 MetaKeyKeyListener.adjustMetaAfterKeypress(buffer);
296 MetaKeyKeyListener.resetLockedMeta(buffer);
297
298 return true;
299 }
300 }
301
302 return handled;
303 }
304
Jeff Brown67b6ab72010-12-17 18:33:02 -0800305 @Override
The Android Open Source Project9066cfe2009-03-03 19:31:44 -0800306 public boolean canSelectArbitrarily() {
307 return true;
308 }
309
Jeff Brown67b6ab72010-12-17 18:33:02 -0800310 @Override
The Android Open Source Project9066cfe2009-03-03 19:31:44 -0800311 public void initialize(TextView widget, Spannable text) {
312 Selection.setSelection(text, 0);
313 }
314
Jeff Brown67b6ab72010-12-17 18:33:02 -0800315 @Override
The Android Open Source Project9066cfe2009-03-03 19:31:44 -0800316 public void onTakeFocus(TextView view, Spannable text, int dir) {
317 if ((dir & (View.FOCUS_FORWARD | View.FOCUS_DOWN)) != 0) {
Gilles Debunne2703a422010-08-23 15:14:03 -0700318 if (view.getLayout() == null) {
319 // This shouldn't be null, but do something sensible if it is.
The Android Open Source Project9066cfe2009-03-03 19:31:44 -0800320 Selection.setSelection(text, text.length());
The Android Open Source Project9066cfe2009-03-03 19:31:44 -0800321 }
322 } else {
323 Selection.setSelection(text, text.length());
324 }
325 }
326
327 public static MovementMethod getInstance() {
Gilles Debunneb0d6ba12010-08-17 20:01:42 -0700328 if (sInstance == null) {
The Android Open Source Project9066cfe2009-03-03 19:31:44 -0800329 sInstance = new ArrowKeyMovementMethod();
Gilles Debunneb0d6ba12010-08-17 20:01:42 -0700330 }
The Android Open Source Project9066cfe2009-03-03 19:31:44 -0800331
332 return sInstance;
333 }
334
Jeff Sharkeye982dfc12011-03-21 16:40:23 -0700335 /**
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() - 1);
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() - 1);
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();
Maryam Garrettce083792009-12-01 14:40:03 -0500432
433 private static final Object LAST_TAP_DOWN = new Object();
The Android Open Source Project9066cfe2009-03-03 19:31:44 -0800434 private static ArrowKeyMovementMethod sInstance;
435}