LinkMovementMethod.java revision 33fa382b8fd453a2f972092b2da1b38d4fd3e47f
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.os.Build;
20import android.text.Layout;
21import android.text.NoCopySpan;
22import android.text.Selection;
23import android.text.Spannable;
24import android.text.style.ClickableSpan;
25import android.view.KeyEvent;
26import android.view.MotionEvent;
27import android.view.View;
28import android.view.textclassifier.TextLinks.TextLinkSpan;
29import android.widget.TextView;
30
31/**
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 {
36    private static final int CLICK = 1;
37    private static final int UP = 2;
38    private static final int DOWN = 3;
39
40    private static final int HIDE_FLOATING_TOOLBAR_DELAY_MS = 200;
41
42    @Override
43    public boolean canSelectArbitrarily() {
44        return true;
45    }
46
47    @Override
48    protected boolean handleMovementKey(TextView widget, Spannable buffer, int keyCode,
49            int movementMetaState, KeyEvent event) {
50        switch (keyCode) {
51            case KeyEvent.KEYCODE_DPAD_CENTER:
52            case KeyEvent.KEYCODE_ENTER:
53                if (KeyEvent.metaStateHasNoModifiers(movementMetaState)) {
54                    if (event.getAction() == KeyEvent.ACTION_DOWN &&
55                            event.getRepeatCount() == 0 && action(CLICK, widget, buffer)) {
56                        return true;
57                    }
58                }
59                break;
60        }
61        return super.handleMovementKey(widget, buffer, keyCode, movementMetaState, event);
62    }
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    }
72
73    @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) {
101        Layout layout = widget.getLayout();
102
103        int padding = widget.getTotalPaddingTop() +
104                      widget.getTotalPaddingBottom();
105        int areaTop = widget.getScrollY();
106        int areaBot = areaTop + widget.getHeight() - padding;
107
108        int lineTop = layout.getLineForVertical(areaTop);
109        int lineBot = layout.getLineForVertical(areaBot);
110
111        int first = layout.getLineStart(lineTop);
112        int last = layout.getLineEnd(lineBot);
113
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) {
134            case CLICK:
135                if (selStart == selEnd) {
136                    return false;
137                }
138
139                ClickableSpan[] links = buffer.getSpans(selStart, selEnd, ClickableSpan.class);
140
141                if (links.length != 1) {
142                    return false;
143                }
144
145                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;
152
153            case UP:
154                int bestStart, bestEnd;
155
156                bestStart = -1;
157                bestEnd = -1;
158
159                for (int i = 0; i < candidates.length; i++) {
160                    int end = buffer.getSpanEnd(candidates[i]);
161
162                    if (end < selEnd || selStart == selEnd) {
163                        if (end > bestEnd) {
164                            bestStart = buffer.getSpanStart(candidates[i]);
165                            bestEnd = end;
166                        }
167                    }
168                }
169
170                if (bestStart >= 0) {
171                    Selection.setSelection(buffer, bestEnd, bestStart);
172                    return true;
173                }
174
175                break;
176
177            case DOWN:
178                bestStart = Integer.MAX_VALUE;
179                bestEnd = Integer.MAX_VALUE;
180
181                for (int i = 0; i < candidates.length; i++) {
182                    int start = buffer.getSpanStart(candidates[i]);
183
184                    if (start > selStart || selStart == selEnd) {
185                        if (start < bestStart) {
186                            bestStart = start;
187                            bestEnd = buffer.getSpanEnd(candidates[i]);
188                        }
189                    }
190                }
191
192                if (bestEnd < Integer.MAX_VALUE) {
193                    Selection.setSelection(buffer, bestStart, bestEnd);
194                    return true;
195                }
196
197                break;
198        }
199
200        return false;
201    }
202
203    @Override
204    public boolean onTouchEvent(TextView widget, Spannable buffer,
205                                MotionEvent event) {
206        int action = event.getAction();
207
208        if (action == MotionEvent.ACTION_UP || action == MotionEvent.ACTION_DOWN) {
209            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
222            ClickableSpan[] links = buffer.getSpans(off, off, ClickableSpan.class);
223
224            if (links.length != 0) {
225                ClickableSpan link = links[0];
226                if (action == MotionEvent.ACTION_UP) {
227                    if (link instanceof TextLinkSpan) {
228                        ((TextLinkSpan) link).onClick(
229                                widget, TextLinkSpan.INVOCATION_METHOD_TOUCH);
230                    } else {
231                        link.onClick(widget);
232                    }
233                } else if (action == MotionEvent.ACTION_DOWN) {
234                    if (widget.getContext().getApplicationInfo().targetSdkVersion
235                            > Build.VERSION_CODES.O_MR1) {
236                        // 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                    }
240                    Selection.setSelection(buffer,
241                            buffer.getSpanStart(link),
242                            buffer.getSpanEnd(link));
243                }
244                return true;
245            } else {
246                Selection.removeSelection(buffer);
247            }
248        }
249
250        return super.onTouchEvent(widget, buffer, event);
251    }
252
253    @Override
254    public void initialize(TextView widget, Spannable text) {
255        Selection.removeSelection(text);
256        text.removeSpan(FROM_BELOW);
257    }
258
259    @Override
260    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}
280