如何为 Android 测试生成缩放/捏合手势

Posted

技术标签:

【中文标题】如何为 Android 测试生成缩放/捏合手势【英文标题】:How to generate zoom/pinch gesture for testing for Android 【发布时间】:2012-07-16 10:17:45 【问题描述】:

我正在尝试生成缩放/捏合手势进行测试,我尝试使用 android API MotionEvent.obtain(),但发现很难实现缩放/捏合事件。

我从 MotionEvent.obtain 引用了 API。你能告诉我正确的方法吗?

这是我的实现:

//for zoom, we need four points coordinations: start0, start1, end0, end1

Instrumentation inst;

// action down event
MotionEvent event = MotionEvent.obtain(downTime, eventTime, MotionEvent.ACTION_DOWN, 1, prop_start0, pointerCoords_start0, 0,  0, 0, 0, 0, 0, 0, 0 );
inst.sendPointerSync(event);

// action pointer 2 down event
event = MotionEvent.obtain(downTime, eventTime, MotionEvent.ACTION_POINTER_2_DOWN, 2, properties_start0_1, pointerCoords_start0_1, 0, 0, 0, 0, 0, 0, 0, 0);
inst.sendPointerSync(event);

// action move events
duration = 1000; //1000 ms
event_interval = 10;//time interval between consecutive events 10ms 
moveEventNum = duration / event_interval; 
stepx0 = (end0.x - start0.x)/moveEventNum;
stepy0 = (end0.y - start0.y)/moveEventNum;
stepx1 = (end1.x - start1.x)/moveEventNum;
stepy1 = (end1.y - start1.y)/moveEventNum;

move_event0= start0;
move_event1 = start1;
for ( int i = 0; i < moveEventNum; i++) 
       //  [generate middle points here ]
       mov_event0.x += stepx0;
       mov_event0.y += stepy0;
       mov_event1.x += stepx1;
       mov_event1.y += stepy1;

      eventTime += event_interval;

      event = MotionEvent.obtain(downTime, eventTime, MotionEvent.ACTION_MOVE, 2, properties_move_event, pointerCoords_move_event0_1, 0, 0, 0, 0, 0, 0, 0, 0); 

     inst.sendPointerSync(event);


event = MotionEvent.obtain(downTime, eventTime, MotionEvent.ACTION_POINTER_2_UP, 2, properties_end0_1, pointerCoords_end0_1, 0, 0, 0, 0, 0, 0, 0, 0);
inst.sendPointerSync(event);

event = MotionEvent.obtain(downTime, eventTime, MotionEvent.ACTION_UP, 1, end1, pointerCoords_end1, 0, 0, 0, 0, 0, 0, 0, 0 );
inst.sendPointerSync(event);

【问题讨论】:

我强烈建议您获取一个设备进行调试。这将是值得的。 是的,我正在设备中调试,并尝试生成用于测试自动化的缩放手势。 【参考方案1】:

嗯,我找到了问题。

问题:

在使用obtain() API时,我们必须在每个事件中设置点的压力和大小。

对于

obtain(long, long, int, int, android.view.MotionEvent.PointerProperties[], 
android.view.MotionEvent.PointerCoords[], int, int, float, float, int, int, int, int)

PointerCoords[],我们要设置压力和大小为1,默认值为0。

对于

public static MotionEvent obtain (long downTime, long eventTime, int action,
         float x, float y, int metaState)

     Create a new MotionEvent, filling in a subset of the basic motion values. 
    Those not specified here are: device id (always 0), pressure and size (always 1), 
x and y precision (always 1), and edgeFlags (always 0).

since the default pressure and size are 1, so we don't need to set them.

创建手势的技巧:

1.遵循真实的手势序列,因为我们要模拟真实的手势

覆盖 onTouchEvent() 以检查接收到的真实事件 应用。这些事件也可以用来比较真实的 用户触摸事件和生成的触摸事件 以浏览器为例 示例:

a) @Override public boolean onTouchEvent(MotionEvent event) Log.i("WebView", event.toString() + event.getAction());布尔值 = super.onTouchEvent(事件);返回 rt;

手动触摸屏幕从 a) 中的 onTouchEvent() 获取真正的手势序列。我们可以在生成事件时遵循事件顺序。 -- 如果我们不遵循手势事件序列,检测到的事件可能会被拒绝。

