• 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.compat.annotation.UnsupportedAppUsage;
20 import android.os.Build;
21 import android.text.Layout;
22 import android.text.NoCopySpan;
23 import android.text.Selection;
24 import android.text.Spannable;
25 import android.text.style.ClickableSpan;
26 import android.view.KeyEvent;
27 import android.view.MotionEvent;
28 import android.view.View;
29 import android.view.textclassifier.TextLinks.TextLinkSpan;
30 import android.widget.TextView;
31 
32 /**
33  * A movement method that traverses links in the text buffer and scrolls if necessary.
34  * Supports clicking on links with DPad Center or Enter.
35  */
36 @android.ravenwood.annotation.RavenwoodKeepWholeClass
37 public class LinkMovementMethod extends ScrollingMovementMethod {
38     private static final int CLICK = 1;
39     private static final int UP = 2;
40     private static final int DOWN = 3;
41 
42     private static final int HIDE_FLOATING_TOOLBAR_DELAY_MS = 200;
43 
44     @Override
canSelectArbitrarily()45     public boolean canSelectArbitrarily() {
46         return true;
47     }
48 
49     @Override
handleMovementKey(TextView widget, Spannable buffer, int keyCode, int movementMetaState, KeyEvent event)50     protected boolean handleMovementKey(TextView widget, Spannable buffer, int keyCode,
51             int movementMetaState, KeyEvent event) {
52         switch (keyCode) {
53             case KeyEvent.KEYCODE_DPAD_CENTER:
54             case KeyEvent.KEYCODE_ENTER:
55                 if (KeyEvent.metaStateHasNoModifiers(movementMetaState)) {
56                     if (event.getAction() == KeyEvent.ACTION_DOWN &&
57                             event.getRepeatCount() == 0 && action(CLICK, widget, buffer)) {
58                         return true;
59                     }
60                 }
61                 break;
62         }
63         return super.handleMovementKey(widget, buffer, keyCode, movementMetaState, event);
64     }
65 
66     @Override
up(TextView widget, Spannable buffer)67     protected boolean up(TextView widget, Spannable buffer) {
68         if (action(UP, widget, buffer)) {
69             return true;
70         }
71 
72         return super.up(widget, buffer);
73     }
74 
75     @Override
down(TextView widget, Spannable buffer)76     protected boolean down(TextView widget, Spannable buffer) {
77         if (action(DOWN, widget, buffer)) {
78             return true;
79         }
80 
81         return super.down(widget, buffer);
82     }
83 
84     @Override
left(TextView widget, Spannable buffer)85     protected boolean left(TextView widget, Spannable buffer) {
86         if (action(UP, widget, buffer)) {
87             return true;
88         }
89 
90         return super.left(widget, buffer);
91     }
92 
93     @Override
right(TextView widget, Spannable buffer)94     protected boolean right(TextView widget, Spannable buffer) {
95         if (action(DOWN, widget, buffer)) {
96             return true;
97         }
98 
99         return super.right(widget, buffer);
100     }
101 
action(int what, TextView widget, Spannable buffer)102     private boolean action(int what, TextView widget, Spannable buffer) {
103         Layout layout = widget.getLayout();
104         if (widget.isOffsetMappingAvailable()) {
105             // The text in the layout is transformed and has OffsetMapping, don't do anything.
106             return false;
107         }
108 
109         int padding = widget.getTotalPaddingTop() +
110                       widget.getTotalPaddingBottom();
111         int areaTop = widget.getScrollY();
112         int areaBot = areaTop + widget.getHeight() - padding;
113 
114         int lineTop = layout.getLineForVertical(areaTop);
115         int lineBot = layout.getLineForVertical(areaBot);
116 
117         int first = layout.getLineStart(lineTop);
118         int last = layout.getLineEnd(lineBot);
119 
120         ClickableSpan[] candidates = buffer.getSpans(first, last, ClickableSpan.class);
121 
122         int a = Selection.getSelectionStart(buffer);
123         int b = Selection.getSelectionEnd(buffer);
124 
125         int selStart = Math.min(a, b);
126         int selEnd = Math.max(a, b);
127 
128         if (selStart < 0) {
129             if (buffer.getSpanStart(FROM_BELOW) >= 0) {
130                 selStart = selEnd = buffer.length();
131             }
132         }
133 
134         if (selStart > last)
135             selStart = selEnd = Integer.MAX_VALUE;
136         if (selEnd < first)
137             selStart = selEnd = -1;
138 
139         switch (what) {
140             case CLICK:
141                 if (selStart == selEnd) {
142                     return false;
143                 }
144 
145                 ClickableSpan[] links = buffer.getSpans(selStart, selEnd, ClickableSpan.class);
146 
147                 if (links.length != 1) {
148                     return false;
149                 }
150 
151                 ClickableSpan link = links[0];
152                 if (link instanceof TextLinkSpan) {
153                     ((TextLinkSpan) link).onClick(widget, TextLinkSpan.INVOCATION_METHOD_KEYBOARD);
154                 } else {
155                     link.onClick(widget);
156                 }
157                 break;
158 
159             case UP:
160                 int bestStart, bestEnd;
161 
162                 bestStart = -1;
163                 bestEnd = -1;
164 
165                 for (int i = 0; i < candidates.length; i++) {
166                     int end = buffer.getSpanEnd(candidates[i]);
167 
168                     if (end < selEnd || selStart == selEnd) {
169                         if (end > bestEnd) {
170                             bestStart = buffer.getSpanStart(candidates[i]);
171                             bestEnd = end;
172                         }
173                     }
174                 }
175 
176                 if (bestStart >= 0) {
177                     Selection.setSelection(buffer, bestEnd, bestStart);
178                     return true;
179                 }
180 
181                 break;
182 
183             case DOWN:
184                 bestStart = Integer.MAX_VALUE;
185                 bestEnd = Integer.MAX_VALUE;
186 
187                 for (int i = 0; i < candidates.length; i++) {
188                     int start = buffer.getSpanStart(candidates[i]);
189 
190                     if (start > selStart || selStart == selEnd) {
191                         if (start < bestStart) {
192                             bestStart = start;
193                             bestEnd = buffer.getSpanEnd(candidates[i]);
194                         }
195                     }
196                 }
197 
198                 if (bestEnd < Integer.MAX_VALUE) {
199                     Selection.setSelection(buffer, bestStart, bestEnd);
200                     return true;
201                 }
202 
203                 break;
204         }
205 
206         return false;
207     }
208 
209     @Override
onTouchEvent(TextView widget, Spannable buffer, MotionEvent event)210     public boolean onTouchEvent(TextView widget, Spannable buffer,
211                                 MotionEvent event) {
212         int action = event.getAction();
213 
214         if (action == MotionEvent.ACTION_UP || action == MotionEvent.ACTION_DOWN) {
215             int x = (int) event.getX();
216             int y = (int) event.getY();
217 
218             x -= widget.getTotalPaddingLeft();
219             y -= widget.getTotalPaddingTop();
220 
221             x += widget.getScrollX();
222             y += widget.getScrollY();
223 
224             Layout layout = widget.getLayout();
225             ClickableSpan[] links;
226             if (y < 0 || y > layout.getHeight()) {
227                 links = null;
228             } else {
229                 int line = layout.getLineForVertical(y);
230                 if (x < layout.getLineLeft(line) || x > layout.getLineRight(line)) {
231                     links = null;
232                 } else {
233                     int off = layout.getOffsetForHorizontal(line, x);
234                     links = buffer.getSpans(off, off, ClickableSpan.class);
235                 }
236             }
237 
238             if (links != null && links.length != 0) {
239                 ClickableSpan link = links[0];
240                 if (action == MotionEvent.ACTION_UP) {
241                     if (link instanceof TextLinkSpan) {
242                         ((TextLinkSpan) link).onClick(
243                                 widget, TextLinkSpan.INVOCATION_METHOD_TOUCH);
244                     } else {
245                         link.onClick(widget);
246                     }
247                 } else if (action == MotionEvent.ACTION_DOWN) {
248                     if (widget.getContext().getApplicationInfo().targetSdkVersion
249                             >= Build.VERSION_CODES.P) {
250                         // Selection change will reposition the toolbar. Hide it for a few ms for a
251                         // smoother transition.
252                         widget.hideFloatingToolbar(HIDE_FLOATING_TOOLBAR_DELAY_MS);
253                     }
254                     Selection.setSelection(buffer,
255                             buffer.getSpanStart(link),
256                             buffer.getSpanEnd(link));
257                 }
258                 return true;
259             } else {
260                 Selection.removeSelection(buffer);
261             }
262         }
263 
264         return super.onTouchEvent(widget, buffer, event);
265     }
266 
267     @Override
initialize(TextView widget, Spannable text)268     public void initialize(TextView widget, Spannable text) {
269         Selection.removeSelection(text);
270         text.removeSpan(FROM_BELOW);
271     }
272 
273     @Override
onTakeFocus(TextView view, Spannable text, int dir)274     public void onTakeFocus(TextView view, Spannable text, int dir) {
275         Selection.removeSelection(text);
276 
277         if ((dir & View.FOCUS_BACKWARD) != 0) {
278             text.setSpan(FROM_BELOW, 0, 0, Spannable.SPAN_POINT_POINT);
279         } else {
280             text.removeSpan(FROM_BELOW);
281         }
282     }
283 
getInstance()284     public static MovementMethod getInstance() {
285         if (sInstance == null)
286             sInstance = new LinkMovementMethod();
287 
288         return sInstance;
289     }
290 
291     @UnsupportedAppUsage
292     private static LinkMovementMethod sInstance;
293     private static Object FROM_BELOW = new NoCopySpan.Concrete();
294 }
295