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.text.Layout;
20 import android.text.Spannable;
21 import android.view.InputDevice;
22 import android.view.KeyEvent;
23 import android.view.MotionEvent;
24 import android.widget.TextView;
25 
26 /**
27  * Base classes for movement methods.
28  */
29 public class BaseMovementMethod implements MovementMethod {
30     @Override
canSelectArbitrarily()31     public boolean canSelectArbitrarily() {
32         return false;
33     }
34 
35     @Override
initialize(TextView widget, Spannable text)36     public void initialize(TextView widget, Spannable text) {
37     }
38 
39     @Override
onKeyDown(TextView widget, Spannable text, int keyCode, KeyEvent event)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
onKeyOther(TextView widget, Spannable text, KeyEvent event)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
onKeyUp(TextView widget, Spannable text, int keyCode, KeyEvent event)74     public boolean onKeyUp(TextView widget, Spannable text, int keyCode, KeyEvent event) {
75         return false;
76     }
77 
78     @Override
onTakeFocus(TextView widget, Spannable text, int direction)79     public void onTakeFocus(TextView widget, Spannable text, int direction) {
80     }
81 
82     @Override
onTouchEvent(TextView widget, Spannable text, MotionEvent event)83     public boolean onTouchEvent(TextView widget, Spannable text, MotionEvent event) {
84         return false;
85     }
86 
87     @Override
onTrackballEvent(TextView widget, Spannable text, MotionEvent event)88     public boolean onTrackballEvent(TextView widget, Spannable text, MotionEvent event) {
89         return false;
90     }
91 
92     @Override
onGenericMotionEvent(TextView widget, Spannable text, MotionEvent event)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      */
getMovementMetaState(Spannable buffer, KeyEvent event)136     protected int getMovementMetaState(Spannable buffer, KeyEvent event) {
137         // We ignore locked modifiers and SHIFT.
138         int metaState = MetaKeyKeyListener.getMetaState(buffer, event)
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      */
handleMovementKey(TextView widget, Spannable buffer, int keyCode, int movementMetaState, KeyEvent event)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_CTRL_ON)) {
168                     return leftWord(widget, buffer);
169                 } else if (KeyEvent.metaStateHasModifiers(movementMetaState,
170                         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,
179                         KeyEvent.META_CTRL_ON)) {
180                     return rightWord(widget, buffer);
181                 } else if (KeyEvent.metaStateHasModifiers(movementMetaState,
182                         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);
226                 } else if (KeyEvent.metaStateHasModifiers(movementMetaState,
227                         KeyEvent.META_CTRL_ON)) {
228                     return top(widget, buffer);
229                 }
230                 break;
231 
232             case KeyEvent.KEYCODE_MOVE_END:
233                 if (KeyEvent.metaStateHasNoModifiers(movementMetaState)) {
234                     return end(widget, buffer);
235                 } else if (KeyEvent.metaStateHasModifiers(movementMetaState,
236                         KeyEvent.META_CTRL_ON)) {
237                     return bottom(widget, buffer);
238                 }
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      */
left(TextView widget, Spannable buffer)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      */
right(TextView widget, Spannable buffer)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      */
up(TextView widget, Spannable buffer)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      */
down(TextView widget, Spannable buffer)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      */
pageUp(TextView widget, Spannable buffer)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      */
pageDown(TextView widget, Spannable buffer)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      */
top(TextView widget, Spannable buffer)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      */
bottom(TextView widget, Spannable buffer)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      */
lineStart(TextView widget, Spannable buffer)348     protected boolean lineStart(TextView widget, Spannable buffer) {
349         return false;
350     }
351 
352     /**
353      * Performs a line-end movement action.
354      * 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      */
lineEnd(TextView widget, Spannable buffer)360     protected boolean lineEnd(TextView widget, Spannable buffer) {
361         return false;
362     }
363 
364     /** {@hide} */
leftWord(TextView widget, Spannable buffer)365     protected boolean leftWord(TextView widget, Spannable buffer) {
366         return false;
367     }
368 
369     /** {@hide} */
rightWord(TextView widget, Spannable buffer)370     protected boolean rightWord(TextView widget, Spannable buffer) {
371         return false;
372     }
373 
374     /**
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      */
home(TextView widget, Spannable buffer)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      */
end(TextView widget, Spannable buffer)398     protected boolean end(TextView widget, Spannable buffer) {
399         return false;
400     }
401 
getTopLine(TextView widget)402     private int getTopLine(TextView widget) {
403         return widget.getLayout().getLineForVertical(widget.getScrollY());
404     }
405 
getBottomLine(TextView widget)406     private int getBottomLine(TextView widget) {
407         return widget.getLayout().getLineForVertical(widget.getScrollY() + getInnerHeight(widget));
408     }
409 
getInnerWidth(TextView widget)410     private int getInnerWidth(TextView widget) {
411         return widget.getWidth() - widget.getTotalPaddingLeft() - widget.getTotalPaddingRight();
412     }
413 
getInnerHeight(TextView widget)414     private int getInnerHeight(TextView widget) {
415         return widget.getHeight() - widget.getTotalPaddingTop() - widget.getTotalPaddingBottom();
416     }
417 
getCharacterWidth(TextView widget)418     private int getCharacterWidth(TextView widget) {
419         return (int) Math.ceil(widget.getPaint().getFontSpacing());
420     }
421 
getScrollBoundsLeft(TextView widget)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 
getScrollBoundsRight(TextView widget)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      */
scrollLeft(TextView widget, Spannable buffer, int amount)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      */
scrollRight(TextView widget, Spannable buffer, int amount)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      */
scrollUp(TextView widget, Spannable buffer, int amount)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      */
scrollDown(TextView widget, Spannable buffer, int amount)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      */
scrollPageUp(TextView widget, Spannable buffer)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      */
scrollPageDown(TextView widget, Spannable buffer)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      */
scrollTop(TextView widget, Spannable buffer)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      */
scrollBottom(TextView widget, Spannable buffer)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      */
scrollLineStart(TextView widget, Spannable buffer)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      */
scrollLineEnd(TextView widget, Spannable buffer)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     }
673 }
674