blob: aff233df16a39609bcdcaac1c44edef0f5d5f01a [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
Jeff Brown67b6ab72010-12-17 18:33:02 -080039 protected boolean handleMovementKey(TextView widget, Spannable buffer, int keyCode,
40 int movementMetaState, KeyEvent event) {
The Android Open Source Project9066cfe2009-03-03 19:31:44 -080041 switch (keyCode) {
Jeff Brown67b6ab72010-12-17 18:33:02 -080042 case KeyEvent.KEYCODE_DPAD_CENTER:
43 case KeyEvent.KEYCODE_ENTER:
44 if (KeyEvent.metaStateHasNoModifiers(movementMetaState)) {
Gilles Debunned9ed7952011-01-25 13:07:22 -080045 if (event.getAction() == KeyEvent.ACTION_DOWN &&
46 event.getRepeatCount() == 0 && action(CLICK, widget, buffer)) {
47 return true;
Jeff Brown67b6ab72010-12-17 18:33:02 -080048 }
The Android Open Source Project9066cfe2009-03-03 19:31:44 -080049 }
Jeff Brown67b6ab72010-12-17 18:33:02 -080050 break;
The Android Open Source Project9066cfe2009-03-03 19:31:44 -080051 }
Jeff Brown67b6ab72010-12-17 18:33:02 -080052 return super.handleMovementKey(widget, buffer, keyCode, movementMetaState, event);
The Android Open Source Project9066cfe2009-03-03 19:31:44 -080053 }
54
55 @Override
56 protected boolean up(TextView widget, Spannable buffer) {
57 if (action(UP, widget, buffer)) {
58 return true;
59 }
60
61 return super.up(widget, buffer);
62 }
63
64 @Override
65 protected boolean down(TextView widget, Spannable buffer) {
66 if (action(DOWN, widget, buffer)) {
67 return true;
68 }
69
70 return super.down(widget, buffer);
71 }
72
73 @Override
74 protected boolean left(TextView widget, Spannable buffer) {
75 if (action(UP, widget, buffer)) {
76 return true;
77 }
78
79 return super.left(widget, buffer);
80 }
81
82 @Override
83 protected boolean right(TextView widget, Spannable buffer) {
84 if (action(DOWN, widget, buffer)) {
85 return true;
86 }
87
88 return super.right(widget, buffer);
89 }
90
91 private boolean action(int what, TextView widget, Spannable buffer) {
The Android Open Source Project9066cfe2009-03-03 19:31:44 -080092 Layout layout = widget.getLayout();
93
94 int padding = widget.getTotalPaddingTop() +
95 widget.getTotalPaddingBottom();
96 int areatop = widget.getScrollY();
97 int areabot = areatop + widget.getHeight() - padding;
98
99 int linetop = layout.getLineForVertical(areatop);
100 int linebot = layout.getLineForVertical(areabot);
101
102 int first = layout.getLineStart(linetop);
103 int last = layout.getLineEnd(linebot);
104
105 ClickableSpan[] candidates = buffer.getSpans(first, last, ClickableSpan.class);
106
107 int a = Selection.getSelectionStart(buffer);
108 int b = Selection.getSelectionEnd(buffer);
109
110 int selStart = Math.min(a, b);
111 int selEnd = Math.max(a, b);
112
113 if (selStart < 0) {
114 if (buffer.getSpanStart(FROM_BELOW) >= 0) {
115 selStart = selEnd = buffer.length();
116 }
117 }
118
119 if (selStart > last)
120 selStart = selEnd = Integer.MAX_VALUE;
121 if (selEnd < first)
122 selStart = selEnd = -1;
123
124 switch (what) {
125 case CLICK:
126 if (selStart == selEnd) {
127 return false;
128 }
129
130 ClickableSpan[] link = buffer.getSpans(selStart, selEnd, ClickableSpan.class);
131
132 if (link.length != 1)
133 return false;
134
135 link[0].onClick(widget);
136 break;
137
138 case UP:
139 int beststart, bestend;
140
141 beststart = -1;
142 bestend = -1;
143
144 for (int i = 0; i < candidates.length; i++) {
145 int end = buffer.getSpanEnd(candidates[i]);
146
147 if (end < selEnd || selStart == selEnd) {
148 if (end > bestend) {
149 beststart = buffer.getSpanStart(candidates[i]);
150 bestend = end;
151 }
152 }
153 }
154
155 if (beststart >= 0) {
156 Selection.setSelection(buffer, bestend, beststart);
157 return true;
158 }
159
160 break;
161
162 case DOWN:
163 beststart = Integer.MAX_VALUE;
164 bestend = Integer.MAX_VALUE;
165
166 for (int i = 0; i < candidates.length; i++) {
167 int start = buffer.getSpanStart(candidates[i]);
168
169 if (start > selStart || selStart == selEnd) {
170 if (start < beststart) {
171 beststart = start;
172 bestend = buffer.getSpanEnd(candidates[i]);
173 }
174 }
175 }
176
177 if (bestend < Integer.MAX_VALUE) {
178 Selection.setSelection(buffer, beststart, bestend);
179 return true;
180 }
181
182 break;
183 }
184
185 return false;
186 }
187
The Android Open Source Project9066cfe2009-03-03 19:31:44 -0800188 @Override
189 public boolean onTouchEvent(TextView widget, Spannable buffer,
190 MotionEvent event) {
191 int action = event.getAction();
192
193 if (action == MotionEvent.ACTION_UP ||
194 action == MotionEvent.ACTION_DOWN) {
195 int x = (int) event.getX();
196 int y = (int) event.getY();
197
198 x -= widget.getTotalPaddingLeft();
199 y -= widget.getTotalPaddingTop();
200
201 x += widget.getScrollX();
202 y += widget.getScrollY();
203
204 Layout layout = widget.getLayout();
205 int line = layout.getLineForVertical(y);
206 int off = layout.getOffsetForHorizontal(line, x);
207
208 ClickableSpan[] link = buffer.getSpans(off, off, ClickableSpan.class);
209
210 if (link.length != 0) {
211 if (action == MotionEvent.ACTION_UP) {
212 link[0].onClick(widget);
213 } else if (action == MotionEvent.ACTION_DOWN) {
214 Selection.setSelection(buffer,
215 buffer.getSpanStart(link[0]),
216 buffer.getSpanEnd(link[0]));
217 }
218
219 return true;
220 } else {
221 Selection.removeSelection(buffer);
222 }
223 }
224
225 return super.onTouchEvent(widget, buffer, event);
226 }
227
Jeff Brown67b6ab72010-12-17 18:33:02 -0800228 @Override
The Android Open Source Project9066cfe2009-03-03 19:31:44 -0800229 public void initialize(TextView widget, Spannable text) {
230 Selection.removeSelection(text);
231 text.removeSpan(FROM_BELOW);
232 }
233
Jeff Brown67b6ab72010-12-17 18:33:02 -0800234 @Override
The Android Open Source Project9066cfe2009-03-03 19:31:44 -0800235 public void onTakeFocus(TextView view, Spannable text, int dir) {
236 Selection.removeSelection(text);
237
238 if ((dir & View.FOCUS_BACKWARD) != 0) {
239 text.setSpan(FROM_BELOW, 0, 0, Spannable.SPAN_POINT_POINT);
240 } else {
241 text.removeSpan(FROM_BELOW);
242 }
243 }
244
245 public static MovementMethod getInstance() {
246 if (sInstance == null)
247 sInstance = new LinkMovementMethod();
248
249 return sInstance;
250 }
251
252 private static LinkMovementMethod sInstance;
253 private static Object FROM_BELOW = new NoCopySpan.Concrete();
254}