blob: 549f8b3953ddb43b9f22bc5b68817feb82aa79f9 [file] [log] [blame]
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08001/*
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
17package android.text.method;
18
Abodunrinwa Toki52096912018-03-21 23:14:42 +000019import android.os.Build;
Gilles Debunne75b7a932010-12-21 12:01:37 -080020import android.text.Layout;
21import android.text.NoCopySpan;
22import android.text.Selection;
23import android.text.Spannable;
24import android.text.style.ClickableSpan;
The Android Open Source Project9066cfe2009-03-03 19:31:44 -080025import android.view.KeyEvent;
26import android.view.MotionEvent;
The Android Open Source Project9066cfe2009-03-03 19:31:44 -080027import android.view.View;
Abodunrinwa Toki33fa3822018-04-16 10:05:16 +010028import android.view.textclassifier.TextLinks.TextLinkSpan;
The Android Open Source Project9066cfe2009-03-03 19:31:44 -080029import android.widget.TextView;
30
Jeff Brown67b6ab72010-12-17 18:33:02 -080031/**
32 * A movement method that traverses links in the text buffer and scrolls if necessary.
33 * Supports clicking on links with DPad Center or Enter.
34 */
35public class LinkMovementMethod extends ScrollingMovementMethod {
The Android Open Source Project9066cfe2009-03-03 19:31:44 -080036 private static final int CLICK = 1;
37 private static final int UP = 2;
38 private static final int DOWN = 3;
39
Abodunrinwa Toki52096912018-03-21 23:14:42 +000040 private static final int HIDE_FLOATING_TOOLBAR_DELAY_MS = 200;
41
The Android Open Source Project9066cfe2009-03-03 19:31:44 -080042 @Override
Victoria Leasecd0b0132013-04-29 14:38:21 -070043 public boolean canSelectArbitrarily() {
44 return true;
45 }
46
47 @Override
Jeff Brown67b6ab72010-12-17 18:33:02 -080048 protected boolean handleMovementKey(TextView widget, Spannable buffer, int keyCode,
49 int movementMetaState, KeyEvent event) {
The Android Open Source Project9066cfe2009-03-03 19:31:44 -080050 switch (keyCode) {
Jeff Brown67b6ab72010-12-17 18:33:02 -080051 case KeyEvent.KEYCODE_DPAD_CENTER:
52 case KeyEvent.KEYCODE_ENTER:
53 if (KeyEvent.metaStateHasNoModifiers(movementMetaState)) {
Gilles Debunned9ed7952011-01-25 13:07:22 -080054 if (event.getAction() == KeyEvent.ACTION_DOWN &&
55 event.getRepeatCount() == 0 && action(CLICK, widget, buffer)) {
56 return true;
Jeff Brown67b6ab72010-12-17 18:33:02 -080057 }
The Android Open Source Project9066cfe2009-03-03 19:31:44 -080058 }
Jeff Brown67b6ab72010-12-17 18:33:02 -080059 break;
The Android Open Source Project9066cfe2009-03-03 19:31:44 -080060 }
Jeff Brown67b6ab72010-12-17 18:33:02 -080061 return super.handleMovementKey(widget, buffer, keyCode, movementMetaState, event);
The Android Open Source Project9066cfe2009-03-03 19:31:44 -080062 }
63
64 @Override
65 protected boolean up(TextView widget, Spannable buffer) {
66 if (action(UP, widget, buffer)) {
67 return true;
68 }
69
70 return super.up(widget, buffer);
71 }
Abodunrinwa Toki52096912018-03-21 23:14:42 +000072
The Android Open Source Project9066cfe2009-03-03 19:31:44 -080073 @Override
74 protected boolean down(TextView widget, Spannable buffer) {
75 if (action(DOWN, widget, buffer)) {
76 return true;
77 }
78
79 return super.down(widget, buffer);
80 }
81
82 @Override
83 protected boolean left(TextView widget, Spannable buffer) {
84 if (action(UP, widget, buffer)) {
85 return true;
86 }
87
88 return super.left(widget, buffer);
89 }
90
91 @Override
92 protected boolean right(TextView widget, Spannable buffer) {
93 if (action(DOWN, widget, buffer)) {
94 return true;
95 }
96
97 return super.right(widget, buffer);
98 }
99
100 private boolean action(int what, TextView widget, Spannable buffer) {
The Android Open Source Project9066cfe2009-03-03 19:31:44 -0800101 Layout layout = widget.getLayout();
102
103 int padding = widget.getTotalPaddingTop() +
104 widget.getTotalPaddingBottom();
Andrei Stingaceanu3df24c32016-08-01 17:15:07 +0100105 int areaTop = widget.getScrollY();
106 int areaBot = areaTop + widget.getHeight() - padding;
The Android Open Source Project9066cfe2009-03-03 19:31:44 -0800107
Andrei Stingaceanu3df24c32016-08-01 17:15:07 +0100108 int lineTop = layout.getLineForVertical(areaTop);
109 int lineBot = layout.getLineForVertical(areaBot);
The Android Open Source Project9066cfe2009-03-03 19:31:44 -0800110
Andrei Stingaceanu3df24c32016-08-01 17:15:07 +0100111 int first = layout.getLineStart(lineTop);
112 int last = layout.getLineEnd(lineBot);
The Android Open Source Project9066cfe2009-03-03 19:31:44 -0800113
114 ClickableSpan[] candidates = buffer.getSpans(first, last, ClickableSpan.class);
115
116 int a = Selection.getSelectionStart(buffer);
117 int b = Selection.getSelectionEnd(buffer);
118
119 int selStart = Math.min(a, b);
120 int selEnd = Math.max(a, b);
121
122 if (selStart < 0) {
123 if (buffer.getSpanStart(FROM_BELOW) >= 0) {
124 selStart = selEnd = buffer.length();
125 }
126 }
127
128 if (selStart > last)
129 selStart = selEnd = Integer.MAX_VALUE;
130 if (selEnd < first)
131 selStart = selEnd = -1;
132
133 switch (what) {
Abodunrinwa Toki33fa3822018-04-16 10:05:16 +0100134 case CLICK:
135 if (selStart == selEnd) {
136 return false;
137 }
The Android Open Source Project9066cfe2009-03-03 19:31:44 -0800138
Abodunrinwa Toki33fa3822018-04-16 10:05:16 +0100139 ClickableSpan[] links = buffer.getSpans(selStart, selEnd, ClickableSpan.class);
The Android Open Source Project9066cfe2009-03-03 19:31:44 -0800140
Abodunrinwa Toki33fa3822018-04-16 10:05:16 +0100141 if (links.length != 1) {
142 return false;
143 }
The Android Open Source Project9066cfe2009-03-03 19:31:44 -0800144
Abodunrinwa Toki33fa3822018-04-16 10:05:16 +0100145 ClickableSpan link = links[0];
146 if (link instanceof TextLinkSpan) {
147 ((TextLinkSpan) link).onClick(widget, TextLinkSpan.INVOCATION_METHOD_KEYBOARD);
148 } else {
149 link.onClick(widget);
150 }
151 break;
The Android Open Source Project9066cfe2009-03-03 19:31:44 -0800152
Abodunrinwa Toki33fa3822018-04-16 10:05:16 +0100153 case UP:
154 int bestStart, bestEnd;
The Android Open Source Project9066cfe2009-03-03 19:31:44 -0800155
Abodunrinwa Toki33fa3822018-04-16 10:05:16 +0100156 bestStart = -1;
157 bestEnd = -1;
The Android Open Source Project9066cfe2009-03-03 19:31:44 -0800158
Abodunrinwa Toki33fa3822018-04-16 10:05:16 +0100159 for (int i = 0; i < candidates.length; i++) {
160 int end = buffer.getSpanEnd(candidates[i]);
The Android Open Source Project9066cfe2009-03-03 19:31:44 -0800161
Abodunrinwa Toki33fa3822018-04-16 10:05:16 +0100162 if (end < selEnd || selStart == selEnd) {
163 if (end > bestEnd) {
164 bestStart = buffer.getSpanStart(candidates[i]);
165 bestEnd = end;
166 }
The Android Open Source Project9066cfe2009-03-03 19:31:44 -0800167 }
168 }
The Android Open Source Project9066cfe2009-03-03 19:31:44 -0800169
Abodunrinwa Toki33fa3822018-04-16 10:05:16 +0100170 if (bestStart >= 0) {
171 Selection.setSelection(buffer, bestEnd, bestStart);
172 return true;
173 }
The Android Open Source Project9066cfe2009-03-03 19:31:44 -0800174
Abodunrinwa Toki33fa3822018-04-16 10:05:16 +0100175 break;
The Android Open Source Project9066cfe2009-03-03 19:31:44 -0800176
Abodunrinwa Toki33fa3822018-04-16 10:05:16 +0100177 case DOWN:
178 bestStart = Integer.MAX_VALUE;
179 bestEnd = Integer.MAX_VALUE;
The Android Open Source Project9066cfe2009-03-03 19:31:44 -0800180
Abodunrinwa Toki33fa3822018-04-16 10:05:16 +0100181 for (int i = 0; i < candidates.length; i++) {
182 int start = buffer.getSpanStart(candidates[i]);
The Android Open Source Project9066cfe2009-03-03 19:31:44 -0800183
Abodunrinwa Toki33fa3822018-04-16 10:05:16 +0100184 if (start > selStart || selStart == selEnd) {
185 if (start < bestStart) {
186 bestStart = start;
187 bestEnd = buffer.getSpanEnd(candidates[i]);
188 }
The Android Open Source Project9066cfe2009-03-03 19:31:44 -0800189 }
190 }
The Android Open Source Project9066cfe2009-03-03 19:31:44 -0800191
Abodunrinwa Toki33fa3822018-04-16 10:05:16 +0100192 if (bestEnd < Integer.MAX_VALUE) {
193 Selection.setSelection(buffer, bestStart, bestEnd);
194 return true;
195 }
The Android Open Source Project9066cfe2009-03-03 19:31:44 -0800196
Abodunrinwa Toki33fa3822018-04-16 10:05:16 +0100197 break;
The Android Open Source Project9066cfe2009-03-03 19:31:44 -0800198 }
199
200 return false;
201 }
202
The Android Open Source Project9066cfe2009-03-03 19:31:44 -0800203 @Override
204 public boolean onTouchEvent(TextView widget, Spannable buffer,
205 MotionEvent event) {
206 int action = event.getAction();
207
Andrei Stingaceanud834c582017-02-10 12:55:01 +0000208 if (action == MotionEvent.ACTION_UP || action == MotionEvent.ACTION_DOWN) {
The Android Open Source Project9066cfe2009-03-03 19:31:44 -0800209 int x = (int) event.getX();
210 int y = (int) event.getY();
211
212 x -= widget.getTotalPaddingLeft();
213 y -= widget.getTotalPaddingTop();
214
215 x += widget.getScrollX();
216 y += widget.getScrollY();
217
218 Layout layout = widget.getLayout();
219 int line = layout.getLineForVertical(y);
220 int off = layout.getOffsetForHorizontal(line, x);
221
Andrei Stingaceanu3df24c32016-08-01 17:15:07 +0100222 ClickableSpan[] links = buffer.getSpans(off, off, ClickableSpan.class);
The Android Open Source Project9066cfe2009-03-03 19:31:44 -0800223
Andrei Stingaceanu3df24c32016-08-01 17:15:07 +0100224 if (links.length != 0) {
Abodunrinwa Toki33fa3822018-04-16 10:05:16 +0100225 ClickableSpan link = links[0];
Andrei Stingaceanud834c582017-02-10 12:55:01 +0000226 if (action == MotionEvent.ACTION_UP) {
Abodunrinwa Toki33fa3822018-04-16 10:05:16 +0100227 if (link instanceof TextLinkSpan) {
228 ((TextLinkSpan) link).onClick(
229 widget, TextLinkSpan.INVOCATION_METHOD_TOUCH);
230 } else {
231 link.onClick(widget);
232 }
Andrei Stingaceanud834c582017-02-10 12:55:01 +0000233 } else if (action == MotionEvent.ACTION_DOWN) {
Abodunrinwa Toki52096912018-03-21 23:14:42 +0000234 if (widget.getContext().getApplicationInfo().targetSdkVersion
Jeff Sharkeyaa1a9112018-04-10 15:18:12 -0600235 >= Build.VERSION_CODES.P) {
Abodunrinwa Toki52096912018-03-21 23:14:42 +0000236 // Selection change will reposition the toolbar. Hide it for a few ms for a
237 // smoother transition.
238 widget.hideFloatingToolbar(HIDE_FLOATING_TOOLBAR_DELAY_MS);
239 }
Andrei Stingaceanud834c582017-02-10 12:55:01 +0000240 Selection.setSelection(buffer,
Abodunrinwa Toki33fa3822018-04-16 10:05:16 +0100241 buffer.getSpanStart(link),
242 buffer.getSpanEnd(link));
Andrei Stingaceanud834c582017-02-10 12:55:01 +0000243 }
The Android Open Source Project9066cfe2009-03-03 19:31:44 -0800244 return true;
245 } else {
246 Selection.removeSelection(buffer);
247 }
248 }
249
250 return super.onTouchEvent(widget, buffer, event);
251 }
252
Jeff Brown67b6ab72010-12-17 18:33:02 -0800253 @Override
The Android Open Source Project9066cfe2009-03-03 19:31:44 -0800254 public void initialize(TextView widget, Spannable text) {
255 Selection.removeSelection(text);
256 text.removeSpan(FROM_BELOW);
257 }
258
Jeff Brown67b6ab72010-12-17 18:33:02 -0800259 @Override
The Android Open Source Project9066cfe2009-03-03 19:31:44 -0800260 public void onTakeFocus(TextView view, Spannable text, int dir) {
261 Selection.removeSelection(text);
262
263 if ((dir & View.FOCUS_BACKWARD) != 0) {
264 text.setSpan(FROM_BELOW, 0, 0, Spannable.SPAN_POINT_POINT);
265 } else {
266 text.removeSpan(FROM_BELOW);
267 }
268 }
269
270 public static MovementMethod getInstance() {
271 if (sInstance == null)
272 sInstance = new LinkMovementMethod();
273
274 return sInstance;
275 }
276
277 private static LinkMovementMethod sInstance;
278 private static Object FROM_BELOW = new NoCopySpan.Concrete();
279}