1 /* 2 * Copyright (C) 2017 The Android Open Source Project 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 17 package androidx.wear.widget.util; 18 19 import android.graphics.Path; 20 import android.graphics.PathMeasure; 21 import android.graphics.RectF; 22 import android.os.SystemClock; 23 import android.support.test.espresso.UiController; 24 import android.support.test.espresso.action.MotionEvents; 25 import android.support.test.espresso.action.Swiper; 26 import android.util.Log; 27 import android.view.MotionEvent; 28 29 import androidx.annotation.VisibleForTesting; 30 import androidx.core.util.Preconditions; 31 32 /** 33 * Swiper for gestures meant to be performed on an arc - part of a circle - not a straight line. 34 * This class assumes a square bounding box with the radius of the circle being half the height of 35 * the box. 36 */ 37 public class ArcSwipe implements Swiper { 38 39 /** Enum describing the exact gesture which will perform the curved swipe. */ 40 public enum Gesture { 41 /** Swipes quickly between the co-ordinates, clockwise. */ 42 FAST_CLOCKWISE(SWIPE_FAST_DURATION_MS, true), 43 /** Swipes deliberately slowly between the co-ordinates, clockwise. */ 44 SLOW_CLOCKWISE(SWIPE_SLOW_DURATION_MS, true), 45 /** Swipes quickly between the co-ordinates, anticlockwise. */ 46 FAST_ANTICLOCKWISE(SWIPE_FAST_DURATION_MS, false), 47 /** Swipes deliberately slowly between the co-ordinates, anticlockwise. */ 48 SLOW_ANTICLOCKWISE(SWIPE_SLOW_DURATION_MS, false); 49 50 private final int mDuration; 51 private final boolean mClockwise; 52 53 Gesture(int duration, boolean clockwise) { 54 mDuration = duration; 55 mClockwise = clockwise; 56 } 57 } 58 59 /** The number of motion events to send for each swipe. */ 60 private static final int SWIPE_EVENT_COUNT = 10; 61 62 /** Length of time a "fast" swipe should last for, in milliseconds. */ 63 private static final int SWIPE_FAST_DURATION_MS = 100; 64 65 /** Length of time a "slow" swipe should last for, in milliseconds. */ 66 private static final int SWIPE_SLOW_DURATION_MS = 1500; 67 68 private static final String TAG = ArcSwipe.class.getSimpleName(); 69 private final RectF mBounds; 70 private final Gesture mGesture; 71 72 public ArcSwipe(Gesture gesture, RectF bounds) { 73 Preconditions.checkArgument(bounds.height() == bounds.width()); 74 mGesture = gesture; 75 mBounds = bounds; 76 } 77 78 @Override 79 public Swiper.Status sendSwipe( 80 UiController uiController, 81 float[] startCoordinates, 82 float[] endCoordinates, 83 float[] precision) { 84 return sendArcSwipe( 85 uiController, 86 startCoordinates, 87 endCoordinates, 88 precision, 89 mGesture.mDuration, 90 mGesture.mClockwise); 91 } 92 93 private float[][] interpolate(float[] start, float[] end, int steps, boolean isClockwise) { 94 float startAngle = getAngle(start[0], start[1]); 95 float endAngle = getAngle(end[0], end[1]); 96 97 Path path = new Path(); 98 PathMeasure pathMeasure = new PathMeasure(); 99 path.moveTo(start[0], start[1]); 100 path.arcTo(mBounds, startAngle, getSweepAngle(startAngle, endAngle, isClockwise)); 101 pathMeasure.setPath(path, false); 102 float pathLength = pathMeasure.getLength(); 103 104 float[][] res = new float[steps][2]; 105 float[] mPathTangent = new float[2]; 106 107 for (int i = 1; i < steps + 1; i++) { 108 pathMeasure.getPosTan((pathLength * i) / (steps + 2f), res[i - 1], mPathTangent); 109 } 110 111 return res; 112 } 113 114 private Swiper.Status sendArcSwipe( 115 UiController uiController, 116 float[] startCoordinates, 117 float[] endCoordinates, 118 float[] precision, 119 int duration, 120 boolean isClockwise) { 121 122 float[][] steps = interpolate(startCoordinates, endCoordinates, SWIPE_EVENT_COUNT, 123 isClockwise); 124 final int delayBetweenMovements = duration / steps.length; 125 126 MotionEvent downEvent = MotionEvents.sendDown(uiController, startCoordinates, 127 precision).down; 128 try { 129 for (int i = 0; i < steps.length; i++) { 130 if (!MotionEvents.sendMovement(uiController, downEvent, steps[i])) { 131 Log.e(TAG, 132 "Injection of move event as part of the swipe failed. Sending cancel " 133 + "event."); 134 MotionEvents.sendCancel(uiController, downEvent); 135 return Swiper.Status.FAILURE; 136 } 137 138 long desiredTime = downEvent.getDownTime() + delayBetweenMovements * i; 139 long timeUntilDesired = desiredTime - SystemClock.uptimeMillis(); 140 if (timeUntilDesired > 10) { 141 uiController.loopMainThreadForAtLeast(timeUntilDesired); 142 } 143 } 144 145 if (!MotionEvents.sendUp(uiController, downEvent, endCoordinates)) { 146 Log.e(TAG, 147 "Injection of up event as part of the swipe failed. Sending cancel event."); 148 MotionEvents.sendCancel(uiController, downEvent); 149 return Swiper.Status.FAILURE; 150 } 151 } finally { 152 downEvent.recycle(); 153 } 154 return Swiper.Status.SUCCESS; 155 } 156 157 @VisibleForTesting 158 float getAngle(double x, double y) { 159 double relativeX = x - (mBounds.width() / 2); 160 double relativeY = y - (mBounds.height() / 2); 161 double rowAngle = Math.atan2(relativeX, relativeY); 162 double angle = -Math.toDegrees(rowAngle) - 180; 163 if (angle < 0) { 164 angle += 360; 165 } 166 return (float) angle; 167 } 168 169 @VisibleForTesting 170 float getSweepAngle(float startAngle, float endAngle, boolean isClockwise) { 171 float sweepAngle = endAngle - startAngle; 172 if (sweepAngle < 0) { 173 sweepAngle += 360; 174 } 175 return isClockwise ? sweepAngle : (360 - sweepAngle); 176 } 177 } 178