1/* 2 * Copyright (C) 2013 DroidDriver committers 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 io.appium.droiddriver.actions; 18 19import android.graphics.Rect; 20import android.os.SystemClock; 21import android.view.ViewConfiguration; 22 23import io.appium.droiddriver.UiElement; 24import io.appium.droiddriver.exceptions.ActionException; 25import io.appium.droiddriver.scroll.Direction.PhysicalDirection; 26import io.appium.droiddriver.util.Events; 27import io.appium.droiddriver.util.Strings; 28import io.appium.droiddriver.util.Strings.ToStringHelper; 29 30/** 31 * An action that swipes the touch screen. 32 */ 33public class SwipeAction extends EventAction implements ScrollAction { 34 // Milliseconds between synthesized ACTION_MOVE events. 35 // Note: ACTION_MOVE_INTERVAL is the minimum interval between injected events; 36 // the actual interval typically is longer. 37 private static final int ACTION_MOVE_INTERVAL = 5; 38 /** 39 * The magic number from UiAutomator. This value is empirical. If it actually 40 * results in a fling, you can change it with {@link #setScrollSteps}. 41 */ 42 private static int scrollSteps = 55; 43 private static int flingSteps = 3; 44 45 /** Returns the {@link #scrollSteps} used in {@link #toScroll}. */ 46 public static int getScrollSteps() { 47 return scrollSteps; 48 } 49 50 /** Sets the {@link #scrollSteps} used in {@link #toScroll}. */ 51 public static void setScrollSteps(int scrollSteps) { 52 SwipeAction.scrollSteps = scrollSteps; 53 } 54 55 /** Returns the {@link #flingSteps} used in {@link #toFling}. */ 56 public static int getFlingSteps() { 57 return flingSteps; 58 } 59 60 /** Sets the {@link #flingSteps} used in {@link #toFling}. */ 61 public static void setFlingSteps(int flingSteps) { 62 SwipeAction.flingSteps = flingSteps; 63 } 64 65 /** 66 * Gets {@link SwipeAction} instances for scrolling. 67 * <p> 68 * Note: This may result in flinging instead of scrolling, depending on the 69 * size of the target UiElement and the SDK version of the device. If it does 70 * not behave as expected, you can change steps with {@link #setScrollSteps}. 71 * </p> 72 * 73 * @param direction specifies where the view port will move, instead of the 74 * finger. 75 * @see ViewConfiguration#getScaledMinimumFlingVelocity 76 */ 77 public static SwipeAction toScroll(PhysicalDirection direction) { 78 return new SwipeAction(direction, scrollSteps); 79 } 80 81 /** 82 * Gets {@link SwipeAction} instances for flinging. 83 * <p> 84 * Note: This may not actually fling, depending on the size of the target 85 * UiElement and the SDK version of the device. If it does not behave as 86 * expected, you can change steps with {@link #setFlingSteps}. 87 * </p> 88 * 89 * @param direction specifies where the view port will move, instead of the 90 * finger. 91 * @see ViewConfiguration#getScaledMinimumFlingVelocity 92 */ 93 public static SwipeAction toFling(PhysicalDirection direction) { 94 return new SwipeAction(direction, flingSteps); 95 } 96 97 private final PhysicalDirection direction; 98 private final boolean drag; 99 private final int steps; 100 private final float topMarginRatio; 101 private final float leftMarginRatio; 102 private final float bottomMarginRatio; 103 private final float rightMarginRatio; 104 105 /** 106 * Defaults timeoutMillis to 1000 and no drag. 107 */ 108 public SwipeAction(PhysicalDirection direction, int steps) { 109 this(direction, steps, false, 1000L); 110 } 111 112 /** 113 * Defaults all margin ratios to 0.1F. 114 */ 115 public SwipeAction(PhysicalDirection direction, int steps, boolean drag, long timeoutMillis) { 116 this(direction, steps, drag, timeoutMillis, 0.1F, 0.1F, 0.1F, 0.1F); 117 } 118 119 /** 120 * @param direction the scroll direction specifying where the view port will 121 * move, instead of the finger. 122 * @param steps minimum 2; (steps-1) is the number of {@code ACTION_MOVE} that 123 * will be injected between {@code ACTION_DOWN} and {@code ACTION_UP}. 124 * @param drag whether this is a drag 125 * @param timeoutMillis the value returned by {@link #getTimeoutMillis} 126 * @param topMarginRatio margin ratio from top 127 * @param leftMarginRatio margin ratio from left 128 * @param bottomMarginRatio margin ratio from bottom 129 * @param rightMarginRatio margin ratio from right 130 */ 131 public SwipeAction(PhysicalDirection direction, int steps, boolean drag, long timeoutMillis, 132 float topMarginRatio, float leftMarginRatio, float bottomMarginRatio, float rightMarginRatio) { 133 super(timeoutMillis); 134 this.direction = direction; 135 this.steps = Math.max(2, steps); 136 this.drag = drag; 137 this.topMarginRatio = topMarginRatio; 138 this.bottomMarginRatio = bottomMarginRatio; 139 this.leftMarginRatio = leftMarginRatio; 140 this.rightMarginRatio = rightMarginRatio; 141 } 142 143 @Override 144 public boolean perform(InputInjector injector, UiElement element) { 145 Rect elementRect = element.getVisibleBounds(); 146 147 int topMargin = (int) (elementRect.height() * topMarginRatio); 148 int bottomMargin = (int) (elementRect.height() * bottomMarginRatio); 149 int leftMargin = (int) (elementRect.width() * leftMarginRatio); 150 int rightMargin = (int) (elementRect.width() * rightMarginRatio); 151 int adjustedbottom = elementRect.bottom - bottomMargin; 152 int adjustedTop = elementRect.top + topMargin; 153 int adjustedLeft = elementRect.left + leftMargin; 154 int adjustedRight = elementRect.right - rightMargin; 155 int startX; 156 int startY; 157 int endX; 158 int endY; 159 160 switch (direction) { 161 case DOWN: 162 startX = elementRect.centerX(); 163 startY = adjustedbottom; 164 endX = elementRect.centerX(); 165 endY = adjustedTop; 166 break; 167 case UP: 168 startX = elementRect.centerX(); 169 startY = adjustedTop; 170 endX = elementRect.centerX(); 171 endY = adjustedbottom; 172 break; 173 case LEFT: 174 startX = adjustedLeft; 175 startY = elementRect.centerY(); 176 endX = adjustedRight; 177 endY = elementRect.centerY(); 178 break; 179 case RIGHT: 180 startX = adjustedRight; 181 startY = elementRect.centerY(); 182 endX = adjustedLeft; 183 endY = elementRect.centerY(); 184 break; 185 default: 186 throw new ActionException("Unknown scroll direction: " + direction); 187 } 188 189 double xStep = ((double) (endX - startX)) / steps; 190 double yStep = ((double) (endY - startY)) / steps; 191 192 // First touch starts exactly at the point requested 193 long downTime = Events.touchDown(injector, startX, startY); 194 SystemClock.sleep(ACTION_MOVE_INTERVAL); 195 if (drag) { 196 SystemClock.sleep((long) (ViewConfiguration.getLongPressTimeout() * 1.5f)); 197 } 198 for (int i = 1; i < steps; i++) { 199 Events.touchMove(injector, downTime, startX + (int) (xStep * i), startY + (int) (yStep * i)); 200 SystemClock.sleep(ACTION_MOVE_INTERVAL); 201 } 202 if (drag) { 203 // Hold final position for a little bit to simulate drag. 204 SystemClock.sleep(100); 205 } 206 Events.touchUp(injector, downTime, endX, endY); 207 return true; 208 } 209 210 @Override 211 public String toString() { 212 ToStringHelper toStringHelper = Strings.toStringHelper(this); 213 toStringHelper.addValue(direction); 214 toStringHelper.add("steps", steps); 215 if (drag) { 216 toStringHelper.addValue("drag"); 217 } 218 return toStringHelper.toString(); 219 } 220} 221