这是一个有效的缩放手势事件序列,(所有事件的downTime都是一样的)

我。 ACTION_DOWN 一个起点

二。两个起点的ACTION_POINTER_2_DOWN

iii.ACTION_MOVE 两个中间点

四。 ACTION_POINTER_2_UP 两个端点

v.一个端点的 ACTION_UP

2。正确使用 API MotionEvent.获取

有两个最常用的obtain() API。

public static MotionEvent 获取 (long downTime, long eventTime, int action, 浮动 x,浮动 y,int 元状态)

public static MotionEvent 获取(long, long, int, int, android.view.MotionEvent.PointerProperties[], android.view.MotionEvent.PointerCoords[], int, int, float, float, int, int, int, int)

第一个通常用于单点手势,如滑动、滚动、单击等。此功能的参数(压力、大小、xPresion、yPresion)均设​​置为1。

第二个是更通用的一个,可以用于多点触控事件的生成。而对于第二个,我们必须将每个触摸点的pointerCoords中的压力,大小设置为1。

以下是生成缩放手势的示例:

public static void generateZoomGesture(Instrumentation inst,
        long startTime, boolean ifMove, GestureInfo.Point startPoint1,
        GestureInfo.Point startPoint2, GestureInfo.Point endPoint1,
        GestureInfo.Point endPoint2, int duration) 

    if (inst == null || startPoint1 == null
            || (ifMove && endPoint1 == null)) 
        return;
    

    long eventTime = startTime;
    long downTime = startTime;
    MotionEvent event;
    float eventX1, eventY1, eventX2, eventY2;

    eventX1 = startPoint1.x;
    eventY1 = startPoint1.y;
    eventX2 = startPoint2.x;
    eventY2 = startPoint2.y;

    // specify the property for the two touch points
    PointerProperties[] properties = new PointerProperties[2];
    PointerProperties pp1 = new PointerProperties();
    pp1.id = 0;
    pp1.toolType = MotionEvent.TOOL_TYPE_FINGER;
    PointerProperties pp2 = new PointerProperties();
    pp2.id = 1;
    pp2.toolType = MotionEvent.TOOL_TYPE_FINGER;

    properties[0] = pp1;
    properties[1] = pp2;

    //specify the coordinations of the two touch points
    //NOTE: you MUST set the pressure and size value, or it doesn't work
    PointerCoords[] pointerCoords = new PointerCoords[2];
    PointerCoords pc1 = new PointerCoords();
    pc1.x = eventX1;
    pc1.y = eventY1;
    pc1.pressure = 1;
    pc1.size = 1;
    PointerCoords pc2 = new PointerCoords();
    pc2.x = eventX2;
    pc2.y = eventY2;
    pc2.pressure = 1;
    pc2.size = 1;
    pointerCoords[0] = pc1;
    pointerCoords[1] = pc2;

    //////////////////////////////////////////////////////////////
    // events sequence of zoom gesture
    // 1. send ACTION_DOWN event of one start point
    // 2. send ACTION_POINTER_2_DOWN of two start points
    // 3. send ACTION_MOVE of two middle points
    // 4. repeat step 3 with updated middle points (x,y),
    //      until reach the end points
    // 5. send ACTION_POINTER_2_UP of two end points
    // 6. send ACTION_UP of one end point
    //////////////////////////////////////////////////////////////

    // step 1
    event = MotionEvent.obtain(downTime, eventTime, 
                MotionEvent.ACTION_DOWN, 1, properties, 
                pointerCoords, 0,  0, 1, 1, 0, 0, 0, 0 );

    inst.sendPointerSync(event);

    //step 2
    event = MotionEvent.obtain(downTime, eventTime, 
                MotionEvent.ACTION_POINTER_2_DOWN, 2, 
                properties, pointerCoords, 0, 0, 1, 1, 0, 0, 0, 0);

    inst.sendPointerSync(event);

    //step 3, 4
    if (ifMove) 
        int moveEventNumber = 1;
        moveEventNumber = duration / EVENT_MIN_INTERVAL;

        float stepX1, stepY1, stepX2, stepY2;

        stepX1 = (endPoint1.x - startPoint1.x) / moveEventNumber;
        stepY1 = (endPoint1.y - startPoint1.y) / moveEventNumber;
        stepX2 = (endPoint2.x - startPoint2.x) / moveEventNumber;
        stepY2 = (endPoint2.y - startPoint2.y) / moveEventNumber;

        for (int i = 0; i < moveEventNumber; i++) 
            // update the move events
            eventTime += EVENT_MIN_INTERVAL;
            eventX1 += stepX1;
            eventY1 += stepY1;
            eventX2 += stepX2;
            eventY2 += stepY2;

            pc1.x = eventX1;
            pc1.y = eventY1;
            pc2.x = eventX2;
            pc2.y = eventY2;

            pointerCoords[0] = pc1;
            pointerCoords[1] = pc2;

            event = MotionEvent.obtain(downTime, eventTime,
                        MotionEvent.ACTION_MOVE, 2, properties, 
                        pointerCoords, 0, 0, 1, 1, 0, 0, 0, 0);

            inst.sendPointerSync(event);
        
    

    //step 5
    pc1.x = endPoint1.x;
    pc1.y = endPoint1.y;
    pc2.x = endPoint2.x;
    pc2.y = endPoint2.y;
    pointerCoords[0] = pc1;
    pointerCoords[1] = pc2;

    eventTime += EVENT_MIN_INTERVAL;
    event = MotionEvent.obtain(downTime, eventTime,
                MotionEvent.ACTION_POINTER_2_UP, 2, properties, 
                pointerCoords, 0, 0, 1, 1, 0, 0, 0, 0);
    inst.sendPointerSync(event);

    // step 6
    eventTime += EVENT_MIN_INTERVAL;
    event = MotionEvent.obtain(downTime, eventTime, 
                MotionEvent.ACTION_UP, 1, properties, 
                pointerCoords, 0, 0, 1, 1, 0, 0, 0, 0 );
    inst.sendPointerSync(event);

