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