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