BaseMovementMethod.java revision 8f34567c71003505456a9b1a0d461a4e62883d70
1/*
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;
21import android.view.InputDevice;
22import 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
92    @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
125    /**
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.
138        int metaState = (event.getMetaState() | MetaKeyKeyListener.getMetaState(buffer))
139                & ~(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,
167                        KeyEvent.META_ALT_ON)) {
168                    return lineStart(widget, buffer);
169                }
170                break;
171
172            case KeyEvent.KEYCODE_DPAD_RIGHT:
173                if (KeyEvent.metaStateHasNoModifiers(movementMetaState)) {
174                    return right(widget, buffer);
175                } else if (KeyEvent.metaStateHasModifiers(movementMetaState,
176                        KeyEvent.META_ALT_ON)) {
177                    return lineEnd(widget, buffer);
178                }
179                break;
180
181            case KeyEvent.KEYCODE_DPAD_UP:
182                if (KeyEvent.metaStateHasNoModifiers(movementMetaState)) {
183                    return up(widget, buffer);
184                } else if (KeyEvent.metaStateHasModifiers(movementMetaState,
185                        KeyEvent.META_ALT_ON)) {
186                    return top(widget, buffer);
187                }
188                break;
189
190            case KeyEvent.KEYCODE_DPAD_DOWN:
191                if (KeyEvent.metaStateHasNoModifiers(movementMetaState)) {
192                    return down(widget, buffer);
193                } else if (KeyEvent.metaStateHasModifiers(movementMetaState,
194                        KeyEvent.META_ALT_ON)) {
195                    return bottom(widget, buffer);
196                }
197                break;
198
199            case KeyEvent.KEYCODE_PAGE_UP:
200                if (KeyEvent.metaStateHasNoModifiers(movementMetaState)) {
201                    return pageUp(widget, buffer);
202                } else if (KeyEvent.metaStateHasModifiers(movementMetaState,
203                        KeyEvent.META_ALT_ON)) {
204                    return top(widget, buffer);
205                }
206                break;
207
208            case KeyEvent.KEYCODE_PAGE_DOWN:
209                if (KeyEvent.metaStateHasNoModifiers(movementMetaState)) {
210                    return pageDown(widget, buffer);
211                } else if (KeyEvent.metaStateHasModifiers(movementMetaState,
212                        KeyEvent.META_ALT_ON)) {
213                    return bottom(widget, buffer);
214                }
215                break;
216
217            case KeyEvent.KEYCODE_MOVE_HOME:
218                if (KeyEvent.metaStateHasNoModifiers(movementMetaState)) {
219                    return home(widget, buffer);
220                }
221                break;
222
223            case KeyEvent.KEYCODE_MOVE_END:
224                if (KeyEvent.metaStateHasNoModifiers(movementMetaState)) {
225                    return end(widget, buffer);
226                }
227                break;
228        }
229        return false;
230    }
231
232    /**
233     * Performs a left movement action.
234     * Moves the cursor or scrolls left by one character.
235     *
236     * @param widget The text view.
237     * @param buffer The text buffer.
238     * @return True if the event was handled.
239     */
240    protected boolean left(TextView widget, Spannable buffer) {
241        return false;
242    }
243
244    /**
245     * Performs a right movement action.
246     * Moves the cursor or scrolls right 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 right(TextView widget, Spannable buffer) {
253        return false;
254    }
255
256    /**
257     * Performs an up movement action.
258     * Moves the cursor or scrolls up by one line.
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 up(TextView widget, Spannable buffer) {
265        return false;
266    }
267
268    /**
269     * Performs a down movement action.
270     * Moves the cursor or scrolls down 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 down(TextView widget, Spannable buffer) {
277        return false;
278    }
279
280    /**
281     * Performs a page-up movement action.
282     * Moves the cursor or scrolls up by one page.
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 pageUp(TextView widget, Spannable buffer) {
289        return false;
290    }
291
292    /**
293     * Performs a page-down movement action.
294     * Moves the cursor or scrolls down 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 pageDown(TextView widget, Spannable buffer) {
301        return false;
302    }
303
304    /**
305     * Performs a top movement action.
306     * Moves the cursor or scrolls to the top of the buffer.
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 top(TextView widget, Spannable buffer) {
313        return false;
314    }
315
316    /**
317     * Performs a bottom movement action.
318     * Moves the cursor or scrolls to the bottom 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 bottom(TextView widget, Spannable buffer) {
325        return false;
326    }
327
328    /**
329     * Performs a line-start movement action.
330     * Moves the cursor or scrolls to the start of the line.
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 lineStart(TextView widget, Spannable buffer) {
337        return false;
338    }
339
340    /**
341     * Performs an line-end movement action.
342     * Moves the cursor or scrolls to the end 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 lineEnd(TextView widget, Spannable buffer) {
349        return false;
350    }
351
352    /**
353     * Performs a home movement action.
354     * Moves the cursor or scrolls to the start of the line or to the top of the
355     * document depending on whether the insertion point is being moved or
356     * the document is being scrolled.
357     *
358     * @param widget The text view.
359     * @param buffer The text buffer.
360     * @return True if the event was handled.
361     */
362    protected boolean home(TextView widget, Spannable buffer) {
363        return false;
364    }
365
366    /**
367     * Performs an end movement action.
368     * Moves the cursor or scrolls to the start of the line or to the top of the
369     * document depending on whether the insertion point is being moved or
370     * the document is being scrolled.
371     *
372     * @param widget The text view.
373     * @param buffer The text buffer.
374     * @return True if the event was handled.
375     */
376    protected boolean end(TextView widget, Spannable buffer) {
377        return false;
378    }
379
380    private int getTopLine(TextView widget) {
381        return widget.getLayout().getLineForVertical(widget.getScrollY());
382    }
383
384    private int getBottomLine(TextView widget) {
385        return widget.getLayout().getLineForVertical(widget.getScrollY() + getInnerHeight(widget));
386    }
387
388    private int getInnerWidth(TextView widget) {
389        return widget.getWidth() - widget.getTotalPaddingLeft() - widget.getTotalPaddingRight();
390    }
391
392    private int getInnerHeight(TextView widget) {
393        return widget.getHeight() - widget.getTotalPaddingTop() - widget.getTotalPaddingBottom();
394    }
395
396    private int getCharacterWidth(TextView widget) {
397        return (int) Math.ceil(widget.getPaint().getFontSpacing());
398    }
399
400    private int getScrollBoundsLeft(TextView widget) {
401        final Layout layout = widget.getLayout();
402        final int topLine = getTopLine(widget);
403        final int bottomLine = getBottomLine(widget);
404        if (topLine > bottomLine) {
405            return 0;
406        }
407        int left = Integer.MAX_VALUE;
408        for (int line = topLine; line <= bottomLine; line++) {
409            final int lineLeft = (int) Math.floor(layout.getLineLeft(line));
410            if (lineLeft < left) {
411                left = lineLeft;
412            }
413        }
414        return left;
415    }
416
417    private int getScrollBoundsRight(TextView widget) {
418        final Layout layout = widget.getLayout();
419        final int topLine = getTopLine(widget);
420        final int bottomLine = getBottomLine(widget);
421        if (topLine > bottomLine) {
422            return 0;
423        }
424        int right = Integer.MIN_VALUE;
425        for (int line = topLine; line <= bottomLine; line++) {
426            final int lineRight = (int) Math.ceil(layout.getLineRight(line));
427            if (lineRight > right) {
428                right = lineRight;
429            }
430        }
431        return right;
432    }
433
434    /**
435     * Performs a scroll left action.
436     * Scrolls left by the specified number of characters.
437     *
438     * @param widget The text view.
439     * @param buffer The text buffer.
440     * @param amount The number of characters to scroll by.  Must be at least 1.
441     * @return True if the event was handled.
442     * @hide
443     */
444    protected boolean scrollLeft(TextView widget, Spannable buffer, int amount) {
445        final int minScrollX = getScrollBoundsLeft(widget);
446        int scrollX = widget.getScrollX();
447        if (scrollX > minScrollX) {
448            scrollX = Math.max(scrollX - getCharacterWidth(widget) * amount, minScrollX);
449            widget.scrollTo(scrollX, widget.getScrollY());
450            return true;
451        }
452        return false;
453    }
454
455    /**
456     * Performs a scroll right action.
457     * Scrolls right by the specified number of characters.
458     *
459     * @param widget The text view.
460     * @param buffer The text buffer.
461     * @param amount The number of characters to scroll by.  Must be at least 1.
462     * @return True if the event was handled.
463     * @hide
464     */
465    protected boolean scrollRight(TextView widget, Spannable buffer, int amount) {
466        final int maxScrollX = getScrollBoundsRight(widget) - getInnerWidth(widget);
467        int scrollX = widget.getScrollX();
468        if (scrollX < maxScrollX) {
469            scrollX = Math.min(scrollX + getCharacterWidth(widget) * amount, maxScrollX);
470            widget.scrollTo(scrollX, widget.getScrollY());
471            return true;
472        }
473        return false;
474    }
475
476    /**
477     * Performs a scroll up action.
478     * Scrolls up by the specified number of lines.
479     *
480     * @param widget The text view.
481     * @param buffer The text buffer.
482     * @param amount The number of lines to scroll by.  Must be at least 1.
483     * @return True if the event was handled.
484     * @hide
485     */
486    protected boolean scrollUp(TextView widget, Spannable buffer, int amount) {
487        final Layout layout = widget.getLayout();
488        final int top = widget.getScrollY();
489        int topLine = layout.getLineForVertical(top);
490        if (layout.getLineTop(topLine) == top) {
491            // If the top line is partially visible, bring it all the way
492            // into view; otherwise, bring the previous line into view.
493            topLine -= 1;
494        }
495        if (topLine >= 0) {
496            topLine = Math.max(topLine - amount + 1, 0);
497            Touch.scrollTo(widget, layout, widget.getScrollX(), layout.getLineTop(topLine));
498            return true;
499        }
500        return false;
501    }
502
503    /**
504     * Performs a scroll down action.
505     * Scrolls down by the specified number of lines.
506     *
507     * @param widget The text view.
508     * @param buffer The text buffer.
509     * @param amount The number of lines to scroll by.  Must be at least 1.
510     * @return True if the event was handled.
511     * @hide
512     */
513    protected boolean scrollDown(TextView widget, Spannable buffer, int amount) {
514        final Layout layout = widget.getLayout();
515        final int innerHeight = getInnerHeight(widget);
516        final int bottom = widget.getScrollY() + innerHeight;
517        int bottomLine = layout.getLineForVertical(bottom);
518        if (layout.getLineTop(bottomLine + 1) < bottom + 1) {
519            // Less than a pixel of this line is out of view,
520            // so we must have tried to make it entirely in view
521            // and now want the next line to be in view instead.
522            bottomLine += 1;
523        }
524        final int limit = layout.getLineCount() - 1;
525        if (bottomLine <= limit) {
526            bottomLine = Math.min(bottomLine + amount - 1, limit);
527            Touch.scrollTo(widget, layout, widget.getScrollX(),
528                    layout.getLineTop(bottomLine + 1) - innerHeight);
529            return true;
530        }
531        return false;
532    }
533
534    /**
535     * Performs a scroll page up action.
536     * Scrolls up by one page.
537     *
538     * @param widget The text view.
539     * @param buffer The text buffer.
540     * @return True if the event was handled.
541     * @hide
542     */
543    protected boolean scrollPageUp(TextView widget, Spannable buffer) {
544        final Layout layout = widget.getLayout();
545        final int top = widget.getScrollY() - getInnerHeight(widget);
546        int topLine = layout.getLineForVertical(top);
547        if (topLine >= 0) {
548            Touch.scrollTo(widget, layout, widget.getScrollX(), layout.getLineTop(topLine));
549            return true;
550        }
551        return false;
552    }
553
554    /**
555     * Performs a scroll page up action.
556     * Scrolls down by one page.
557     *
558     * @param widget The text view.
559     * @param buffer The text buffer.
560     * @return True if the event was handled.
561     * @hide
562     */
563    protected boolean scrollPageDown(TextView widget, Spannable buffer) {
564        final Layout layout = widget.getLayout();
565        final int innerHeight = getInnerHeight(widget);
566        final int bottom = widget.getScrollY() + innerHeight + innerHeight;
567        int bottomLine = layout.getLineForVertical(bottom);
568        if (bottomLine <= layout.getLineCount() - 1) {
569            Touch.scrollTo(widget, layout, widget.getScrollX(),
570                    layout.getLineTop(bottomLine + 1) - innerHeight);
571            return true;
572        }
573        return false;
574    }
575
576    /**
577     * Performs a scroll to top action.
578     * Scrolls to the top of the document.
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 scrollTop(TextView widget, Spannable buffer) {
586        final Layout layout = widget.getLayout();
587        if (getTopLine(widget) >= 0) {
588            Touch.scrollTo(widget, layout, widget.getScrollX(), layout.getLineTop(0));
589            return true;
590        }
591        return false;
592    }
593
594    /**
595     * Performs a scroll to bottom action.
596     * Scrolls to the bottom of the document.
597     *
598     * @param widget The text view.
599     * @param buffer The text buffer.
600     * @return True if the event was handled.
601     * @hide
602     */
603    protected boolean scrollBottom(TextView widget, Spannable buffer) {
604        final Layout layout = widget.getLayout();
605        final int lineCount = layout.getLineCount();
606        if (getBottomLine(widget) <= lineCount - 1) {
607            Touch.scrollTo(widget, layout, widget.getScrollX(),
608                    layout.getLineTop(lineCount) - getInnerHeight(widget));
609            return true;
610        }
611        return false;
612    }
613
614    /**
615     * Performs a scroll to line start action.
616     * Scrolls to the start of the line.
617     *
618     * @param widget The text view.
619     * @param buffer The text buffer.
620     * @return True if the event was handled.
621     * @hide
622     */
623    protected boolean scrollLineStart(TextView widget, Spannable buffer) {
624        final int minScrollX = getScrollBoundsLeft(widget);
625        int scrollX = widget.getScrollX();
626        if (scrollX > minScrollX) {
627            widget.scrollTo(minScrollX, widget.getScrollY());
628            return true;
629        }
630        return false;
631    }
632
633    /**
634     * Performs a scroll to line end action.
635     * Scrolls to the end of the line.
636     *
637     * @param widget The text view.
638     * @param buffer The text buffer.
639     * @return True if the event was handled.
640     * @hide
641     */
642    protected boolean scrollLineEnd(TextView widget, Spannable buffer) {
643        final int maxScrollX = getScrollBoundsRight(widget) - getInnerWidth(widget);
644        int scrollX = widget.getScrollX();
645        if (scrollX < maxScrollX) {
646            widget.scrollTo(maxScrollX, widget.getScrollY());
647            return true;
648        }
649        return false;
650    }
651}
652