1 /* 2 * Copyright (C) 2016 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 android.view; 18 19 import static android.support.test.espresso.core.deps.guava.base.Preconditions.checkNotNull; 20 import static android.support.test.espresso.matcher.ViewMatchers.isAssignableFrom; 21 import static android.support.test.espresso.matcher.ViewMatchers.isCompletelyDisplayed; 22 import static org.hamcrest.Matchers.allOf; 23 24 import android.os.SystemClock; 25 import android.support.test.espresso.InjectEventSecurityException; 26 import android.support.test.espresso.PerformException; 27 import android.support.test.espresso.ViewAction; 28 import android.support.test.espresso.action.MotionEvents; 29 import android.support.test.espresso.action.Swiper; 30 import android.support.test.espresso.UiController; 31 import android.support.test.espresso.util.HumanReadables; 32 import android.view.MotionEvent; 33 import android.view.View; 34 import android.view.ViewConfiguration; 35 import javax.annotation.Nullable; 36 import org.hamcrest.Matcher; 37 38 /** 39 * Pinch and zooms on a View using touch events. 40 * <br> 41 * View constraints: 42 * <ul> 43 * <li>must be displayed on screen 44 * <ul> 45 */ 46 public class PinchZoomAction implements ViewAction { 47 public static Swiper.Status sendPinchZoomAction(UiController uiController, 48 float[] firstFingerStartCoords, 49 float[] firstFingerEndCoords, 50 float[] secondFingerStartCoords, 51 float[] secondFingerEndCoords, 52 float[] precision) { 53 checkNotNull(uiController); 54 checkNotNull(firstFingerStartCoords); 55 checkNotNull(firstFingerEndCoords); 56 checkNotNull(secondFingerStartCoords); 57 checkNotNull(secondFingerEndCoords); 58 checkNotNull(precision); 59 60 // Specify the touch properties for the finger events. 61 final MotionEvent.PointerProperties pp1 = new MotionEvent.PointerProperties(); 62 pp1.id = 0; 63 pp1.toolType = MotionEvent.TOOL_TYPE_FINGER; 64 final MotionEvent.PointerProperties pp2 = new MotionEvent.PointerProperties(); 65 pp2.id = 1; 66 pp2.toolType = MotionEvent.TOOL_TYPE_FINGER; 67 MotionEvent.PointerProperties[] pointerProperties = 68 new MotionEvent.PointerProperties[]{pp1, pp2}; 69 70 // Specify the motion properties of the two touch points. 71 final MotionEvent.PointerCoords pc1 = new MotionEvent.PointerCoords(); 72 pc1.x = firstFingerStartCoords[0]; 73 pc1.y = firstFingerStartCoords[1]; 74 pc1.pressure = 1; 75 pc1.size = 1; 76 final MotionEvent.PointerCoords pc2 = new MotionEvent.PointerCoords(); 77 pc2.x = secondFingerStartCoords[0]; 78 pc2.y = secondFingerEndCoords[1]; 79 pc2.pressure = 1; 80 pc2.size = 1; 81 82 final long startTime = SystemClock.uptimeMillis(); 83 long eventTime = startTime; 84 final MotionEvent.PointerCoords[] pointerCoords = new MotionEvent.PointerCoords[]{pc1, pc2}; 85 86 final MotionEvent firstFingerEvent = MotionEvent.obtain(startTime, 87 eventTime, MotionEvent.ACTION_DOWN, 1, pointerProperties, pointerCoords, 88 0, 0, 1, 1, 0, 0, 0, 0); 89 90 eventTime = SystemClock.uptimeMillis(); 91 final MotionEvent secondFingerEvent = MotionEvent.obtain(startTime, eventTime, 92 MotionEvent.ACTION_POINTER_DOWN + 93 (pp2.id << MotionEvent.ACTION_POINTER_INDEX_SHIFT), 94 2, pointerProperties, pointerCoords, 0, 0, 1, 1, 0, 0, 0, 0); 95 96 try { 97 uiController.injectMotionEvent(firstFingerEvent); 98 } catch (InjectEventSecurityException e) { 99 throw new PerformException.Builder() 100 .withActionDescription("First finger down event") 101 .withViewDescription("Scale gesture detector") 102 .withCause(e) 103 .build(); 104 } 105 106 try { 107 uiController.injectMotionEvent(secondFingerEvent); 108 } catch (InjectEventSecurityException e) { 109 throw new PerformException.Builder() 110 .withActionDescription("Second finger down event") 111 .withViewDescription("Scale gesture detector") 112 .withCause(e) 113 .build(); 114 } 115 116 // Specify the coordinates of the two touch points. 117 final float[][] stepsFirstFinger = interpolate(firstFingerStartCoords, 118 firstFingerEndCoords); 119 final float[][] stepsSecondFinger = interpolate(secondFingerStartCoords, 120 secondFingerEndCoords); 121 122 // Loop until the end points of the two fingers are reached. 123 for (int i = 0; i < PINCH_STEP_COUNT; i++) { 124 eventTime = SystemClock.uptimeMillis(); 125 126 pc1.x = stepsFirstFinger[i][0]; 127 pc1.y = stepsFirstFinger[i][1]; 128 pc2.x = stepsSecondFinger[i][0]; 129 pc2.y = stepsSecondFinger[i][1]; 130 131 final MotionEvent event = MotionEvent.obtain(startTime, eventTime, 132 MotionEvent.ACTION_MOVE, 2, pointerProperties, pointerCoords, 133 0, 0, 1, 1, 0, 0, 0, 0); 134 135 try { 136 uiController.injectMotionEvent(event); 137 } catch (InjectEventSecurityException e) { 138 throw new PerformException.Builder() 139 .withActionDescription("Move event") 140 .withViewDescription("Scale gesture event") 141 .withCause(e) 142 .build(); 143 } 144 145 uiController.loopMainThreadForAtLeast(800); 146 } 147 148 eventTime = SystemClock.uptimeMillis(); 149 150 // Send the up event for the second finger. 151 final MotionEvent secondFingerUpEvent = MotionEvent.obtain(startTime, eventTime, 152 MotionEvent.ACTION_POINTER_UP, 2, pointerProperties, pointerCoords, 153 0, 0, 1, 1, 0, 0, 0, 0); 154 try { 155 uiController.injectMotionEvent(secondFingerUpEvent); 156 } catch (InjectEventSecurityException e) { 157 throw new PerformException.Builder() 158 .withActionDescription("Second finger up event") 159 .withViewDescription("Scale gesture detector") 160 .withCause(e) 161 .build(); 162 } 163 164 eventTime = SystemClock.uptimeMillis(); 165 // Send the up event for the first finger. 166 final MotionEvent firstFingerUpEvent = MotionEvent.obtain(startTime, eventTime, 167 MotionEvent.ACTION_POINTER_UP, 1, pointerProperties, pointerCoords, 168 0, 0, 1, 1, 0, 0, 0, 0); 169 try { 170 uiController.injectMotionEvent(firstFingerUpEvent); 171 } catch (InjectEventSecurityException e) { 172 throw new PerformException.Builder() 173 .withActionDescription("First finger up event") 174 .withViewDescription("Scale gesture detector") 175 .withCause(e) 176 .build(); 177 } 178 return Swiper.Status.SUCCESS; 179 } 180 181 private static float[][] interpolate(float[] start, float[] end) { 182 float[][] res = new float[PINCH_STEP_COUNT][2]; 183 184 for (int i = 0; i < PINCH_STEP_COUNT; i++) { 185 res[i][0] = start[0] + (end[0] - start[0]) * i / (PINCH_STEP_COUNT - 1f); 186 res[i][1] = start[1] + (end[1] - start[1]) * i / (PINCH_STEP_COUNT - 1f); 187 } 188 189 return res; 190 } 191 192 /** The number of move events to send for each pinch. */ 193 private static final int PINCH_STEP_COUNT = 10; 194 195 private final Class<? extends View> mViewClass; 196 private final float[] mFirstFingerStartCoords; 197 private final float[] mFirstFingerEndCoords; 198 private final float[] mSecondFingerStartCoords; 199 private final float[] mSecondFingerEndCoords; 200 201 public PinchZoomAction(float[] firstFingerStartCoords, 202 float[] firstFingerEndCoords, 203 float[] secondFingerStartCoords, 204 float[] secondFingerEndCoords, 205 Class<? extends View> viewClass) { 206 mFirstFingerStartCoords = firstFingerStartCoords; 207 mFirstFingerEndCoords = firstFingerEndCoords; 208 mSecondFingerStartCoords = secondFingerStartCoords; 209 mSecondFingerEndCoords = secondFingerEndCoords; 210 mViewClass = viewClass; 211 } 212 213 @Override 214 @SuppressWarnings("unchecked") 215 public Matcher<View> getConstraints() { 216 return allOf(isCompletelyDisplayed(), isAssignableFrom(mViewClass)); 217 } 218 219 @Override 220 public void perform(UiController uiController, View view) { 221 checkNotNull(uiController); 222 checkNotNull(view); 223 Swiper.Status status; 224 final float[] precision = {1.0f, 1.0f, 1.0f, 1.0f}; 225 226 try { 227 status = sendPinchZoomAction(uiController, this.mFirstFingerStartCoords, 228 this.mFirstFingerEndCoords, this.mSecondFingerStartCoords, 229 this.mSecondFingerEndCoords, precision); 230 } catch (RuntimeException re) { 231 throw new PerformException.Builder() 232 .withActionDescription(getDescription()) 233 .withViewDescription(HumanReadables.describe(view)) 234 .withCause(re) 235 .build(); 236 } 237 if (status == Swiper.Status.FAILURE) { 238 throw new PerformException.Builder() 239 .withActionDescription(getDescription()) 240 .withViewDescription(HumanReadables.describe(view)) 241 .withCause(new RuntimeException(getDescription() + " failed")) 242 .build(); 243 } 244 } 245 246 @Override 247 public String getDescription() { 248 return "Pinch Zoom Action"; 249 } 250 } 251