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