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 17 package android.text.method; 18 19 import android.text.Layout; 20 import android.text.NoCopySpan; 21 import android.text.Selection; 22 import android.text.Spannable; 23 import android.text.style.ClickableSpan; 24 import android.view.KeyEvent; 25 import android.view.MotionEvent; 26 import android.view.View; 27 import android.widget.TextView; 28 29 /** 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 */ 33 public class LinkMovementMethod extends ScrollingMovementMethod { 34 private static final int CLICK = 1; 35 private static final int UP = 2; 36 private static final int DOWN = 3; 37 38 @Override canSelectArbitrarily()39 public boolean canSelectArbitrarily() { 40 return true; 41 } 42 43 @Override handleMovementKey(TextView widget, Spannable buffer, int keyCode, int movementMetaState, KeyEvent event)44 protected boolean handleMovementKey(TextView widget, Spannable buffer, int keyCode, 45 int movementMetaState, KeyEvent event) { 46 switch (keyCode) { 47 case KeyEvent.KEYCODE_DPAD_CENTER: 48 case KeyEvent.KEYCODE_ENTER: 49 if (KeyEvent.metaStateHasNoModifiers(movementMetaState)) { 50 if (event.getAction() == KeyEvent.ACTION_DOWN && 51 event.getRepeatCount() == 0 && action(CLICK, widget, buffer)) { 52 return true; 53 } 54 } 55 break; 56 } 57 return super.handleMovementKey(widget, buffer, keyCode, movementMetaState, event); 58 } 59 60 @Override up(TextView widget, Spannable buffer)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 down(TextView widget, Spannable buffer)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 left(TextView widget, Spannable buffer)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 right(TextView widget, Spannable buffer)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 action(int what, TextView widget, Spannable buffer)96 private boolean action(int what, TextView widget, Spannable buffer) { 97 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 193 @Override onTouchEvent(TextView widget, Spannable buffer, MotionEvent event)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 233 @Override initialize(TextView widget, Spannable text)234 public void initialize(TextView widget, Spannable text) { 235 Selection.removeSelection(text); 236 text.removeSpan(FROM_BELOW); 237 } 238 239 @Override onTakeFocus(TextView view, Spannable text, int dir)240 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 getInstance()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 } 260