blob: 3855ff3ae44be3d138045866fc781ecd51b5025e [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
Gilles Debunne75b7a932010-12-21 12:01:37 -080019import android.text.Layout;
20import android.text.NoCopySpan;
21import android.text.Selection;
22import android.text.Spannable;
23import android.text.style.ClickableSpan;
The Android Open Source Project9066cfe2009-03-03 19:31:44 -080024import android.view.KeyEvent;
25import android.view.MotionEvent;
The Android Open Source Project9066cfe2009-03-03 19:31:44 -080026import android.view.View;
27import android.widget.TextView;
28
Jeff Brown67b6ab72010-12-17 18:33:02 -080029/**
30 * A movement method that traverses links in the text buffer and scrolls if necessary.
31 * Supports clicking on links with DPad Center or Enter.
32 */
33public class LinkMovementMethod extends ScrollingMovementMethod {
The Android Open Source Project9066cfe2009-03-03 19:31:44 -080034 private static final int CLICK = 1;
35 private static final int UP = 2;
36 private static final int DOWN = 3;
37
38 @Override
Victoria Leasecd0b0132013-04-29 14:38:21 -070039 public boolean canSelectArbitrarily() {
40 return true;
41 }
42
43 @Override
Jeff Brown67b6ab72010-12-17 18:33:02 -080044 protected boolean handleMovementKey(TextView widget, Spannable buffer, int keyCode,
45 int movementMetaState, KeyEvent event) {
The Android Open Source Project9066cfe2009-03-03 19:31:44 -080046 switch (keyCode) {
Jeff Brown67b6ab72010-12-17 18:33:02 -080047 case KeyEvent.KEYCODE_DPAD_CENTER:
48 case KeyEvent.KEYCODE_ENTER:
49 if (KeyEvent.metaStateHasNoModifiers(movementMetaState)) {
Gilles Debunned9ed7952011-01-25 13:07:22 -080050 if (event.getAction() == KeyEvent.ACTION_DOWN &&
51 event.getRepeatCount() == 0 && action(CLICK, widget, buffer)) {
52 return true;
Jeff Brown67b6ab72010-12-17 18:33:02 -080053 }
The Android Open Source Project9066cfe2009-03-03 19:31:44 -080054 }
Jeff Brown67b6ab72010-12-17 18:33:02 -080055 break;
The Android Open Source Project9066cfe2009-03-03 19:31:44 -080056 }
Jeff Brown67b6ab72010-12-17 18:33:02 -080057 return super.handleMovementKey(widget, buffer, keyCode, movementMetaState, event);
The Android Open Source Project9066cfe2009-03-03 19:31:44 -080058 }
59
60 @Override
61 protected boolean up(TextView widget, Spannable buffer) {
62 if (action(UP, widget, buffer)) {
63 return true;
64 }
65
66 return super.up(widget, buffer);
67 }
68
69 @Override
70 protected boolean down(TextView widget, Spannable buffer) {
71 if (action(DOWN, widget, buffer)) {
72 return true;
73 }
74
75 return super.down(widget, buffer);
76 }
77
78 @Override
79 protected boolean left(TextView widget, Spannable buffer) {
80 if (action(UP, widget, buffer)) {
81 return true;
82 }
83
84 return super.left(widget, buffer);
85 }
86
87 @Override
88 protected boolean right(TextView widget, Spannable buffer) {
89 if (action(DOWN, widget, buffer)) {
90 return true;
91 }
92
93 return super.right(widget, buffer);
94 }
95
96 private boolean action(int what, TextView widget, Spannable buffer) {
The Android Open Source Project9066cfe2009-03-03 19:31:44 -080097 Layout layout = widget.getLayout();
98
99 int padding = widget.getTotalPaddingTop() +
100 widget.getTotalPaddingBottom();
101 int areatop = widget.getScrollY();
102 int areabot = areatop + widget.getHeight() - padding;
103
104 int linetop = layout.getLineForVertical(areatop);
105 int linebot = layout.getLineForVertical(areabot);
106
107 int first = layout.getLineStart(linetop);
108 int last = layout.getLineEnd(linebot);
109
110 ClickableSpan[] candidates = buffer.getSpans(first, last, ClickableSpan.class);
111
112 int a = Selection.getSelectionStart(buffer);
113 int b = Selection.getSelectionEnd(buffer);
114
115 int selStart = Math.min(a, b);
116 int selEnd = Math.max(a, b);
117
118 if (selStart < 0) {
119 if (buffer.getSpanStart(FROM_BELOW) >= 0) {
120 selStart = selEnd = buffer.length();
121 }
122 }
123
124 if (selStart > last)
125 selStart = selEnd = Integer.MAX_VALUE;
126 if (selEnd < first)
127 selStart = selEnd = -1;
128
129 switch (what) {
130 case CLICK:
131 if (selStart == selEnd) {
132 return false;
133 }
134
135 ClickableSpan[] link = buffer.getSpans(selStart, selEnd, ClickableSpan.class);
136
137 if (link.length != 1)
138 return false;
139
140 link[0].onClick(widget);
141 break;
142
143 case UP:
144 int beststart, bestend;
145
146 beststart = -1;
147 bestend = -1;
148
149 for (int i = 0; i < candidates.length; i++) {
150 int end = buffer.getSpanEnd(candidates[i]);
151
152 if (end < selEnd || selStart == selEnd) {
153 if (end > bestend) {
154 beststart = buffer.getSpanStart(candidates[i]);
155 bestend = end;
156 }
157 }
158 }
159
160 if (beststart >= 0) {
161 Selection.setSelection(buffer, bestend, beststart);
162 return true;
163 }
164
165 break;
166
167 case DOWN:
168 beststart = Integer.MAX_VALUE;
169 bestend = Integer.MAX_VALUE;
170
171 for (int i = 0; i < candidates.length; i++) {
172 int start = buffer.getSpanStart(candidates[i]);
173
174 if (start > selStart || selStart == selEnd) {
175 if (start < beststart) {
176 beststart = start;
177 bestend = buffer.getSpanEnd(candidates[i]);
178 }
179 }
180 }
181
182 if (bestend < Integer.MAX_VALUE) {
183 Selection.setSelection(buffer, beststart, bestend);
184 return true;
185 }
186
187 break;
188 }
189
190 return false;
191 }
192
The Android Open Source Project9066cfe2009-03-03 19:31:44 -0800193 @Override
194 public boolean onTouchEvent(TextView widget, Spannable buffer,
195 MotionEvent event) {
196 int action = event.getAction();
197
198 if (action == MotionEvent.ACTION_UP ||
199 action == MotionEvent.ACTION_DOWN) {
200 int x = (int) event.getX();
201 int y = (int) event.getY();
202
203 x -= widget.getTotalPaddingLeft();
204 y -= widget.getTotalPaddingTop();
205
206 x += widget.getScrollX();
207 y += widget.getScrollY();
208
209 Layout layout = widget.getLayout();
210 int line = layout.getLineForVertical(y);
211 int off = layout.getOffsetForHorizontal(line, x);
212
213 ClickableSpan[] link = buffer.getSpans(off, off, ClickableSpan.class);
214
215 if (link.length != 0) {
216 if (action == MotionEvent.ACTION_UP) {
217 link[0].onClick(widget);
218 } else if (action == MotionEvent.ACTION_DOWN) {
219 Selection.setSelection(buffer,
220 buffer.getSpanStart(link[0]),
221 buffer.getSpanEnd(link[0]));
222 }
223
224 return true;
225 } else {
226 Selection.removeSelection(buffer);
227 }
228 }
229
230 return super.onTouchEvent(widget, buffer, event);
231 }
232
Jeff Brown67b6ab72010-12-17 18:33:02 -0800233 @Override
The Android Open Source Project9066cfe2009-03-03 19:31:44 -0800234 public void initialize(TextView widget, Spannable text) {
235 Selection.removeSelection(text);
236 text.removeSpan(FROM_BELOW);
237 }
238
Jeff Brown67b6ab72010-12-17 18:33:02 -0800239 @Override
The Android Open Source Project9066cfe2009-03-03 19:31:44 -0800240 public void onTakeFocus(TextView view, Spannable text, int dir) {
241 Selection.removeSelection(text);
242
243 if ((dir & View.FOCUS_BACKWARD) != 0) {
244 text.setSpan(FROM_BELOW, 0, 0, Spannable.SPAN_POINT_POINT);
245 } else {
246 text.removeSpan(FROM_BELOW);
247 }
248 }
249
250 public static MovementMethod getInstance() {
251 if (sInstance == null)
252 sInstance = new LinkMovementMethod();
253
254 return sInstance;
255 }
256
257 private static LinkMovementMethod sInstance;
258 private static Object FROM_BELOW = new NoCopySpan.Concrete();
259}