1 /*
2  * Copyright (C) 2006 The Android Open Source Project
3  *
4  * Licensed under the Apache License, Version 2.0 (the "License");
5  * you may not use this file except in compliance with the License.
6  * You may obtain a copy of the License at
7  *
8  *      http://www.apache.org/licenses/LICENSE-2.0
9  *
10  * Unless required by applicable law or agreed to in writing, software
11  * distributed under the License is distributed on an "AS IS" BASIS,
12  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13  * See the License for the specific language governing permissions and
14  * limitations under the License.
15  */
16 
17 package android.text.method;
18 
19 import android.annotation.NonNull;
20 import android.graphics.Rect;
21 import android.text.Layout;
22 import android.text.Selection;
23 import android.text.Spannable;
24 import android.view.KeyEvent;
25 import android.view.MotionEvent;
26 import android.view.View;
27 import android.widget.TextView;
28 
29 /**
30  * A movement method that provides cursor movement and selection.
31  * Supports displaying the context menu on DPad Center.
32  */
33 public class ArrowKeyMovementMethod extends BaseMovementMethod implements MovementMethod {
isSelecting(Spannable buffer)34     private static boolean isSelecting(Spannable buffer) {
35         return ((MetaKeyKeyListener.getMetaState(buffer, MetaKeyKeyListener.META_SHIFT_ON) == 1) ||
36                 (MetaKeyKeyListener.getMetaState(buffer, MetaKeyKeyListener.META_SELECTING) != 0));
37     }
38 
getCurrentLineTop(Spannable buffer, Layout layout)39     private static int getCurrentLineTop(Spannable buffer, Layout layout) {
40         return layout.getLineTop(layout.getLineForOffset(Selection.getSelectionEnd(buffer)));
41     }
42 
getPageHeight(TextView widget)43     private static int getPageHeight(TextView widget) {
44         // This calculation does not take into account the view transformations that
45         // may have been applied to the child or its containers.  In case of scaling or
46         // rotation, the calculated page height may be incorrect.
47         final Rect rect = new Rect();
48         return widget.getGlobalVisibleRect(rect) ? rect.height() : 0;
49     }
50 
51     @Override
handleMovementKey(TextView widget, Spannable buffer, int keyCode, int movementMetaState, KeyEvent event)52     protected boolean handleMovementKey(TextView widget, Spannable buffer, int keyCode,
53             int movementMetaState, KeyEvent event) {
54         switch (keyCode) {
55             case KeyEvent.KEYCODE_DPAD_CENTER:
56                 if (KeyEvent.metaStateHasNoModifiers(movementMetaState)) {
57                     if (event.getAction() == KeyEvent.ACTION_DOWN
58                             && event.getRepeatCount() == 0
59                             && MetaKeyKeyListener.getMetaState(buffer,
60                                         MetaKeyKeyListener.META_SELECTING, event) != 0) {
61                         return widget.showContextMenu();
62                     }
63                 }
64                 break;
65         }
66         return super.handleMovementKey(widget, buffer, keyCode, movementMetaState, event);
67     }
68 
69     @Override
left(TextView widget, Spannable buffer)70     protected boolean left(TextView widget, Spannable buffer) {
71         if (widget.isOffsetMappingAvailable()) {
72             return false;
73         }
74         final Layout layout = widget.getLayout();
75         if (isSelecting(buffer)) {
76             return Selection.extendLeft(buffer, layout);
77         } else {
78             return Selection.moveLeft(buffer, layout);
79         }
80     }
81 
82     @Override
right(TextView widget, Spannable buffer)83     protected boolean right(TextView widget, Spannable buffer) {
84         if (widget.isOffsetMappingAvailable()) {
85             return false;
86         }
87         final Layout layout = widget.getLayout();
88         if (isSelecting(buffer)) {
89             return Selection.extendRight(buffer, layout);
90         } else {
91             return Selection.moveRight(buffer, layout);
92         }
93     }
94 
95     @Override
up(TextView widget, Spannable buffer)96     protected boolean up(TextView widget, Spannable buffer) {
97         if (widget.isOffsetMappingAvailable()) {
98             return false;
99         }
100         final Layout layout = widget.getLayout();
101         if (isSelecting(buffer)) {
102             return Selection.extendUp(buffer, layout);
103         } else {
104             return Selection.moveUp(buffer, layout);
105         }
106     }
107 
108     @Override
down(TextView widget, Spannable buffer)109     protected boolean down(TextView widget, Spannable buffer) {
110         if (widget.isOffsetMappingAvailable()) {
111             return false;
112         }
113         final Layout layout = widget.getLayout();
114         if (isSelecting(buffer)) {
115             return Selection.extendDown(buffer, layout);
116         } else {
117             return Selection.moveDown(buffer, layout);
118         }
119     }
120 
121     @Override
pageUp(TextView widget, Spannable buffer)122     protected boolean pageUp(TextView widget, Spannable buffer) {
123         if (widget.isOffsetMappingAvailable()) {
124             return false;
125         }
126         final Layout layout = widget.getLayout();
127         final boolean selecting = isSelecting(buffer);
128         final int targetY = getCurrentLineTop(buffer, layout) - getPageHeight(widget);
129         boolean handled = false;
130         for (;;) {
131             final int previousSelectionEnd = Selection.getSelectionEnd(buffer);
132             if (selecting) {
133                 Selection.extendUp(buffer, layout);
134             } else {
135                 Selection.moveUp(buffer, layout);
136             }
137             if (Selection.getSelectionEnd(buffer) == previousSelectionEnd) {
138                 break;
139             }
140             handled = true;
141             if (getCurrentLineTop(buffer, layout) <= targetY) {
142                 break;
143             }
144         }
145         return handled;
146     }
147 
148     @Override
pageDown(TextView widget, Spannable buffer)149     protected boolean pageDown(TextView widget, Spannable buffer) {
150         if (widget.isOffsetMappingAvailable()) {
151             return false;
152         }
153         final Layout layout = widget.getLayout();
154         final boolean selecting = isSelecting(buffer);
155         final int targetY = getCurrentLineTop(buffer, layout) + getPageHeight(widget);
156         boolean handled = false;
157         for (;;) {
158             final int previousSelectionEnd = Selection.getSelectionEnd(buffer);
159             if (selecting) {
160                 Selection.extendDown(buffer, layout);
161             } else {
162                 Selection.moveDown(buffer, layout);
163             }
164             if (Selection.getSelectionEnd(buffer) == previousSelectionEnd) {
165                 break;
166             }
167             handled = true;
168             if (getCurrentLineTop(buffer, layout) >= targetY) {
169                 break;
170             }
171         }
172         return handled;
173     }
174 
175     @Override
top(TextView widget, Spannable buffer)176     protected boolean top(TextView widget, Spannable buffer) {
177         if (isSelecting(buffer)) {
178             Selection.extendSelection(buffer, 0);
179         } else {
180             Selection.setSelection(buffer, 0);
181         }
182         return true;
183     }
184 
185     @Override
bottom(TextView widget, Spannable buffer)186     protected boolean bottom(TextView widget, Spannable buffer) {
187         if (isSelecting(buffer)) {
188             Selection.extendSelection(buffer, buffer.length());
189         } else {
190             Selection.setSelection(buffer, buffer.length());
191         }
192         return true;
193     }
194 
195     @Override
lineStart(TextView widget, Spannable buffer)196     protected boolean lineStart(TextView widget, Spannable buffer) {
197         if (widget.isOffsetMappingAvailable()) {
198             return false;
199         }
200         final Layout layout = widget.getLayout();
201         if (isSelecting(buffer)) {
202             return Selection.extendToLeftEdge(buffer, layout);
203         } else {
204             return Selection.moveToLeftEdge(buffer, layout);
205         }
206     }
207 
208     @Override
lineEnd(TextView widget, Spannable buffer)209     protected boolean lineEnd(TextView widget, Spannable buffer) {
210         if (widget.isOffsetMappingAvailable()) {
211             return false;
212         }
213         final Layout layout = widget.getLayout();
214         if (isSelecting(buffer)) {
215             return Selection.extendToRightEdge(buffer, layout);
216         } else {
217             return Selection.moveToRightEdge(buffer, layout);
218         }
219     }
220 
221     /** {@hide} */
222     @Override
leftWord(TextView widget, Spannable buffer)223     protected boolean leftWord(TextView widget, Spannable buffer) {
224         final int selectionEnd = widget.getSelectionEnd();
225         final WordIterator wordIterator = widget.getWordIterator();
226         wordIterator.setCharSequence(buffer, selectionEnd, selectionEnd);
227         return Selection.moveToPreceding(buffer, wordIterator, isSelecting(buffer));
228     }
229 
230     /** {@hide} */
231     @Override
rightWord(TextView widget, Spannable buffer)232     protected boolean rightWord(TextView widget, Spannable buffer) {
233         final int selectionEnd = widget.getSelectionEnd();
234         final WordIterator wordIterator = widget.getWordIterator();
235         wordIterator.setCharSequence(buffer, selectionEnd, selectionEnd);
236         return Selection.moveToFollowing(buffer, wordIterator, isSelecting(buffer));
237     }
238 
239     @Override
home(TextView widget, Spannable buffer)240     protected boolean home(TextView widget, Spannable buffer) {
241         return lineStart(widget, buffer);
242     }
243 
244     @Override
end(TextView widget, Spannable buffer)245     protected boolean end(TextView widget, Spannable buffer) {
246         return lineEnd(widget, buffer);
247     }
248 
249     @Override
previousParagraph(@onNull TextView widget, @NonNull Spannable buffer)250     public boolean previousParagraph(@NonNull TextView widget, @NonNull Spannable buffer) {
251         if (widget.isOffsetMappingAvailable()) {
252             return false;
253         }
254         final Layout layout = widget.getLayout();
255         if (isSelecting(buffer)) {
256             return Selection.extendToParagraphStart(buffer);
257         } else {
258             return Selection.moveToParagraphStart(buffer, layout);
259         }
260     }
261 
262     @Override
nextParagraph(@onNull TextView widget, @NonNull Spannable buffer)263     public boolean nextParagraph(@NonNull TextView widget, @NonNull  Spannable buffer) {
264         if (widget.isOffsetMappingAvailable()) {
265             return false;
266         }
267         final Layout layout = widget.getLayout();
268         if (isSelecting(buffer)) {
269             return Selection.extendToParagraphEnd(buffer);
270         } else {
271             return Selection.moveToParagraphEnd(buffer, layout);
272         }
273     }
274 
275     @Override
onTouchEvent(TextView widget, Spannable buffer, MotionEvent event)276     public boolean onTouchEvent(TextView widget, Spannable buffer, MotionEvent event) {
277         int initialScrollX = -1;
278         int initialScrollY = -1;
279         final int action = event.getAction();
280 
281         if (action == MotionEvent.ACTION_UP) {
282             initialScrollX = Touch.getInitialScrollX(widget, buffer);
283             initialScrollY = Touch.getInitialScrollY(widget, buffer);
284         }
285 
286         boolean wasTouchSelecting = isSelecting(buffer);
287         boolean handled = Touch.onTouchEvent(widget, buffer, event);
288 
289         if (widget.didTouchFocusSelect()) {
290             return handled;
291         }
292         if (action == MotionEvent.ACTION_DOWN) {
293             // For touch events, the code should run only when selection is active.
294             if (isSelecting(buffer)) {
295                 if (!widget.isFocused()) {
296                     if (!widget.requestFocus()) {
297                         return handled;
298                     }
299                 }
300                 int offset = widget.getOffsetForPosition(event.getX(), event.getY());
301                 buffer.setSpan(LAST_TAP_DOWN, offset, offset, Spannable.SPAN_POINT_POINT);
302                 // Disallow intercepting of the touch events, so that
303                 // users can scroll and select at the same time.
304                 // without this, users would get booted out of select
305                 // mode once the view detected it needed to scroll.
306                 widget.getParent().requestDisallowInterceptTouchEvent(true);
307             }
308         } else if (widget.isFocused()) {
309             if (action == MotionEvent.ACTION_MOVE) {
310                 if (isSelecting(buffer) && handled) {
311                     final int startOffset = buffer.getSpanStart(LAST_TAP_DOWN);
312                     // Before selecting, make sure we've moved out of the "slop".
313                     // handled will be true, if we're in select mode AND we're
314                     // OUT of the slop
315 
316                     // Turn long press off while we're selecting. User needs to
317                     // re-tap on the selection to enable long press
318                     widget.cancelLongPress();
319 
320                     // Update selection as we're moving the selection area.
321 
322                     // Get the current touch position
323                     final int offset = widget.getOffsetForPosition(event.getX(), event.getY());
324                     Selection.setSelection(buffer, Math.min(startOffset, offset),
325                             Math.max(startOffset, offset));
326                     return true;
327                 }
328             } else if (action == MotionEvent.ACTION_UP) {
329                 // If we have scrolled, then the up shouldn't move the cursor,
330                 // but we do need to make sure the cursor is still visible at
331                 // the current scroll offset to avoid the scroll jumping later
332                 // to show it.
333                 if ((initialScrollY >= 0 && initialScrollY != widget.getScrollY()) ||
334                     (initialScrollX >= 0 && initialScrollX != widget.getScrollX())) {
335                     widget.moveCursorToVisibleOffset();
336                     return true;
337                 }
338 
339                 if (wasTouchSelecting) {
340                     final int startOffset = buffer.getSpanStart(LAST_TAP_DOWN);
341                     final int endOffset = widget.getOffsetForPosition(event.getX(), event.getY());
342                     Selection.setSelection(buffer, Math.min(startOffset, endOffset),
343                             Math.max(startOffset, endOffset));
344                     buffer.removeSpan(LAST_TAP_DOWN);
345                 }
346 
347                 MetaKeyKeyListener.adjustMetaAfterKeypress(buffer);
348                 MetaKeyKeyListener.resetLockedMeta(buffer);
349 
350                 return true;
351             }
352         }
353         return handled;
354     }
355 
356     @Override
canSelectArbitrarily()357     public boolean canSelectArbitrarily() {
358         return true;
359     }
360 
361     @Override
initialize(TextView widget, Spannable text)362     public void initialize(TextView widget, Spannable text) {
363         Selection.setSelection(text, 0);
364     }
365 
366     @Override
onTakeFocus(TextView view, Spannable text, int dir)367     public void onTakeFocus(TextView view, Spannable text, int dir) {
368         if ((dir & (View.FOCUS_FORWARD | View.FOCUS_DOWN)) != 0) {
369             if (view.getLayout() == null) {
370                 // This shouldn't be null, but do something sensible if it is.
371                 Selection.setSelection(text, text.length());
372             }
373         } else {
374             Selection.setSelection(text, text.length());
375         }
376     }
377 
getInstance()378     public static MovementMethod getInstance() {
379         if (sInstance == null) {
380             sInstance = new ArrowKeyMovementMethod();
381         }
382 
383         return sInstance;
384     }
385 
386     private static final Object LAST_TAP_DOWN = new Object();
387     private static ArrowKeyMovementMethod sInstance;
388 }
389