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.text.Layout; 20import android.text.NoCopySpan; 21import android.text.Selection; 22import android.text.Spannable; 23import android.text.style.ClickableSpan; 24import android.view.KeyEvent; 25import android.view.MotionEvent; 26import android.view.View; 27import 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 */ 33public 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 39 protected boolean handleMovementKey(TextView widget, Spannable buffer, int keyCode, 40 int movementMetaState, KeyEvent event) { 41 switch (keyCode) { 42 case KeyEvent.KEYCODE_DPAD_CENTER: 43 case KeyEvent.KEYCODE_ENTER: 44 if (KeyEvent.metaStateHasNoModifiers(movementMetaState)) { 45 if (event.getAction() == KeyEvent.ACTION_DOWN && 46 event.getRepeatCount() == 0 && action(CLICK, widget, buffer)) { 47 return true; 48 } 49 } 50 break; 51 } 52 return super.handleMovementKey(widget, buffer, keyCode, movementMetaState, event); 53 } 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) { 92 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 188 @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 228 @Override 229 public void initialize(TextView widget, Spannable text) { 230 Selection.removeSelection(text); 231 text.removeSpan(FROM_BELOW); 232 } 233 234 @Override 235 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} 255