blob: 240e13e24e02aea49a2c5b863aa2afbf95373087 [file] [log] [blame]
/*
* 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);
}
}