Home | History | Annotate | Download | only in view
      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