【讨论】:

MotionEvent.ACTION_POINTER_2_UP 和 MotionEvent.ACTION_POINTER_2_DOWN 似乎已被弃用,但如果我们使用 MotionEvent.ACTION_POINTER_DOWN 和 MotionEvent.ACTION_POINTER_UP 它不起作用。你知道原因吗? 对于浓缩咖啡,实现 au ViewAction。然后将Instrumentation 替换为UiController,将inst.sendPointerSync(event); 替换为uiController.injectMotionEvent(event);。最后将generateZoomGesture方法调用放在perform方法中。 我对这个例子很感兴趣,但不知道如何为 Espresso UI 测试实现它。你们能发布一个 Espresso 缩放手势的例子吗? 什么是 GestureInfo 类? @Täg 请分享更多关于如何在 espresso 中实现它的信息,如果可能的话,使用代码,最好在这篇文章中发表评论【参考方案2】:

正如 Täg 在评论中提到的,这可以与 Espresso 一起使用。这是我在 Espresso 中执行此操作的代码,基于 longchuan 的非常好的答案和更新表格 peetasan。

在浓缩咖啡测试中:

// Pinch out (to zoom in):
onView(withId(R.id.MyViewId)).perform(pinchOut());

辅助方法,包括从视图中获取正确坐标的好方法,还包括对uiController.injectMotionEvent(event) 的正确错误处理:

public static ViewAction pinchOut() 
    return new ViewAction() 
        @Override
        public Matcher<View> getConstraints() 
            return ViewMatchers.isEnabled();
        

        @Override
        public String getDescription() 
            return "Pinch out";
        

        @Override
        public void perform(UiController uiController, View view) 
            Point middlePosition = getCenterPoint(view);

            final int startDelta = 0; // How far from the center point each finger should start
            final int endDelta = 500; // How far from the center point each finger should end (note: Be sure to have this large enough so that the gesture is recognized!)

            Point startPoint1 = new Point(middlePosition.x - startDelta, middlePosition.y);
            Point startPoint2 = new Point(middlePosition.x + startDelta, middlePosition.y);
            Point endPoint1 = new Point(middlePosition.x - endDelta, middlePosition.y);
            Point endPoint2 = new Point(middlePosition.x + endDelta, middlePosition.y);

            performPinch(uiController, startPoint1, startPoint2, endPoint1, endPoint2);
        
    ;


