UiScrollable.java revision e54d649fb83a0a44516e5c25a9ac1992c8950e59
1/* 2 * Copyright (C) 2012 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 */ 16package com.android.uiautomator.core; 17 18import android.graphics.Rect; 19import android.util.Log; 20import android.view.accessibility.AccessibilityNodeInfo; 21 22/** 23 * UiScrollable is a {@link UiCollection} however this class provides additional functionality 24 * where the tests need to deal with scrollable contents or desire to enumerate lists of 25 * items. This calls can perform automatic searches within a scrollable container. Whether 26 * the content scrolls vertically or horizontally can be set by calling 27 * {@link #setAsVerticalList()} which is the default, or {@link #setAsHorizontalList()}. 28 */ 29public class UiScrollable extends UiCollection { 30 private static final String LOG_TAG = UiScrollable.class.getSimpleName(); 31 32 // More steps slows the swipe and prevents contents from being flung too far 33 private static final int SCROLL_STEPS = 55; 34 35 private static final int FLING_STEPS = 5; 36 37 // Restrict a swipe's starting and ending points inside a 10% margin of the target 38 private static final double DEFAULT_SWIPE_DEADZONE_PCT = 0.1; 39 40 // Limits the number of swipes/scrolls performed during a search 41 private static int mMaxSearchSwipes = 30; 42 43 // Used in ScrollForward() and ScrollBackward() to determine swipe direction 44 protected boolean mIsVerticalList = true; 45 46 private double mSwipeDeadZonePercentage = DEFAULT_SWIPE_DEADZONE_PCT; 47 48 /** 49 * UiScrollable is a {@link UiCollection} and as such requires a {@link By} selector to identify 50 * the UI element it represents. In the case of UiScrollable, the selector specified is 51 * considered a container where further calls to enumerate or find children will be performed 52 * in. 53 * @param container a {@link By} selector 54 */ 55 public UiScrollable(By container) { 56 // wrap the container selector with container so that QueryController can handle 57 // this type of enumeration search accordingly 58 super(container); 59 } 60 61 /** 62 * Set the direction of swipes when performing scroll search 63 */ 64 public void setAsVerticalList() { 65 mIsVerticalList = true; 66 } 67 68 /** 69 * Set the direction of swipes when performing scroll search 70 */ 71 public void setAsHorizontalList() { 72 mIsVerticalList = false; 73 } 74 75 /** 76 * Used privately when performing swipe searches to decide if an element has become 77 * visible or not. 78 * @param selector 79 * @return true if found else false 80 */ 81 protected boolean exists(By selector) { 82 if(getQueryController().findAccessibilityNodeInfo(selector) != null) { 83 return true; 84 } 85 return false; 86 } 87 88 /** 89 * Searches for child UI element within the constraints of this UiScrollable {@link By} 90 * selector. It looks for any child matching the <code>childPattern</code> argument that has 91 * a child UI element anywhere within its sub hierarchy that has content-description text. 92 * The returned UiObject will point at the <code>childPattern</code> instance that matched the 93 * search and not at the identifying child element that matched the content description.</p> 94 * By default this operation will perform scroll search while attempting to find the 95 * UI element. 96 * See {@link #getChildByDescription(By, String, boolean)} 97 * @param childPattern {@link By} selector of the child pattern to match and return 98 * @param text String of the identifying child contents of of the <code>childPattern</code> 99 * @return {@link UiObject} pointing at and instance of <code>childPattern</code> 100 * @throws UiObjectNotFoundException 101 */ 102 @Override 103 public UiObject getChildByDescription(By childPattern, String text) 104 throws UiObjectNotFoundException { 105 return getChildByDescription(childPattern, text, true); 106 } 107 108 /** 109 * Searches for child UI element within the constraints of this UiScrollable {@link By} 110 * selector. It looks for any child matching the <code>childPattern</code> argument that has 111 * a child UI element anywhere within its sub hierarchy that has content-description text. 112 * The returned UiObject will point at the <code>childPattern</code> instance that matched the 113 * search and not at the identifying child element that matched the content description. 114 * @param childPattern {@link By} selector of the child pattern to match and return 115 * @param text String may be a partial match for the content-description of a child element. 116 * @param allowScrollSearch set to true if scrolling is allowed 117 * @return {@link UiObject} pointing at and instance of <code>childPattern</code> 118 * @throws UiObjectNotFoundException 119 */ 120 public UiObject getChildByDescription(By childPattern, String text, boolean allowScrollSearch) 121 throws UiObjectNotFoundException { 122 if (text != null) { 123 if (allowScrollSearch) { 124 scrollIntoView(By.selector().descriptionContains(text)); 125 } 126 return super.getChildByDescription(childPattern, text); 127 } 128 throw new UiObjectNotFoundException("for description= \"" + text + "\""); 129 } 130 131 /** 132 * Searches for child UI element within the constraints of this UiScrollable {@link By} 133 * selector. It looks for any child matching the <code>childPattern</code> argument and 134 * return the <code>instance</code> specified. The operation is performed only on the visible 135 * items and no scrolling is performed in this case. 136 * @param childPattern {@link By} selector of the child pattern to match and return 137 * @param instance int the desired matched instance of this <code>childPattern</code> 138 * @return {@link UiObject} pointing at and instance of <code>childPattern</code> 139 */ 140 @Override 141 public UiObject getChildByInstance(By childPattern, int instance) 142 throws UiObjectNotFoundException { 143 By patternSelector = By.patternBuilder(getSelector(), 144 By.patternBuilder(childPattern).instance(instance)); 145 return new UiObject(patternSelector); 146 } 147 148 /** 149 * Searches for child UI element within the constraints of this UiScrollable {@link By} 150 * selector. It looks for any child matching the <code>childPattern</code> argument that has 151 * a child UI element anywhere within its sub hierarchy that has text attribute = 152 * <code>text</code>. The returned UiObject will point at the <code>childPattern</code> 153 * instance that matched the search and not at the identifying child element that matched the 154 * text attribute.</p> 155 * By default this operation will perform scroll search while attempting to find the UI 156 * element. 157 * See {@link #getChildByText(By, String, boolean)} 158 * @param childPattern {@link By} selector of the child pattern to match and return 159 * @param text String of the identifying child contents of of the <code>childPattern</code> 160 * @return {@link UiObject} pointing at and instance of <code>childPattern</code> 161 * @throws UiObjectNotFoundException 162 */ 163 @Override 164 public UiObject getChildByText(By childPattern, String text) 165 throws UiObjectNotFoundException { 166 return getChildByText(childPattern, text, true); 167 } 168 169 /** 170 * Searches for child UI element within the constraints of this UiScrollable {@link By} 171 * selector. It looks for any child matching the <code>childPattern</code> argument that has 172 * a child UI element anywhere within its sub hierarchy that has the text attribute = 173 * <code>text</code>. 174 * The returned UiObject will point at the <code>childPattern</code> instance that matched the 175 * search and not at the identifying child element that matched the text attribute. 176 * @param childPattern {@link By} selector of the child pattern to match and return 177 * @param text String of the identifying child contents of of the <code>childPattern</code> 178 * @param allowScrollSearch set to true if scrolling is allowed 179 * @return {@link UiObject} pointing at and instance of <code>childPattern</code> 180 * @throws UiObjectNotFoundException 181 */ 182 public UiObject getChildByText(By childPattern, String text, boolean allowScrollSearch) 183 throws UiObjectNotFoundException { 184 185 if (text != null) { 186 if (allowScrollSearch) { 187 scrollIntoView(By.selector().text(text)); 188 } 189 return super.getChildByText(childPattern, text); 190 } 191 throw new UiObjectNotFoundException("for text= \"" + text + "\""); 192 } 193 194 /** 195 * Performs a swipe Up on the associated UI element until the requested content-description 196 * is found or until swipe attempts have been exhausted. See {@link #setMaxSearchSwipes(int)} 197 * @param text to look for anywhere within the contents of this scrollable. 198 * @return true if item us found else false 199 */ 200 public boolean scrollDescriptionIntoView(String text) { 201 return scrollIntoView(By.selector().description(text)); 202 } 203 204 /** 205 * Perform a scroll search for a UI element matching the {@link By} selector argument. Also 206 * see {@link #scrollDescriptionIntoView(String)} and {@link #scrollTextIntoView(String)}. 207 * @param selector {@link By} selector 208 * @return true if the item was found and now is in view else false 209 */ 210 public boolean scrollIntoView(By selector) { 211 // if we happen to be on top of the text we want then return here 212 if (exists(getSelector().childSelector(selector))) { 213 return (true); 214 } else { 215 // we will need to reset the search from the beginning to start search 216 scrollToBeginning(mMaxSearchSwipes); 217 if (exists(getSelector().childSelector(selector))) { 218 return (true); 219 } 220 for (int x = 0; x < mMaxSearchSwipes; x++) { 221 if(!scrollForward()) { 222 return false; 223 } 224 225 if(exists(getSelector().childSelector(selector))) { 226 return true; 227 } 228 } 229 } 230 return false; 231 } 232 233 /** 234 * Performs a swipe up on the associated display element until the requested text 235 * appears or until swipe attempts have been exhausted. See {@link #setMaxSearchSwipes(int)} 236 * @param text to look for 237 * @return true if item us found else false 238 */ 239 public boolean scrollTextIntoView(String text) { 240 return scrollIntoView(By.selector().text(text)); 241 } 242 243 /** 244 * {@link #getChildByDescription(String, boolean)} and {@link #getChildByText(String, boolean)} 245 * use an arguments that specifies if scrolling is allowed while searching for the UI element. 246 * The number of scrolls allowed to perform a search can be modified by this method. 247 * The current value can be read by calling {@link #getMaxSearchSwipes()} 248 * @param swipes 249 */ 250 public void setMaxSearchSwipes(int swipes) { 251 mMaxSearchSwipes = swipes; 252 } 253 254 /** 255 * {@link #getChildByDescription(String, boolean)} and {@link #getChildByText(String, boolean)} 256 * use an arguments that specifies if scrolling is allowed while searching for the UI element. 257 * The number of scrolls currently allowed to perform a search can be read by this method. 258 * See {@link #setMaxSearchSwipes(int)} 259 * @return max value of the number of swipes currently allowed during a scroll search 260 */ 261 public int getMaxSearchSwipes() { 262 return mMaxSearchSwipes; 263 } 264 265 /** 266 * A convenience version of {@link UiScrollable#scrollForward(int)}, performs a fling 267 * 268 * @return true if scrolled and false if can't scroll anymore 269 */ 270 public boolean flingForward() { 271 return scrollForward(FLING_STEPS); 272 } 273 274 /** 275 * A convenience version of {@link UiScrollable#scrollForward(int)}, performs a regular scroll 276 * 277 * @return true if scrolled and false if can't scroll anymore 278 */ 279 public boolean scrollForward() { 280 return scrollForward(SCROLL_STEPS); 281 } 282 283 /** 284 * Perform a scroll forward. If this list is set to vertical (see {@link #setAsVerticalList()} 285 * default) then the swipes will be executed from the bottom to top. If this list is set 286 * to horizontal (see {@link #setAsHorizontalList()}) then the swipes will be executed from 287 * the right to left. 288 * 289 * @param steps use steps to control the speed, so that it may be a scroll, or fling 290 * @return true if scrolled and false if can't scroll anymore 291 */ 292 public boolean scrollForward(int steps) { 293 Log.d(LOG_TAG, "scrollForward() on selector = " + getSelector()); 294 AccessibilityNodeInfo node = findAccessibilityNodeInfo(WAIT_FOR_SELECTOR_TIMEOUT); 295 if(node == null) { 296 // Object Not Found 297 return false; 298 } 299 Rect rect = new Rect();; 300 node.getBoundsInScreen(rect); 301 302 int downX = 0; 303 int downY = 0; 304 int upX = 0; 305 int upY = 0; 306 307 // scrolling is by default assumed vertically unless the object is explicitly 308 // set otherwise by setAsHorizontalContainer() 309 if(mIsVerticalList) { 310 int swipeAreaAdjust = (int)(rect.height() * getSwipeDeadZonePercentage()); 311 // scroll vertically: swipe down -> up 312 downX = rect.centerX(); 313 downY = rect.bottom - swipeAreaAdjust; 314 upX = rect.centerX(); 315 upY = rect.top + swipeAreaAdjust; 316 } else { 317 int swipeAreaAdjust = (int)(rect.width() * getSwipeDeadZonePercentage()); 318 // scroll horizontally: swipe right -> left 319 // TODO: Assuming device is not in right to left language 320 downX = rect.right - swipeAreaAdjust; 321 downY = rect.centerY(); 322 upX = rect.left + swipeAreaAdjust; 323 upY = rect.centerY(); 324 } 325 return getInteractionController().scrollSwipe(downX, downY, upX, upY, steps); 326 } 327 328 /** 329 * A convenience version of {@link UiScrollable#scrollBackward(int)}, performs a fling 330 * 331 * @return true if scrolled and false if can't scroll anymore 332 */ 333 public boolean flingBackward() { 334 return scrollBackward(FLING_STEPS); 335 } 336 337 /** 338 * A convenience version of {@link UiScrollable#scrollBackward(int)}, performs a regular scroll 339 * 340 * @return true if scrolled and false if can't scroll anymore 341 */ 342 public boolean scrollBackward() { 343 return scrollBackward(SCROLL_STEPS); 344 } 345 346 /** 347 * Perform a scroll backward. If this list is set to vertical (see {@link #setAsVerticalList()} 348 * default) then the swipes will be executed from the top to bottom. If this list is set 349 * to horizontal (see {@link #setAsHorizontalList()}) then the swipes will be executed from 350 * the left to right. 351 * 352 * @param steps use steps to control the speed, so that it may be a scroll, or fling 353 * @return true if scrolled and false if can't scroll anymore 354 */ 355 public boolean scrollBackward(int steps) { 356 Log.d(LOG_TAG, "scrollBackward() on selector = " + getSelector()); 357 AccessibilityNodeInfo node = findAccessibilityNodeInfo(WAIT_FOR_SELECTOR_TIMEOUT); 358 if(node == null) { 359 // Object Not Found 360 return false; 361 } 362 Rect rect = new Rect();; 363 node.getBoundsInScreen(rect); 364 365 int downX = 0; 366 int downY = 0; 367 int upX = 0; 368 int upY = 0; 369 370 // scrolling is by default assumed vertically unless the object is explicitly 371 // set otherwise by setAsHorizontalContainer() 372 if(mIsVerticalList) { 373 int swipeAreaAdjust = (int)(rect.height() * getSwipeDeadZonePercentage()); 374 Log.d(LOG_TAG, "scrollToBegining() using vertical scroll"); 375 // scroll vertically: swipe up -> down 376 downX = rect.centerX(); 377 downY = rect.top + swipeAreaAdjust; 378 upX = rect.centerX(); 379 upY = rect.bottom - swipeAreaAdjust; 380 } else { 381 int swipeAreaAdjust = (int)(rect.width() * getSwipeDeadZonePercentage()); 382 Log.d(LOG_TAG, "scrollToBegining() using hotizontal scroll"); 383 // scroll horizontally: swipe left -> right 384 // TODO: Assuming device is not in right to left language 385 downX = rect.left + swipeAreaAdjust; 386 downY = rect.centerY(); 387 upX = rect.right - swipeAreaAdjust; 388 upY = rect.centerY(); 389 } 390 return getInteractionController().scrollSwipe(downX, downY, upX, upY, steps); 391 } 392 393 /** 394 * Scrolls to the beginning of a scrollable UI element. The beginning could be the top most 395 * in case of vertical lists or the left most in case of horizontal lists. 396 * 397 * @param steps use steps to control the speed, so that it may be a scroll, or fling 398 * @return true on scrolled else false 399 */ 400 public boolean scrollToBeginning(int maxSwipes, int steps) { 401 Log.d(LOG_TAG, "scrollToBeginning() on selector = " + getSelector()); 402 // protect against potential hanging and return after preset attempts 403 for(int x = 0; x < maxSwipes; x++) { 404 if(!scrollBackward(steps)) { 405 break; 406 } 407 } 408 return true; 409 } 410 411 /** 412 * A convenience version of {@link UiScrollable#scrollToBeginning(int, int)} with regular scroll 413 * 414 * @param maxSwipes 415 * @return true on scrolled else false 416 */ 417 public boolean scrollToBeginning(int maxSwipes) { 418 return scrollToBeginning(maxSwipes, SCROLL_STEPS); 419 } 420 421 /** 422 * A convenience version of {@link UiScrollable#scrollToBeginning(int, int)} with fling 423 * 424 * @param maxSwipes 425 * @return true on scrolled else false 426 */ 427 public boolean flingToBeginning(int maxSwipes) { 428 return scrollToBeginning(maxSwipes, FLING_STEPS); 429 } 430 431 /** 432 * Scrolls to the end of a scrollable UI element. The end could be the bottom most 433 * in case of vertical controls or the right most for horizontal controls 434 * 435 * @param steps use steps to control the speed, so that it may be a scroll, or fling 436 * @return true on scrolled else false 437 */ 438 public boolean scrollToEnd(int maxSwipes, int steps) { 439 // protect against potential hanging and return after preset attempts 440 for(int x = 0; x < maxSwipes; x++) { 441 if(!scrollForward(steps)) { 442 break; 443 } 444 } 445 return true; 446 } 447 448 /** 449 * A convenience version of {@link UiScrollable#scrollToEnd(int, int)} with regular scroll 450 * 451 * @param maxSwipes 452 * @return true on scrolled else false 453 */ 454 public boolean scrollToEnd(int maxSwipes) { 455 return scrollToEnd(maxSwipes, SCROLL_STEPS); 456 } 457 458 /** 459 * A convenience version of {@link UiScrollable#scrollToEnd(int, int)} with fling 460 * 461 * @param maxSwipes 462 * @return true on scrolled else false 463 */ 464 public boolean flingToEnd(int maxSwipes) { 465 return scrollToEnd(maxSwipes, FLING_STEPS); 466 } 467 468 public double getSwipeDeadZonePercentage() { 469 return mSwipeDeadZonePercentage; 470 } 471 472 public void setSwipeDeadZonePercentage(double swipeDeadZonePercentage) { 473 mSwipeDeadZonePercentage = swipeDeadZonePercentage; 474 } 475} 476