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