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