public static ViewAction pinchIn() 
    return new ViewAction() 
        @Override
        public Matcher<View> getConstraints() 
            return ViewMatchers.isEnabled();
        

        @Override
        public String getDescription() 
            return "Pinch in";
        

        @Override
        public void perform(UiController uiController, View view) 
            Point middlePosition = getCenterPoint(view);

            final int startDelta = 500; // How far from the center point each finger should start (note: Be sure to have this large enough so that the gesture is recognized!)
            final int endDelta = 0; // How far from the center point each finger should end

            Point startPoint1 = new Point(middlePosition.x - startDelta, middlePosition.y);
            Point startPoint2 = new Point(middlePosition.x + startDelta, middlePosition.y);
            Point endPoint1 = new Point(middlePosition.x - endDelta, middlePosition.y);
            Point endPoint2 = new Point(middlePosition.x + endDelta, middlePosition.y);

            performPinch(uiController, startPoint1, startPoint2, endPoint1, endPoint2);
        
    ;


@NonNull
private static Point getCenterPoint(View view) 
    int[] locationOnScreen = new int[2];
    view.getLocationOnScreen(locationOnScreen);
    float viewHeight = view.getHeight() * view.getScaleY();
    float viewWidth = view.getWidth() * view.getScaleX();
    return new Point(
            (int) (locationOnScreen[0] + viewWidth / 2),
            (int) (locationOnScreen[1] + viewHeight / 2));


private static void performPinch(UiController uiController, Point startPoint1, Point startPoint2, Point endPoint1, Point endPoint2) 
    final int duration = 500;
    final long eventMinInterval = 10;
    final long startTime = SystemClock.uptimeMillis();
    long eventTime = startTime;
    MotionEvent event;
    float eventX1, eventY1, eventX2, eventY2;

    eventX1 = startPoint1.x;
    eventY1 = startPoint1.y;
    eventX2 = startPoint2.x;
    eventY2 = startPoint2.y;

    // Specify the property for the two touch points
    MotionEvent.PointerProperties[] properties = new MotionEvent.PointerProperties[2];
    MotionEvent.PointerProperties pp1 = new MotionEvent.PointerProperties();
    pp1.id = 0;
    pp1.toolType = MotionEvent.TOOL_TYPE_FINGER;
    MotionEvent.PointerProperties pp2 = new MotionEvent.PointerProperties();
    pp2.id = 1;
    pp2.toolType = MotionEvent.TOOL_TYPE_FINGER;

    properties[0] = pp1;
    properties[1] = pp2;

    // Specify the coordinations of the two touch points
    // NOTE: you MUST set the pressure and size value, or it doesn't work
    MotionEvent.PointerCoords[] pointerCoords = new MotionEvent.PointerCoords[2];
    MotionEvent.PointerCoords pc1 = new MotionEvent.PointerCoords();
    pc1.x = eventX1;
    pc1.y = eventY1;
    pc1.pressure = 1;
    pc1.size = 1;
    MotionEvent.PointerCoords pc2 = new MotionEvent.PointerCoords();
    pc2.x = eventX2;
    pc2.y = eventY2;
    pc2.pressure = 1;
    pc2.size = 1;
    pointerCoords[0] = pc1;
    pointerCoords[1] = pc2;

    /*
     * Events sequence of zoom gesture:
     *
     * 1. Send ACTION_DOWN event of one start point
     * 2. Send ACTION_POINTER_DOWN of two start points
     * 3. Send ACTION_MOVE of two middle points
     * 4. Repeat step 3 with updated middle points (x,y), until reach the end points
     * 5. Send ACTION_POINTER_UP of two end points
     * 6. Send ACTION_UP of one end point
     */

    try 
        // Step 1
        event = MotionEvent.obtain(startTime, eventTime,
                MotionEvent.ACTION_DOWN, 1, properties,
                pointerCoords, 0, 0, 1, 1, 0, 0, 0, 0);
        injectMotionEventToUiController(uiController, event);

        // Step 2
        event = MotionEvent.obtain(startTime, eventTime,
                MotionEvent.ACTION_POINTER_DOWN + (pp2.id << MotionEvent.ACTION_POINTER_INDEX_SHIFT), 2,
                properties, pointerCoords, 0, 0, 1, 1, 0, 0, 0, 0);
        injectMotionEventToUiController(uiController, event);

        // Step 3, 4
        long moveEventNumber = duration / eventMinInterval;

        float stepX1, stepY1, stepX2, stepY2;

        stepX1 = (endPoint1.x - startPoint1.x) / moveEventNumber;
        stepY1 = (endPoint1.y - startPoint1.y) / moveEventNumber;
        stepX2 = (endPoint2.x - startPoint2.x) / moveEventNumber;
        stepY2 = (endPoint2.y - startPoint2.y) / moveEventNumber;

        for (int i = 0; i < moveEventNumber; i++) 
            // Update the move events
            eventTime += eventMinInterval;
            eventX1 += stepX1;
            eventY1 += stepY1;
            eventX2 += stepX2;
            eventY2 += stepY2;

            pc1.x = eventX1;
            pc1.y = eventY1;
            pc2.x = eventX2;
            pc2.y = eventY2;

            pointerCoords[0] = pc1;
            pointerCoords[1] = pc2;

            event = MotionEvent.obtain(startTime, eventTime,
                    MotionEvent.ACTION_MOVE, 2, properties,
                    pointerCoords, 0, 0, 1, 1, 0, 0, 0, 0);
            injectMotionEventToUiController(uiController, event);
        

        // Step 5
        pc1.x = endPoint1.x;
        pc1.y = endPoint1.y;
        pc2.x = endPoint2.x;
        pc2.y = endPoint2.y;
        pointerCoords[0] = pc1;
        pointerCoords[1] = pc2;

        eventTime += eventMinInterval;
        event = MotionEvent.obtain(startTime, eventTime,
                MotionEvent.ACTION_POINTER_UP + (pp2.id << MotionEvent.ACTION_POINTER_INDEX_SHIFT), 2, properties,
                pointerCoords, 0, 0, 1, 1, 0, 0, 0, 0);
        injectMotionEventToUiController(uiController, event);

        // Step 6
        eventTime += eventMinInterval;
        event = MotionEvent.obtain(startTime, eventTime,
                MotionEvent.ACTION_UP, 1, properties,
                pointerCoords, 0, 0, 1, 1, 0, 0, 0, 0);
        injectMotionEventToUiController(uiController, event);
     catch (InjectEventSecurityException e) 
        throw new RuntimeException("Could not perform pinch", e);
    


