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