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