/* * Copyright (C) 2013 DroidDriver committers * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package com.google.android.droiddriver.actions; import android.graphics.Rect; import android.os.SystemClock; import android.view.ViewConfiguration; import com.google.android.droiddriver.UiElement; import com.google.android.droiddriver.exceptions.ActionException; import com.google.android.droiddriver.scroll.Direction.PhysicalDirection; import com.google.android.droiddriver.util.Events; import com.google.android.droiddriver.util.Strings; import com.google.android.droiddriver.util.Strings.ToStringHelper; /** * An action that swipes the touch screen. */ public class SwipeAction extends EventAction implements ScrollAction { // Milliseconds between synthesized ACTION_MOVE events. // Note: ACTION_MOVE_INTERVAL is the minimum interval between injected events; // the actual interval typically is longer. private static final int ACTION_MOVE_INTERVAL = 5; /** * The magic number from UiAutomator. This value is empirical. If it actually * results in a fling, you can change it with {@link #setScrollSteps}. */ private static int scrollSteps = 55; private static int flingSteps = 3; /** Returns the {@link #scrollSteps} used in {@link #toScroll}. */ public static int getScrollSteps() { return scrollSteps; } /** Sets the {@link #scrollSteps} used in {@link #toScroll}. */ public static void setScrollSteps(int scrollSteps) { SwipeAction.scrollSteps = scrollSteps; } /** Returns the {@link #flingSteps} used in {@link #toFling}. */ public static int getFlingSteps() { return flingSteps; } /** Sets the {@link #flingSteps} used in {@link #toFling}. */ public static void setFlingSteps(int flingSteps) { SwipeAction.flingSteps = flingSteps; } /** * Gets {@link SwipeAction} instances for scrolling. *

* Note: This may result in flinging instead of scrolling, depending on the * size of the target UiElement and the SDK version of the device. If it does * not behave as expected, you can change steps with {@link #setScrollSteps}. *

* * @param direction specifies where the view port will move, instead of the * finger. * @see ViewConfiguration#getScaledMinimumFlingVelocity */ public static SwipeAction toScroll(PhysicalDirection direction) { return new SwipeAction(direction, scrollSteps); } /** * Gets {@link SwipeAction} instances for flinging. *

* Note: This may not actually fling, depending on the size of the target * UiElement and the SDK version of the device. If it does not behave as * expected, you can change steps with {@link #setFlingSteps}. *

* * @param direction specifies where the view port will move, instead of the * finger. * @see ViewConfiguration#getScaledMinimumFlingVelocity */ public static SwipeAction toFling(PhysicalDirection direction) { return new SwipeAction(direction, flingSteps); } private final PhysicalDirection direction; private final boolean drag; private final int steps; private final float topMarginRatio; private final float leftMarginRatio; private final float bottomMarginRatio; private final float rightMarginRatio; /** * Defaults timeoutMillis to 1000 and no drag. */ public SwipeAction(PhysicalDirection direction, int steps) { this(direction, steps, false, 1000L); } /** * Defaults all margin ratios to 0.1F. */ public SwipeAction(PhysicalDirection direction, int steps, boolean drag, long timeoutMillis) { this(direction, steps, drag, timeoutMillis, 0.1F, 0.1F, 0.1F, 0.1F); } /** * @param direction the scroll direction specifying where the view port will * move, instead of the finger. * @param steps minimum 2; (steps-1) is the number of {@code ACTION_MOVE} that * will be injected between {@code ACTION_DOWN} and {@code ACTION_UP}. * @param drag whether this is a drag * @param timeoutMillis * @param topMarginRatio margin ratio from top * @param leftMarginRatio margin ratio from left * @param bottomMarginRatio margin ratio from bottom * @param rightMarginRatio margin ratio from right */ public SwipeAction(PhysicalDirection direction, int steps, boolean drag, long timeoutMillis, float topMarginRatio, float leftMarginRatio, float bottomMarginRatio, float rightMarginRatio) { super(timeoutMillis); this.direction = direction; this.steps = Math.max(2, steps); this.drag = drag; this.topMarginRatio = topMarginRatio; this.bottomMarginRatio = bottomMarginRatio; this.leftMarginRatio = leftMarginRatio; this.rightMarginRatio = rightMarginRatio; } @Override public boolean perform(InputInjector injector, UiElement element) { Rect elementRect = element.getVisibleBounds(); int topMargin = (int) (elementRect.height() * topMarginRatio); int bottomMargin = (int) (elementRect.height() * bottomMarginRatio); int leftMargin = (int) (elementRect.width() * leftMarginRatio); int rightMargin = (int) (elementRect.width() * rightMarginRatio); int adjustedbottom = elementRect.bottom - bottomMargin; int adjustedTop = elementRect.top + topMargin; int adjustedLeft = elementRect.left + leftMargin; int adjustedRight = elementRect.right - rightMargin; int startX; int startY; int endX; int endY; switch (direction) { case DOWN: startX = elementRect.centerX(); startY = adjustedbottom; endX = elementRect.centerX(); endY = adjustedTop; break; case UP: startX = elementRect.centerX(); startY = adjustedTop; endX = elementRect.centerX(); endY = adjustedbottom; break; case LEFT: startX = adjustedLeft; startY = elementRect.centerY(); endX = adjustedRight; endY = elementRect.centerY(); break; case RIGHT: startX = adjustedRight; startY = elementRect.centerY(); endX = adjustedLeft; endY = elementRect.centerY(); break; default: throw new ActionException("Unknown scroll direction: " + direction); } double xStep = ((double) (endX - startX)) / steps; double yStep = ((double) (endY - startY)) / steps; // First touch starts exactly at the point requested long downTime = Events.touchDown(injector, startX, startY); SystemClock.sleep(ACTION_MOVE_INTERVAL); if (drag) { SystemClock.sleep((long) (ViewConfiguration.getLongPressTimeout() * 1.5f)); } for (int i = 1; i < steps; i++) { Events.touchMove(injector, downTime, startX + (int) (xStep * i), startY + (int) (yStep * i)); SystemClock.sleep(ACTION_MOVE_INTERVAL); } if (drag) { // Hold final position for a little bit to simulate drag. SystemClock.sleep(100); } Events.touchUp(injector, downTime, endX, endY); return true; } @Override public String toString() { ToStringHelper toStringHelper = Strings.toStringHelper(this); toStringHelper.addValue(direction); toStringHelper.add("steps", steps); if (drag) { toStringHelper.addValue("drag"); } return toStringHelper.toString(); } }