TextViewActions.java revision 5f71b5afe83ea6a183a9a010c05ce4e1453e264b
1/* 2 * Copyright (C) 2015 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.widget.espresso; 18 19import static android.support.test.espresso.action.ViewActions.actionWithAssertions; 20import android.graphics.Rect; 21import android.support.test.espresso.PerformException; 22import android.support.test.espresso.ViewAction; 23import android.support.test.espresso.action.CoordinatesProvider; 24import android.support.test.espresso.action.GeneralClickAction; 25import android.support.test.espresso.action.Press; 26import android.support.test.espresso.action.Tap; 27import android.support.test.espresso.util.HumanReadables; 28import android.text.Layout; 29import android.view.View; 30import android.widget.Editor; 31import android.widget.TextView; 32 33/** 34 * A collection of actions on a {@link android.widget.TextView}. 35 */ 36public final class TextViewActions { 37 38 private TextViewActions() {} 39 40 /** 41 * Returns an action that clicks on text at an index on the TextView.<br> 42 * <br> 43 * View constraints: 44 * <ul> 45 * <li>must be a TextView displayed on screen 46 * <ul> 47 * 48 * @param index The index of the TextView's text to click on. 49 */ 50 public static ViewAction clickOnTextAtIndex(int index) { 51 return actionWithAssertions( 52 new GeneralClickAction(Tap.SINGLE, new TextCoordinates(index), Press.FINGER)); 53 } 54 55 /** 56 * Returns an action that clicks by mouse on text at an index on the TextView.<br> 57 * <br> 58 * View constraints: 59 * <ul> 60 * <li>must be a TextView displayed on screen 61 * <ul> 62 * 63 * @param index The index of the TextView's text to click on. 64 */ 65 public static ViewAction mouseClickOnTextAtIndex(int index) { 66 return actionWithAssertions( 67 new MouseClickAction(Tap.SINGLE, new TextCoordinates(index), Press.PINPOINT)); 68 } 69 70 /** 71 * Returns an action that double-clicks on text at an index on the TextView.<br> 72 * <br> 73 * View constraints: 74 * <ul> 75 * <li>must be a TextView displayed on screen 76 * <ul> 77 * 78 * @param index The index of the TextView's text to double-click on. 79 */ 80 public static ViewAction doubleClickOnTextAtIndex(int index) { 81 return actionWithAssertions( 82 new GeneralClickAction(Tap.DOUBLE, new TextCoordinates(index), Press.FINGER)); 83 } 84 85 /** 86 * Returns an action that double-clicks by mouse on text at an index on the TextView.<br> 87 * <br> 88 * View constraints: 89 * <ul> 90 * <li>must be a TextView displayed on screen 91 * <ul> 92 * 93 * @param index The index of the TextView's text to double-click on. 94 */ 95 public static ViewAction mouseDoubleClickOnTextAtIndex(int index) { 96 return actionWithAssertions( 97 new MouseClickAction(Tap.DOUBLE, new TextCoordinates(index), Press.PINPOINT)); 98 } 99 100 /** 101 * Returns an action that long presses on text at an index on the TextView.<br> 102 * <br> 103 * View constraints: 104 * <ul> 105 * <li>must be a TextView displayed on screen 106 * <ul> 107 * 108 * @param index The index of the TextView's text to long press on. 109 */ 110 public static ViewAction longPressOnTextAtIndex(int index) { 111 return actionWithAssertions( 112 new GeneralClickAction(Tap.LONG, new TextCoordinates(index), Press.FINGER)); 113 } 114 115 /** 116 * Returns an action that long click by mouse on text at an index on the TextView.<br> 117 * <br> 118 * View constraints: 119 * <ul> 120 * <li>must be a TextView displayed on screen 121 * <ul> 122 * 123 * @param index The index of the TextView's text to long click on. 124 */ 125 public static ViewAction mouseLongClickOnTextAtIndex(int index) { 126 return actionWithAssertions( 127 new MouseClickAction(Tap.LONG, new TextCoordinates(index), Press.PINPOINT)); 128 } 129 130 /** 131 * Returns an action that long presses then drags on text from startIndex to endIndex on the 132 * TextView.<br> 133 * <br> 134 * View constraints: 135 * <ul> 136 * <li>must be a TextView displayed on screen 137 * <ul> 138 * 139 * @param startIndex The index of the TextView's text to start a drag from 140 * @param endIndex The index of the TextView's text to end the drag at 141 */ 142 public static ViewAction longPressAndDragOnText(int startIndex, int endIndex) { 143 return actionWithAssertions( 144 new DragAction( 145 DragAction.Drag.LONG_PRESS, 146 new TextCoordinates(startIndex), 147 new TextCoordinates(endIndex), 148 Press.FINGER, 149 TextView.class)); 150 } 151 152 /** 153 * Returns an action that double taps then drags on text from startIndex to endIndex on the 154 * TextView.<br> 155 * <br> 156 * View constraints: 157 * <ul> 158 * <li>must be a TextView displayed on screen 159 * <ul> 160 * 161 * @param startIndex The index of the TextView's text to start a drag from 162 * @param endIndex The index of the TextView's text to end the drag at 163 */ 164 public static ViewAction doubleTapAndDragOnText(int startIndex, int endIndex) { 165 return actionWithAssertions( 166 new DragAction( 167 DragAction.Drag.DOUBLE_TAP, 168 new TextCoordinates(startIndex), 169 new TextCoordinates(endIndex), 170 Press.FINGER, 171 TextView.class)); 172 } 173 174 /** 175 * Returns an action that click then drags by mouse on text from startIndex to endIndex on the 176 * TextView.<br> 177 * <br> 178 * View constraints: 179 * <ul> 180 * <li>must be a TextView displayed on screen 181 * <ul> 182 * 183 * @param startIndex The index of the TextView's text to start a drag from 184 * @param endIndex The index of the TextView's text to end the drag at 185 */ 186 public static ViewAction mouseDragOnText(int startIndex, int endIndex) { 187 return actionWithAssertions( 188 new DragAction( 189 DragAction.Drag.MOUSE_DOWN, 190 new TextCoordinates(startIndex), 191 new TextCoordinates(endIndex), 192 Press.PINPOINT, 193 TextView.class)); 194 } 195 196 /** 197 * Returns an action that double click then drags by mouse on text from startIndex to endIndex 198 * on the TextView.<br> 199 * <br> 200 * View constraints: 201 * <ul> 202 * <li>must be a TextView displayed on screen 203 * <ul> 204 * 205 * @param startIndex The index of the TextView's text to start a drag from 206 * @param endIndex The index of the TextView's text to end the drag at 207 */ 208 public static ViewAction mouseDoubleClickAndDragOnText(int startIndex, int endIndex) { 209 return actionWithAssertions( 210 new DragAction( 211 DragAction.Drag.MOUSE_DOUBLE_CLICK, 212 new TextCoordinates(startIndex), 213 new TextCoordinates(endIndex), 214 Press.PINPOINT, 215 TextView.class)); 216 } 217 218 /** 219 * Returns an action that long click then drags by mouse on text from startIndex to endIndex 220 * on the TextView.<br> 221 * <br> 222 * View constraints: 223 * <ul> 224 * <li>must be a TextView displayed on screen 225 * <ul> 226 * 227 * @param startIndex The index of the TextView's text to start a drag from 228 * @param endIndex The index of the TextView's text to end the drag at 229 */ 230 public static ViewAction mouseLongClickAndDragOnText(int startIndex, int endIndex) { 231 return actionWithAssertions( 232 new DragAction( 233 DragAction.Drag.MOUSE_LONG_CLICK, 234 new TextCoordinates(startIndex), 235 new TextCoordinates(endIndex), 236 Press.PINPOINT, 237 TextView.class)); 238 } 239 240 public enum Handle { 241 SELECTION_START, 242 SELECTION_END, 243 INSERTION 244 }; 245 246 /** 247 * Returns an action that tap then drags on the handle from the current position to endIndex on 248 * the TextView.<br> 249 * <br> 250 * View constraints: 251 * <ul> 252 * <li>must be a TextView's drag-handle displayed on screen 253 * <ul> 254 * 255 * @param textView TextView the handle is on 256 * @param handleType Type of the handle 257 * @param endIndex The index of the TextView's text to end the drag at 258 */ 259 public static ViewAction dragHandle(TextView textView, Handle handleType, int endIndex) { 260 final int currentOffset = handleType == Handle.SELECTION_START ? 261 textView.getSelectionStart() : textView.getSelectionEnd(); 262 return actionWithAssertions( 263 new DragAction( 264 DragAction.Drag.TAP, 265 new HandleCoordinates(textView, handleType, currentOffset), 266 new HandleCoordinates(textView, handleType, endIndex), 267 Press.FINGER, 268 Editor.HandleView.class)); 269 } 270 271 /** 272 * A provider of the x, y coordinates of the handle that points the specified text index in a 273 * text view. 274 */ 275 private static final class HandleCoordinates implements CoordinatesProvider { 276 // Must be larger than Editor#LINE_SLOP_MULTIPLIER_FOR_HANDLEVIEWS. 277 private final static float LINE_SLOP_MULTIPLIER = 0.6f; 278 private final TextView mTextView; 279 private final Handle mHandleType; 280 private final int mIndex; 281 private final String mActionDescription; 282 283 public HandleCoordinates(TextView textView, Handle handleType, int index) { 284 mTextView = textView; 285 mHandleType = handleType; 286 mIndex = index; 287 mActionDescription = "Could not locate " + handleType.toString() 288 + " handle that points text index: " + index; 289 } 290 291 @Override 292 public float[] calculateCoordinates(View view) { 293 try { 294 return locateHandlePointsTextIndex(view); 295 } catch (StringIndexOutOfBoundsException e) { 296 throw new PerformException.Builder() 297 .withActionDescription(mActionDescription) 298 .withViewDescription(HumanReadables.describe(view)) 299 .withCause(e) 300 .build(); 301 } 302 } 303 304 private float[] locateHandlePointsTextIndex(View view) { 305 final int currentOffset = mHandleType == Handle.SELECTION_START ? 306 mTextView.getSelectionStart() : mTextView.getSelectionEnd(); 307 308 final Layout layout = mTextView.getLayout(); 309 final int currentLine = layout.getLineForOffset(currentOffset); 310 final int targetLine = layout.getLineForOffset(mIndex); 311 312 final float[] currentCoordinates = 313 (new TextCoordinates(currentOffset)).calculateCoordinates(mTextView); 314 final float[] targetCoordinates = 315 (new TextCoordinates(mIndex)).calculateCoordinates(mTextView); 316 final Rect bounds = new Rect(); 317 view.getBoundsOnScreen(bounds); 318 final Rect visibleDisplayBounds = new Rect(); 319 mTextView.getWindowVisibleDisplayFrame(visibleDisplayBounds); 320 visibleDisplayBounds.right -= 1; 321 visibleDisplayBounds.bottom -= 1; 322 if (!visibleDisplayBounds.intersect(bounds)) { 323 throw new PerformException.Builder() 324 .withActionDescription(mActionDescription 325 + " The handle is entirely out of the visible display frame of" 326 + "the TextView's window.") 327 .withViewDescription(HumanReadables.describe(view)) 328 .build(); 329 } 330 final float dragPointX = Math.max(Math.min(bounds.centerX(), 331 visibleDisplayBounds.right), visibleDisplayBounds.left); 332 final float diffX = dragPointX - currentCoordinates[0]; 333 final float verticalOffset = bounds.height() * 0.7f; 334 final float dragPointY = Math.max(Math.min(bounds.top + verticalOffset, 335 visibleDisplayBounds.bottom), visibleDisplayBounds.top); 336 float diffY = dragPointY - currentCoordinates[1]; 337 if (currentLine > targetLine) { 338 diffY -= mTextView.getLineHeight() * LINE_SLOP_MULTIPLIER; 339 } else if (currentLine < targetLine) { 340 diffY += mTextView.getLineHeight() * LINE_SLOP_MULTIPLIER; 341 } 342 return new float[] {targetCoordinates[0] + diffX, targetCoordinates[1] + diffY}; 343 } 344 } 345 346 /** 347 * A provider of the x, y coordinates of the text at the specified index in a text view. 348 */ 349 private static final class TextCoordinates implements CoordinatesProvider { 350 351 private final int mIndex; 352 private final String mActionDescription; 353 354 public TextCoordinates(int index) { 355 mIndex = index; 356 mActionDescription = "Could not locate text at index: " + mIndex; 357 } 358 359 @Override 360 public float[] calculateCoordinates(View view) { 361 try { 362 return locateTextAtIndex((TextView) view, mIndex); 363 } catch (ClassCastException e) { 364 throw new PerformException.Builder() 365 .withActionDescription(mActionDescription) 366 .withViewDescription(HumanReadables.describe(view)) 367 .withCause(e) 368 .build(); 369 } catch (StringIndexOutOfBoundsException e) { 370 throw new PerformException.Builder() 371 .withActionDescription(mActionDescription) 372 .withViewDescription(HumanReadables.describe(view)) 373 .withCause(e) 374 .build(); 375 } 376 } 377 378 /** 379 * @throws StringIndexOutOfBoundsException 380 */ 381 private float[] locateTextAtIndex(TextView textView, int index) { 382 if (index < 0 || index > textView.getText().length()) { 383 throw new StringIndexOutOfBoundsException(index); 384 } 385 final int[] xy = new int[2]; 386 textView.getLocationOnScreen(xy); 387 final Layout layout = textView.getLayout(); 388 final int line = layout.getLineForOffset(index); 389 final float x = textView.getTotalPaddingLeft() - textView.getScrollX() 390 + layout.getPrimaryHorizontal(index); 391 final float y = textView.getTotalPaddingTop() - textView.getScrollY() 392 + layout.getLineTop(line); 393 return new float[]{x + xy[0], y + xy[1]}; 394 } 395 } 396} 397