blob: 155a2c4fcfa096148fb2a7b7e01db36427ea659b [file] [log] [blame]
Jeff Brown67b6ab72010-12-17 18:33:02 -08001/*
2 * Copyright (C) 2010 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
19import android.text.Layout;
20import android.text.Spannable;
Jeff Brown8f345672011-02-26 13:29:53 -080021import android.view.InputDevice;
Jeff Brown67b6ab72010-12-17 18:33:02 -080022import android.view.KeyEvent;
23import android.view.MotionEvent;
24import android.widget.TextView;
25
26/**
27 * Base classes for movement methods.
28 */
29public class BaseMovementMethod implements MovementMethod {
30 @Override
31 public boolean canSelectArbitrarily() {
32 return false;
33 }
34
35 @Override
36 public void initialize(TextView widget, Spannable text) {
37 }
38
39 @Override
40 public boolean onKeyDown(TextView widget, Spannable text, int keyCode, KeyEvent event) {
41 final int movementMetaState = getMovementMetaState(text, event);
42 boolean handled = handleMovementKey(widget, text, keyCode, movementMetaState, event);
43 if (handled) {
44 MetaKeyKeyListener.adjustMetaAfterKeypress(text);
45 MetaKeyKeyListener.resetLockedMeta(text);
46 }
47 return handled;
48 }
49
50 @Override
51 public boolean onKeyOther(TextView widget, Spannable text, KeyEvent event) {
52 final int movementMetaState = getMovementMetaState(text, event);
53 final int keyCode = event.getKeyCode();
54 if (keyCode != KeyEvent.KEYCODE_UNKNOWN
55 && event.getAction() == KeyEvent.ACTION_MULTIPLE) {
56 final int repeat = event.getRepeatCount();
57 boolean handled = false;
58 for (int i = 0; i < repeat; i++) {
59 if (!handleMovementKey(widget, text, keyCode, movementMetaState, event)) {
60 break;
61 }
62 handled = true;
63 }
64 if (handled) {
65 MetaKeyKeyListener.adjustMetaAfterKeypress(text);
66 MetaKeyKeyListener.resetLockedMeta(text);
67 }
68 return handled;
69 }
70 return false;
71 }
72
73 @Override
74 public boolean onKeyUp(TextView widget, Spannable text, int keyCode, KeyEvent event) {
75 return false;
76 }
77
78 @Override
79 public void onTakeFocus(TextView widget, Spannable text, int direction) {
80 }
81
82 @Override
83 public boolean onTouchEvent(TextView widget, Spannable text, MotionEvent event) {
84 return false;
85 }
86
87 @Override
88 public boolean onTrackballEvent(TextView widget, Spannable text, MotionEvent event) {
89 return false;
90 }
91
Jeff Brown8f345672011-02-26 13:29:53 -080092 @Override
93 public boolean onGenericMotionEvent(TextView widget, Spannable text, MotionEvent event) {
94 if ((event.getSource() & InputDevice.SOURCE_CLASS_POINTER) != 0) {
95 switch (event.getAction()) {
96 case MotionEvent.ACTION_SCROLL: {
97 final float vscroll;
98 final float hscroll;
99 if ((event.getMetaState() & KeyEvent.META_SHIFT_ON) != 0) {
100 vscroll = 0;
101 hscroll = event.getAxisValue(MotionEvent.AXIS_VSCROLL);
102 } else {
103 vscroll = -event.getAxisValue(MotionEvent.AXIS_VSCROLL);
104 hscroll = event.getAxisValue(MotionEvent.AXIS_HSCROLL);
105 }
106
107 boolean handled = false;
108 if (hscroll < 0) {
109 handled |= scrollLeft(widget, text, (int)Math.ceil(-hscroll));
110 } else if (hscroll > 0) {
111 handled |= scrollRight(widget, text, (int)Math.ceil(hscroll));
112 }
113 if (vscroll < 0) {
114 handled |= scrollUp(widget, text, (int)Math.ceil(-vscroll));
115 } else if (vscroll > 0) {
116 handled |= scrollDown(widget, text, (int)Math.ceil(vscroll));
117 }
118 return handled;
119 }
120 }
121 }
122 return false;
123 }
124
Jeff Brown67b6ab72010-12-17 18:33:02 -0800125 /**
126 * Gets the meta state used for movement using the modifiers tracked by the text
127 * buffer as well as those present in the key event.
128 *
129 * The movement meta state excludes the state of locked modifiers or the SHIFT key
130 * since they are not used by movement actions (but they may be used for selection).
131 *
132 * @param buffer The text buffer.
133 * @param event The key event.
134 * @return The keyboard meta states used for movement.
135 */
136 protected int getMovementMetaState(Spannable buffer, KeyEvent event) {
137 // We ignore locked modifiers and SHIFT.
Jean Chalard8a1597b2013-03-04 18:45:12 -0800138 int metaState = MetaKeyKeyListener.getMetaState(buffer, event)
Jeff Brown67b6ab72010-12-17 18:33:02 -0800139 & ~(MetaKeyKeyListener.META_ALT_LOCKED | MetaKeyKeyListener.META_SYM_LOCKED);
140 return KeyEvent.normalizeMetaState(metaState) & ~KeyEvent.META_SHIFT_MASK;
141 }
142
143 /**
144 * Performs a movement key action.
145 * The default implementation decodes the key down and invokes movement actions
146 * such as {@link #down} and {@link #up}.
147 * {@link #onKeyDown(TextView, Spannable, int, KeyEvent)} calls this method once
148 * to handle an {@link KeyEvent#ACTION_DOWN}.
149 * {@link #onKeyOther(TextView, Spannable, KeyEvent)} calls this method repeatedly
150 * to handle each repetition of an {@link KeyEvent#ACTION_MULTIPLE}.
151 *
152 * @param widget The text view.
153 * @param buffer The text buffer.
154 * @param event The key event.
155 * @param keyCode The key code.
156 * @param movementMetaState The keyboard meta states used for movement.
157 * @param event The key event.
158 * @return True if the event was handled.
159 */
160 protected boolean handleMovementKey(TextView widget, Spannable buffer,
161 int keyCode, int movementMetaState, KeyEvent event) {
162 switch (keyCode) {
163 case KeyEvent.KEYCODE_DPAD_LEFT:
164 if (KeyEvent.metaStateHasNoModifiers(movementMetaState)) {
165 return left(widget, buffer);
166 } else if (KeyEvent.metaStateHasModifiers(movementMetaState,
Jeff Sharkeye982dfc12011-03-21 16:40:23 -0700167 KeyEvent.META_CTRL_ON)) {
168 return leftWord(widget, buffer);
169 } else if (KeyEvent.metaStateHasModifiers(movementMetaState,
Jeff Brown67b6ab72010-12-17 18:33:02 -0800170 KeyEvent.META_ALT_ON)) {
171 return lineStart(widget, buffer);
172 }
173 break;
174
175 case KeyEvent.KEYCODE_DPAD_RIGHT:
176 if (KeyEvent.metaStateHasNoModifiers(movementMetaState)) {
177 return right(widget, buffer);
178 } else if (KeyEvent.metaStateHasModifiers(movementMetaState,
Jeff Sharkeye982dfc12011-03-21 16:40:23 -0700179 KeyEvent.META_CTRL_ON)) {
180 return rightWord(widget, buffer);
181 } else if (KeyEvent.metaStateHasModifiers(movementMetaState,
Jeff Brown67b6ab72010-12-17 18:33:02 -0800182 KeyEvent.META_ALT_ON)) {
183 return lineEnd(widget, buffer);
184 }
185 break;
186
187 case KeyEvent.KEYCODE_DPAD_UP:
188 if (KeyEvent.metaStateHasNoModifiers(movementMetaState)) {
189 return up(widget, buffer);
190 } else if (KeyEvent.metaStateHasModifiers(movementMetaState,
191 KeyEvent.META_ALT_ON)) {
192 return top(widget, buffer);
193 }
194 break;
195
196 case KeyEvent.KEYCODE_DPAD_DOWN:
197 if (KeyEvent.metaStateHasNoModifiers(movementMetaState)) {
198 return down(widget, buffer);
199 } else if (KeyEvent.metaStateHasModifiers(movementMetaState,
200 KeyEvent.META_ALT_ON)) {
201 return bottom(widget, buffer);
202 }
203 break;
204
205 case KeyEvent.KEYCODE_PAGE_UP:
206 if (KeyEvent.metaStateHasNoModifiers(movementMetaState)) {
207 return pageUp(widget, buffer);
208 } else if (KeyEvent.metaStateHasModifiers(movementMetaState,
209 KeyEvent.META_ALT_ON)) {
210 return top(widget, buffer);
211 }
212 break;
213
214 case KeyEvent.KEYCODE_PAGE_DOWN:
215 if (KeyEvent.metaStateHasNoModifiers(movementMetaState)) {
216 return pageDown(widget, buffer);
217 } else if (KeyEvent.metaStateHasModifiers(movementMetaState,
218 KeyEvent.META_ALT_ON)) {
219 return bottom(widget, buffer);
220 }
221 break;
222
223 case KeyEvent.KEYCODE_MOVE_HOME:
224 if (KeyEvent.metaStateHasNoModifiers(movementMetaState)) {
225 return home(widget, buffer);
Jeff Sharkeye982dfc12011-03-21 16:40:23 -0700226 } else if (KeyEvent.metaStateHasModifiers(movementMetaState,
227 KeyEvent.META_CTRL_ON)) {
228 return top(widget, buffer);
Jeff Brown67b6ab72010-12-17 18:33:02 -0800229 }
230 break;
231
232 case KeyEvent.KEYCODE_MOVE_END:
233 if (KeyEvent.metaStateHasNoModifiers(movementMetaState)) {
234 return end(widget, buffer);
Jeff Sharkeye982dfc12011-03-21 16:40:23 -0700235 } else if (KeyEvent.metaStateHasModifiers(movementMetaState,
236 KeyEvent.META_CTRL_ON)) {
237 return bottom(widget, buffer);
Jeff Brown67b6ab72010-12-17 18:33:02 -0800238 }
239 break;
240 }
241 return false;
242 }
243
244 /**
245 * Performs a left movement action.
246 * Moves the cursor or scrolls left by one character.
247 *
248 * @param widget The text view.
249 * @param buffer The text buffer.
250 * @return True if the event was handled.
251 */
252 protected boolean left(TextView widget, Spannable buffer) {
253 return false;
254 }
255
256 /**
257 * Performs a right movement action.
258 * Moves the cursor or scrolls right by one character.
259 *
260 * @param widget The text view.
261 * @param buffer The text buffer.
262 * @return True if the event was handled.
263 */
264 protected boolean right(TextView widget, Spannable buffer) {
265 return false;
266 }
267
268 /**
269 * Performs an up movement action.
270 * Moves the cursor or scrolls up by one line.
271 *
272 * @param widget The text view.
273 * @param buffer The text buffer.
274 * @return True if the event was handled.
275 */
276 protected boolean up(TextView widget, Spannable buffer) {
277 return false;
278 }
279
280 /**
281 * Performs a down movement action.
282 * Moves the cursor or scrolls down by one line.
283 *
284 * @param widget The text view.
285 * @param buffer The text buffer.
286 * @return True if the event was handled.
287 */
288 protected boolean down(TextView widget, Spannable buffer) {
289 return false;
290 }
291
292 /**
293 * Performs a page-up movement action.
294 * Moves the cursor or scrolls up by one page.
295 *
296 * @param widget The text view.
297 * @param buffer The text buffer.
298 * @return True if the event was handled.
299 */
300 protected boolean pageUp(TextView widget, Spannable buffer) {
301 return false;
302 }
303
304 /**
305 * Performs a page-down movement action.
306 * Moves the cursor or scrolls down by one page.
307 *
308 * @param widget The text view.
309 * @param buffer The text buffer.
310 * @return True if the event was handled.
311 */
312 protected boolean pageDown(TextView widget, Spannable buffer) {
313 return false;
314 }
315
316 /**
317 * Performs a top movement action.
318 * Moves the cursor or scrolls to the top of the buffer.
319 *
320 * @param widget The text view.
321 * @param buffer The text buffer.
322 * @return True if the event was handled.
323 */
324 protected boolean top(TextView widget, Spannable buffer) {
325 return false;
326 }
327
328 /**
329 * Performs a bottom movement action.
330 * Moves the cursor or scrolls to the bottom of the buffer.
331 *
332 * @param widget The text view.
333 * @param buffer The text buffer.
334 * @return True if the event was handled.
335 */
336 protected boolean bottom(TextView widget, Spannable buffer) {
337 return false;
338 }
339
340 /**
341 * Performs a line-start movement action.
342 * Moves the cursor or scrolls to the start of the line.
343 *
344 * @param widget The text view.
345 * @param buffer The text buffer.
346 * @return True if the event was handled.
347 */
348 protected boolean lineStart(TextView widget, Spannable buffer) {
349 return false;
350 }
351
352 /**
Ken Wakasaf76a50c2012-03-09 19:56:35 +0900353 * Performs a line-end movement action.
Jeff Brown67b6ab72010-12-17 18:33:02 -0800354 * Moves the cursor or scrolls to the end of the line.
355 *
356 * @param widget The text view.
357 * @param buffer The text buffer.
358 * @return True if the event was handled.
359 */
360 protected boolean lineEnd(TextView widget, Spannable buffer) {
361 return false;
362 }
363
Jeff Sharkeye982dfc12011-03-21 16:40:23 -0700364 /** {@hide} */
365 protected boolean leftWord(TextView widget, Spannable buffer) {
366 return false;
367 }
368
369 /** {@hide} */
370 protected boolean rightWord(TextView widget, Spannable buffer) {
371 return false;
372 }
373
Jeff Brown67b6ab72010-12-17 18:33:02 -0800374 /**
375 * Performs a home movement action.
376 * Moves the cursor or scrolls to the start of the line or to the top of the
377 * document depending on whether the insertion point is being moved or
378 * the document is being scrolled.
379 *
380 * @param widget The text view.
381 * @param buffer The text buffer.
382 * @return True if the event was handled.
383 */
384 protected boolean home(TextView widget, Spannable buffer) {
385 return false;
386 }
387
388 /**
389 * Performs an end movement action.
390 * Moves the cursor or scrolls to the start of the line or to the top of the
391 * document depending on whether the insertion point is being moved or
392 * the document is being scrolled.
393 *
394 * @param widget The text view.
395 * @param buffer The text buffer.
396 * @return True if the event was handled.
397 */
398 protected boolean end(TextView widget, Spannable buffer) {
399 return false;
400 }
Jeff Brown8f345672011-02-26 13:29:53 -0800401
402 private int getTopLine(TextView widget) {
403 return widget.getLayout().getLineForVertical(widget.getScrollY());
404 }
405
406 private int getBottomLine(TextView widget) {
407 return widget.getLayout().getLineForVertical(widget.getScrollY() + getInnerHeight(widget));
408 }
409
410 private int getInnerWidth(TextView widget) {
411 return widget.getWidth() - widget.getTotalPaddingLeft() - widget.getTotalPaddingRight();
412 }
413
414 private int getInnerHeight(TextView widget) {
415 return widget.getHeight() - widget.getTotalPaddingTop() - widget.getTotalPaddingBottom();
416 }
417
418 private int getCharacterWidth(TextView widget) {
419 return (int) Math.ceil(widget.getPaint().getFontSpacing());
420 }
421
422 private int getScrollBoundsLeft(TextView widget) {
423 final Layout layout = widget.getLayout();
424 final int topLine = getTopLine(widget);
425 final int bottomLine = getBottomLine(widget);
426 if (topLine > bottomLine) {
427 return 0;
428 }
429 int left = Integer.MAX_VALUE;
430 for (int line = topLine; line <= bottomLine; line++) {
431 final int lineLeft = (int) Math.floor(layout.getLineLeft(line));
432 if (lineLeft < left) {
433 left = lineLeft;
434 }
435 }
436 return left;
437 }
438
439 private int getScrollBoundsRight(TextView widget) {
440 final Layout layout = widget.getLayout();
441 final int topLine = getTopLine(widget);
442 final int bottomLine = getBottomLine(widget);
443 if (topLine > bottomLine) {
444 return 0;
445 }
446 int right = Integer.MIN_VALUE;
447 for (int line = topLine; line <= bottomLine; line++) {
448 final int lineRight = (int) Math.ceil(layout.getLineRight(line));
449 if (lineRight > right) {
450 right = lineRight;
451 }
452 }
453 return right;
454 }
455
456 /**
457 * Performs a scroll left action.
458 * Scrolls left by the specified number of characters.
459 *
460 * @param widget The text view.
461 * @param buffer The text buffer.
462 * @param amount The number of characters to scroll by. Must be at least 1.
463 * @return True if the event was handled.
464 * @hide
465 */
466 protected boolean scrollLeft(TextView widget, Spannable buffer, int amount) {
467 final int minScrollX = getScrollBoundsLeft(widget);
468 int scrollX = widget.getScrollX();
469 if (scrollX > minScrollX) {
470 scrollX = Math.max(scrollX - getCharacterWidth(widget) * amount, minScrollX);
471 widget.scrollTo(scrollX, widget.getScrollY());
472 return true;
473 }
474 return false;
475 }
476
477 /**
478 * Performs a scroll right action.
479 * Scrolls right by the specified number of characters.
480 *
481 * @param widget The text view.
482 * @param buffer The text buffer.
483 * @param amount The number of characters to scroll by. Must be at least 1.
484 * @return True if the event was handled.
485 * @hide
486 */
487 protected boolean scrollRight(TextView widget, Spannable buffer, int amount) {
488 final int maxScrollX = getScrollBoundsRight(widget) - getInnerWidth(widget);
489 int scrollX = widget.getScrollX();
490 if (scrollX < maxScrollX) {
491 scrollX = Math.min(scrollX + getCharacterWidth(widget) * amount, maxScrollX);
492 widget.scrollTo(scrollX, widget.getScrollY());
493 return true;
494 }
495 return false;
496 }
497
498 /**
499 * Performs a scroll up action.
500 * Scrolls up by the specified number of lines.
501 *
502 * @param widget The text view.
503 * @param buffer The text buffer.
504 * @param amount The number of lines to scroll by. Must be at least 1.
505 * @return True if the event was handled.
506 * @hide
507 */
508 protected boolean scrollUp(TextView widget, Spannable buffer, int amount) {
509 final Layout layout = widget.getLayout();
510 final int top = widget.getScrollY();
511 int topLine = layout.getLineForVertical(top);
512 if (layout.getLineTop(topLine) == top) {
513 // If the top line is partially visible, bring it all the way
514 // into view; otherwise, bring the previous line into view.
515 topLine -= 1;
516 }
517 if (topLine >= 0) {
518 topLine = Math.max(topLine - amount + 1, 0);
519 Touch.scrollTo(widget, layout, widget.getScrollX(), layout.getLineTop(topLine));
520 return true;
521 }
522 return false;
523 }
524
525 /**
526 * Performs a scroll down action.
527 * Scrolls down by the specified number of lines.
528 *
529 * @param widget The text view.
530 * @param buffer The text buffer.
531 * @param amount The number of lines to scroll by. Must be at least 1.
532 * @return True if the event was handled.
533 * @hide
534 */
535 protected boolean scrollDown(TextView widget, Spannable buffer, int amount) {
536 final Layout layout = widget.getLayout();
537 final int innerHeight = getInnerHeight(widget);
538 final int bottom = widget.getScrollY() + innerHeight;
539 int bottomLine = layout.getLineForVertical(bottom);
540 if (layout.getLineTop(bottomLine + 1) < bottom + 1) {
541 // Less than a pixel of this line is out of view,
542 // so we must have tried to make it entirely in view
543 // and now want the next line to be in view instead.
544 bottomLine += 1;
545 }
546 final int limit = layout.getLineCount() - 1;
547 if (bottomLine <= limit) {
548 bottomLine = Math.min(bottomLine + amount - 1, limit);
549 Touch.scrollTo(widget, layout, widget.getScrollX(),
550 layout.getLineTop(bottomLine + 1) - innerHeight);
551 return true;
552 }
553 return false;
554 }
555
556 /**
557 * Performs a scroll page up action.
558 * Scrolls up by one page.
559 *
560 * @param widget The text view.
561 * @param buffer The text buffer.
562 * @return True if the event was handled.
563 * @hide
564 */
565 protected boolean scrollPageUp(TextView widget, Spannable buffer) {
566 final Layout layout = widget.getLayout();
567 final int top = widget.getScrollY() - getInnerHeight(widget);
568 int topLine = layout.getLineForVertical(top);
569 if (topLine >= 0) {
570 Touch.scrollTo(widget, layout, widget.getScrollX(), layout.getLineTop(topLine));
571 return true;
572 }
573 return false;
574 }
575
576 /**
577 * Performs a scroll page up action.
578 * Scrolls down by one page.
579 *
580 * @param widget The text view.
581 * @param buffer The text buffer.
582 * @return True if the event was handled.
583 * @hide
584 */
585 protected boolean scrollPageDown(TextView widget, Spannable buffer) {
586 final Layout layout = widget.getLayout();
587 final int innerHeight = getInnerHeight(widget);
588 final int bottom = widget.getScrollY() + innerHeight + innerHeight;
589 int bottomLine = layout.getLineForVertical(bottom);
590 if (bottomLine <= layout.getLineCount() - 1) {
591 Touch.scrollTo(widget, layout, widget.getScrollX(),
592 layout.getLineTop(bottomLine + 1) - innerHeight);
593 return true;
594 }
595 return false;
596 }
597
598 /**
599 * Performs a scroll to top action.
600 * Scrolls to the top of the document.
601 *
602 * @param widget The text view.
603 * @param buffer The text buffer.
604 * @return True if the event was handled.
605 * @hide
606 */
607 protected boolean scrollTop(TextView widget, Spannable buffer) {
608 final Layout layout = widget.getLayout();
609 if (getTopLine(widget) >= 0) {
610 Touch.scrollTo(widget, layout, widget.getScrollX(), layout.getLineTop(0));
611 return true;
612 }
613 return false;
614 }
615
616 /**
617 * Performs a scroll to bottom action.
618 * Scrolls to the bottom of the document.
619 *
620 * @param widget The text view.
621 * @param buffer The text buffer.
622 * @return True if the event was handled.
623 * @hide
624 */
625 protected boolean scrollBottom(TextView widget, Spannable buffer) {
626 final Layout layout = widget.getLayout();
627 final int lineCount = layout.getLineCount();
628 if (getBottomLine(widget) <= lineCount - 1) {
629 Touch.scrollTo(widget, layout, widget.getScrollX(),
630 layout.getLineTop(lineCount) - getInnerHeight(widget));
631 return true;
632 }
633 return false;
634 }
635
636 /**
637 * Performs a scroll to line start action.
638 * Scrolls to the start of the line.
639 *
640 * @param widget The text view.
641 * @param buffer The text buffer.
642 * @return True if the event was handled.
643 * @hide
644 */
645 protected boolean scrollLineStart(TextView widget, Spannable buffer) {
646 final int minScrollX = getScrollBoundsLeft(widget);
647 int scrollX = widget.getScrollX();
648 if (scrollX > minScrollX) {
649 widget.scrollTo(minScrollX, widget.getScrollY());
650 return true;
651 }
652 return false;
653 }
654
655 /**
656 * Performs a scroll to line end action.
657 * Scrolls to the end of the line.
658 *
659 * @param widget The text view.
660 * @param buffer The text buffer.
661 * @return True if the event was handled.
662 * @hide
663 */
664 protected boolean scrollLineEnd(TextView widget, Spannable buffer) {
665 final int maxScrollX = getScrollBoundsRight(widget) - getInnerWidth(widget);
666 int scrollX = widget.getScrollX();
667 if (scrollX < maxScrollX) {
668 widget.scrollTo(maxScrollX, widget.getScrollY());
669 return true;
670 }
671 return false;
672 }
Jeff Brown67b6ab72010-12-17 18:33:02 -0800673}