/* * Copyright (C) 2017 The Android Open Source Project * * 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 androidx.wear.widget.util; import android.graphics.Path; import android.graphics.PathMeasure; import android.graphics.RectF; import android.os.SystemClock; import android.support.test.espresso.UiController; import android.support.test.espresso.action.MotionEvents; import android.support.test.espresso.action.Swiper; import android.util.Log; import android.view.MotionEvent; import androidx.annotation.VisibleForTesting; import androidx.core.util.Preconditions; /** * Swiper for gestures meant to be performed on an arc - part of a circle - not a straight line. * This class assumes a square bounding box with the radius of the circle being half the height of * the box. */ public class ArcSwipe implements Swiper { /** Enum describing the exact gesture which will perform the curved swipe. */ public enum Gesture { /** Swipes quickly between the co-ordinates, clockwise. */ FAST_CLOCKWISE(SWIPE_FAST_DURATION_MS, true), /** Swipes deliberately slowly between the co-ordinates, clockwise. */ SLOW_CLOCKWISE(SWIPE_SLOW_DURATION_MS, true), /** Swipes quickly between the co-ordinates, anticlockwise. */ FAST_ANTICLOCKWISE(SWIPE_FAST_DURATION_MS, false), /** Swipes deliberately slowly between the co-ordinates, anticlockwise. */ SLOW_ANTICLOCKWISE(SWIPE_SLOW_DURATION_MS, false); private final int mDuration; private final boolean mClockwise; Gesture(int duration, boolean clockwise) { mDuration = duration; mClockwise = clockwise; } } /** The number of motion events to send for each swipe. */ private static final int SWIPE_EVENT_COUNT = 10; /** Length of time a "fast" swipe should last for, in milliseconds. */ private static final int SWIPE_FAST_DURATION_MS = 100; /** Length of time a "slow" swipe should last for, in milliseconds. */ private static final int SWIPE_SLOW_DURATION_MS = 1500; private static final String TAG = ArcSwipe.class.getSimpleName(); private final RectF mBounds; private final Gesture mGesture; public ArcSwipe(Gesture gesture, RectF bounds) { Preconditions.checkArgument(bounds.height() == bounds.width()); mGesture = gesture; mBounds = bounds; } @Override public Swiper.Status sendSwipe( UiController uiController, float[] startCoordinates, float[] endCoordinates, float[] precision) { return sendArcSwipe( uiController, startCoordinates, endCoordinates, precision, mGesture.mDuration, mGesture.mClockwise); } private float[][] interpolate(float[] start, float[] end, int steps, boolean isClockwise) { float startAngle = getAngle(start[0], start[1]); float endAngle = getAngle(end[0], end[1]); Path path = new Path(); PathMeasure pathMeasure = new PathMeasure(); path.moveTo(start[0], start[1]); path.arcTo(mBounds, startAngle, getSweepAngle(startAngle, endAngle, isClockwise)); pathMeasure.setPath(path, false); float pathLength = pathMeasure.getLength(); float[][] res = new float[steps][2]; float[] mPathTangent = new float[2]; for (int i = 1; i < steps + 1; i++) { pathMeasure.getPosTan((pathLength * i) / (steps + 2f), res[i - 1], mPathTangent); } return res; } private Swiper.Status sendArcSwipe( UiController uiController, float[] startCoordinates, float[] endCoordinates, float[] precision, int duration, boolean isClockwise) { float[][] steps = interpolate(startCoordinates, endCoordinates, SWIPE_EVENT_COUNT, isClockwise); final int delayBetweenMovements = duration / steps.length; MotionEvent downEvent = MotionEvents.sendDown(uiController, startCoordinates, precision).down; try { for (int i = 0; i < steps.length; i++) { if (!MotionEvents.sendMovement(uiController, downEvent, steps[i])) { Log.e(TAG, "Injection of move event as part of the swipe failed. Sending cancel " + "event."); MotionEvents.sendCancel(uiController, downEvent); return Swiper.Status.FAILURE; } long desiredTime = downEvent.getDownTime() + delayBetweenMovements * i; long timeUntilDesired = desiredTime - SystemClock.uptimeMillis(); if (timeUntilDesired > 10) { uiController.loopMainThreadForAtLeast(timeUntilDesired); } } if (!MotionEvents.sendUp(uiController, downEvent, endCoordinates)) { Log.e(TAG, "Injection of up event as part of the swipe failed. Sending cancel event."); MotionEvents.sendCancel(uiController, downEvent); return Swiper.Status.FAILURE; } } finally { downEvent.recycle(); } return Swiper.Status.SUCCESS; } @VisibleForTesting float getAngle(double x, double y) { double relativeX = x - (mBounds.width() / 2); double relativeY = y - (mBounds.height() / 2); double rowAngle = Math.atan2(relativeX, relativeY); double angle = -Math.toDegrees(rowAngle) - 180; if (angle < 0) { angle += 360; } return (float) angle; } @VisibleForTesting float getSweepAngle(float startAngle, float endAngle, boolean isClockwise) { float sweepAngle = endAngle - startAngle; if (sweepAngle < 0) { sweepAngle += 360; } return isClockwise ? sweepAngle : (360 - sweepAngle); } }