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 104 int padding = widget.getTotalPaddingTop() + 105 widget.getTotalPaddingBottom(); 106 int areaTop = widget.getScrollY(); 107 int areaBot = areaTop + widget.getHeight() - padding; 108 109 int lineTop = layout.getLineForVertical(areaTop); 110 int lineBot = layout.getLineForVertical(areaBot); 111 112 int first = layout.getLineStart(lineTop); 113 int last = layout.getLineEnd(lineBot); 114 115 ClickableSpan[] candidates = buffer.getSpans(first, last, ClickableSpan.class); 116 117 int a = Selection.getSelectionStart(buffer); 118 int b = Selection.getSelectionEnd(buffer); 119 120 int selStart = Math.min(a, b); 121 int selEnd = Math.max(a, b); 122 123 if (selStart < 0) { 124 if (buffer.getSpanStart(FROM_BELOW) >= 0) { 125 selStart = selEnd = buffer.length(); 126 } 127 } 128 129 if (selStart > last) 130 selStart = selEnd = Integer.MAX_VALUE; 131 if (selEnd < first) 132 selStart = selEnd = -1; 133 134 switch (what) { 135 case CLICK: 136 if (selStart == selEnd) { 137 return false; 138 } 139 140 ClickableSpan[] links = buffer.getSpans(selStart, selEnd, ClickableSpan.class); 141 142 if (links.length != 1) { 143 return false; 144 } 145 146 ClickableSpan link = links[0]; 147 if (link instanceof TextLinkSpan) { 148 ((TextLinkSpan) link).onClick(widget, TextLinkSpan.INVOCATION_METHOD_KEYBOARD); 149 } else { 150 link.onClick(widget); 151 } 152 break; 153 154 case UP: 155 int bestStart, bestEnd; 156 157 bestStart = -1; 158 bestEnd = -1; 159 160 for (int i = 0; i < candidates.length; i++) { 161 int end = buffer.getSpanEnd(candidates[i]); 162 163 if (end < selEnd || selStart == selEnd) { 164 if (end > bestEnd) { 165 bestStart = buffer.getSpanStart(candidates[i]); 166 bestEnd = end; 167 } 168 } 169 } 170 171 if (bestStart >= 0) { 172 Selection.setSelection(buffer, bestEnd, bestStart); 173 return true; 174 } 175 176 break; 177 178 case DOWN: 179 bestStart = Integer.MAX_VALUE; 180 bestEnd = Integer.MAX_VALUE; 181 182 for (int i = 0; i < candidates.length; i++) { 183 int start = buffer.getSpanStart(candidates[i]); 184 185 if (start > selStart || selStart == selEnd) { 186 if (start < bestStart) { 187 bestStart = start; 188 bestEnd = buffer.getSpanEnd(candidates[i]); 189 } 190 } 191 } 192 193 if (bestEnd < Integer.MAX_VALUE) { 194 Selection.setSelection(buffer, bestStart, bestEnd); 195 return true; 196 } 197 198 break; 199 } 200 201 return false; 202 } 203 204 @Override onTouchEvent(TextView widget, Spannable buffer, MotionEvent event)205 public boolean onTouchEvent(TextView widget, Spannable buffer, 206 MotionEvent event) { 207 int action = event.getAction(); 208 209 if (action == MotionEvent.ACTION_UP || action == MotionEvent.ACTION_DOWN) { 210 int x = (int) event.getX(); 211 int y = (int) event.getY(); 212 213 x -= widget.getTotalPaddingLeft(); 214 y -= widget.getTotalPaddingTop(); 215 216 x += widget.getScrollX(); 217 y += widget.getScrollY(); 218 219 Layout layout = widget.getLayout(); 220 int line = layout.getLineForVertical(y); 221 int off = layout.getOffsetForHorizontal(line, x); 222 223 ClickableSpan[] links = buffer.getSpans(off, off, ClickableSpan.class); 224 225 if (links.length != 0) { 226 ClickableSpan link = links[0]; 227 if (action == MotionEvent.ACTION_UP) { 228 if (link instanceof TextLinkSpan) { 229 ((TextLinkSpan) link).onClick( 230 widget, TextLinkSpan.INVOCATION_METHOD_TOUCH); 231 } else { 232 link.onClick(widget); 233 } 234 } else if (action == MotionEvent.ACTION_DOWN) { 235 if (widget.getContext().getApplicationInfo().targetSdkVersion 236 >= Build.VERSION_CODES.P) { 237 // Selection change will reposition the toolbar. Hide it for a few ms for a 238 // smoother transition. 239 widget.hideFloatingToolbar(HIDE_FLOATING_TOOLBAR_DELAY_MS); 240 } 241 Selection.setSelection(buffer, 242 buffer.getSpanStart(link), 243 buffer.getSpanEnd(link)); 244 } 245 return true; 246 } else { 247 Selection.removeSelection(buffer); 248 } 249 } 250 251 return super.onTouchEvent(widget, buffer, event); 252 } 253 254 @Override initialize(TextView widget, Spannable text)255 public void initialize(TextView widget, Spannable text) { 256 Selection.removeSelection(text); 257 text.removeSpan(FROM_BELOW); 258 } 259 260 @Override onTakeFocus(TextView view, Spannable text, int dir)261 public void onTakeFocus(TextView view, Spannable text, int dir) { 262 Selection.removeSelection(text); 263 264 if ((dir & View.FOCUS_BACKWARD) != 0) { 265 text.setSpan(FROM_BELOW, 0, 0, Spannable.SPAN_POINT_POINT); 266 } else { 267 text.removeSpan(FROM_BELOW); 268 } 269 } 270 getInstance()271 public static MovementMethod getInstance() { 272 if (sInstance == null) 273 sInstance = new LinkMovementMethod(); 274 275 return sInstance; 276 } 277 278 @UnsupportedAppUsage 279 private static LinkMovementMethod sInstance; 280 private static Object FROM_BELOW = new NoCopySpan.Concrete(); 281 } 282