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