/**
 * Safely call uiController.injectMotionEvent(event): Detect any error and "convert" it to an
 * IllegalStateException
 */
private static void injectMotionEventToUiController(UiController uiController, MotionEvent event) throws InjectEventSecurityException 
    boolean injectEventSucceeded = uiController.injectMotionEvent(event);
    if (!injectEventSucceeded) 
        throw new IllegalStateException("Error performing event " + event);
    

【讨论】:

这确实应该是公认的答案。很棒的工作。一个建议是在步骤 6 完成后在 performPinch() 中添加 uiController.loopMainThreadForAtLeast(duration)。这会阻止测试在手势完成之前继续进行。 优秀的答案。请注意:如果您将 startDelta 保留为 0,您试图“捏”的视图可能会消失。当两个“手指”完全出现在同一个位置时,Android 似乎表现得很奇怪。可能会发生一些除以 0 的数学运算。 还有一个额外的错误:当stepX1stepY1 等在performPinch 中计算时,您将int 除以int 得到float。这会产生舍入/类型转换错误。我将moveEventNumber 转换为float 并修复了它。【参考方案3】:

为了使上面的代码与我使用的非弃用常量一起工作 MotionEvent.ACTION_POINTER_DOWN + (pp2.id &lt;&lt; MotionEvent.ACTION_POINTER_INDEX_SHIFT) 而不是 MotionEvent.ACTION_POINTER_2_DOWN

所以我不得不替换这一行:

event = MotionEvent.obtain(downTime, eventTime, MotionEvent.ACTION_POINTER_2_DOWN, 2, 属性, 指针坐标, 0, 0, 1, 1, 0, 0, 0, 0);

event = MotionEvent.obtain(downTime, eventTime, MotionEvent.ACTION_POINTER_DOWN + (pp2.id

【讨论】:

感谢您发布此更新!我假设使用 ACTION_POINTER_UP/DOWN,但没有意识到需要掩码。这让我很头疼!

以上是关于如何为 Android 测试生成缩放/捏合手势的主要内容,如果未能解决你的问题,请参考以下文章

平移手势(按住/拖动)缩放相机,如 Snapchat

UIScrollView 并取消缩放捏合手势

通过捏合手势缩放 UICollectionView

在 iOS 10 上的 WKWebView 中打开 PDF 文件时启用捏合缩放手势

捏合和点击手势无法正常工作

UIImageView 上的捏合、平移和双击手势