Android UI 测试长按拖拽

Posted

技术标签:

【中文标题】Android UI 测试长按拖拽【英文标题】:Android UI Test Long Click and Drag 【发布时间】:2017-12-31 18:45:58 【问题描述】:

在我的应用程序中,我有一个功能,即用户可以通过长按创建拖动阴影来重新排序列表中的项目,然后用户可以将其拖动并插入到选择的位置。丢弃后,这些物品会重新排序。

我正在努力为此开发 UI 测试。我可以成功地长按该项目,以创建拖动阴影或实现拖动动作。我似乎无法将两者结合为一个动作。

我在我的 android UI 测试中使用 Espresso 和 Barista。

对于长按,我使用了 Barista 的 API:

longClickOn("ITEM");

对于拖动动作,我尝试创建自己的 Espresso ViewAction:

return new ViewAction() 
        @Override
        public Matcher<View> getConstraints() 
            return isAssignableFrom(ViewGroup.class);
        

        @Override
        public String getDescription() 
            return "Swiping child " + srcIndex + " to child " + destIndex;
        

        @Override
        public void perform(UiController uiController, View view) 
            ViewGroup parent = (ViewGroup) view;

            final View srcChild = parent.getChildAt(srcIndex);
            final View destChild = parent.getChildAt(destIndex);

            final CoordinatesProvider srcCoordinatesProvider = new CoordinatesProvider() 
                @Override
                public float[] calculateCoordinates(View view) 
                    int[] location = new int[2];
                    srcChild.getLocationInWindow(location);
                    float x = location[0] + (view.getMeasuredWidth() / 2);
                    float y = location[1] + (view.getMeasuredHeight() / 2);

                    return new float[] x, y;
                
            ;

            final CoordinatesProvider destCoordinatesProvider = new CoordinatesProvider() 
                @Override
                public float[] calculateCoordinates(View view) 
                    int[] location = new int[2];
                    destChild.getLocationInWindow(location);
                    float x = location[0] + (view.getMeasuredWidth() / 2);
                    float y = location[1] + (view.getMeasuredHeight() / 2);

                    return new float[] x, y;
                
            ;

            GeneralSwipeAction swipe = new GeneralSwipeAction(Swipe.FAST,
                    srcCoordinatesProvider, destCoordinatesProvider, Press.FINGER);
            swipe.perform(uiController, parent);
        
    ;

编辑:

根据@Be_Negative 的回答,我定制了给定的答案并想出了这个:

private static ViewAction drag(final int srcIndex, final int destIndex) 
    return new ViewAction() 
        @Override
        public Matcher<View> getConstraints() 
            return isAssignableFrom(ViewGroup.class);
        

        @Override
        public String getDescription() 
            return "Swiping child " + srcIndex + " to child " + destIndex;
        

        @Override
        public void perform(UiController uiController, View view) 
            ViewGroup parent = (ViewGroup) view;

            uiController.loopMainThreadUntilIdle();
            final View srcChild = parent.getChildAt(srcIndex);
            final View destChild = parent.getChildAt(destIndex);

            final CoordinatesProvider coordinatesProvider = getCoordinatesProdvider();

            float[] precision = Press.PINPOINT.describePrecision();
            MotionEvent downEvent = MotionEvents.sendDown(uiController, coordinatesProvider.calculateCoordinates(srcChild), precision).down;

            try 
                long longPressTimeout = (long) (ViewConfiguration.getLongPressTimeout() * 1.5);
                uiController.loopMainThreadForAtLeast(longPressTimeout);

                float[][] steps = interpolateDragging(
                        coordinatesProvider.calculateCoordinates(srcChild),
                        coordinatesProvider.calculateCoordinates(destChild)
                );

                uiController.loopMainThreadUntilIdle();

                for(float[] step : steps) 
                    if( !MotionEvents.sendMovement(uiController, downEvent, step)) 
                        MotionEvents.sendCancel(uiController, downEvent);
                    
                

                if(!MotionEvents.sendUp(uiController, downEvent, coordinatesProvider.calculateCoordinates(destChild))) 
                    MotionEvents.sendCancel(uiController, downEvent);
                
             catch(Exception e) 
                System.out.println(e);
             finally 
                downEvent.recycle();
            
        
    ;

不幸的是,它在MotionEvents.sendMovement 处抛出错误,如下所示:

Error performing 'inject motion event (corresponding down event: MotionEvent  action=ACTION_DOWN, actionButton=0, id[0]=0, x[0]=540.0, y[0]=302.0, toolType[0]=TOOL_TYPE_UNKNOWN, buttonState=BUTTON_PRIMARY, metaState=0, flags=0x0, edgeFlags=0x0, pointerCount=1, historySize=0, eventTime=34586525, downTime=34586525, deviceId=0, source=0x1002 )' on view 'unknown'.

【问题讨论】:

【参考方案1】:

这是我的想法。我严重依赖long press 和swipe 的现有实现将它们组合在一起。有一些分歧的想法 - 例如,默认实现中的拖动插值器往往会下冲,所以我不得不稍微调整一下。

class DragAndDropAction(private val sourceViewPosition: Int,
                        private val targetViewPosition: Int) : ViewAction 

    override fun getConstraints(): Matcher<View> 
        return allOf(isDisplayed(), isAssignableFrom(RecyclerView::class.java))
    

    override fun getDescription(): String 
        return "Drag and drop action"
    

    override fun perform(uiController: UiController, view: View) 
        val recyclerView: RecyclerView = view as RecyclerView
        //Sending down
        recyclerView.scrollToPosition(sourceViewPosition)
        uiController.loopMainThreadUntilIdle()
        val sourceView = recyclerView.findViewHolderForAdapterPosition(sourceViewPosition).itemView

        val sourceViewCenter = GeneralLocation.VISIBLE_CENTER.calculateCoordinates(sourceView)
        val fingerPrecision = Press.FINGER.describePrecision()

        val downEvent = MotionEvents.sendDown(uiController, sourceViewCenter, fingerPrecision).down
        try 
            // Factor 1.5 is needed, otherwise a long press is not safely detected.
            val longPressTimeout = (ViewConfiguration.getLongPressTimeout() * 1.5f).toLong()
            uiController.loopMainThreadForAtLeast(longPressTimeout)

            //Drag to the position
            recyclerView.scrollToPosition(targetViewPosition)
            uiController.loopMainThreadUntilIdle()
            val targetView = recyclerView.findViewHolderForAdapterPosition(targetViewPosition).itemView
            val targetViewLocation = if (targetViewPosition > sourceViewPosition) 
                GeneralLocation.BOTTOM_CENTER.calculateCoordinates(targetView)
             else 
                GeneralLocation.TOP_CENTER.calculateCoordinates(targetView)
            

            val steps = interpolate(sourceViewCenter, targetViewLocation)

            for (i in 0 until steps.size) 
                if (!MotionEvents.sendMovement(uiController, downEvent, steps[i])) 
                    MotionEvents.sendCancel(uiController, downEvent)
                
            

            //Release
            if (!MotionEvents.sendUp(uiController, downEvent, targetViewLocation)) 
                MotionEvents.sendCancel(uiController, downEvent)
            
         finally 
            downEvent.recycle()
        
    

    private val SWIPE_EVENT_COUNT = 10

    private fun interpolate(start: FloatArray, end: FloatArray): Array<FloatArray> 
        val res = Array(SWIPE_EVENT_COUNT)  FloatArray(2) 

        for (i in 1..SWIPE_EVENT_COUNT) 
            res[i - 1][0] = start[0] + (end[0] - start[0]) * i / SWIPE_EVENT_COUNT
            res[i - 1][1] = start[1] + (end[1] - start[1]) * i / SWIPE_EVENT_COUNT
        

        return res
    

还有很大的改进空间,例如,理想情况下,我希望我的操作可以在视图匹配器中进行而不是位置。除此之外,它似乎一切正常。

【讨论】:

感谢您的帮助。不幸的是,它仍然无法正常工作。请查看我的编辑以获取更多信息。 它似乎开始接触系统 UI 元素之一(键盘、通知面板或底部按钮栏)。我怀疑您需要将滚动合并到您的视图操作中。就我而言,我正在使用回收站视图并滚动到项目并拖动项目。如果你不在recyclerview中,可以使用ScrollToAction作为参考 实际上,您可以从自定义ViewMatchers.withParentIndex() 调用视图操作可能很方便,或者编写自己的匹配器来匹配给定视图(默认情况下,espresso 不提供这样的匹配器是理所当然的)因为它可能被滥用)

以上是关于Android UI 测试长按拖拽的主要内容,如果未能解决你的问题,请参考以下文章

RecyclerView侧滑菜单,滑动删除,长按拖拽,下拉刷新上拉加载

RecyclerView侧滑菜单,滑动删除,长按拖拽,下拉刷新上拉加载

Swift 4.0iOS 11 UICollectionView 长按拖拽删除崩溃的问题

Android测试随笔模拟长按电源键

iPhone 4 iOS 5 高级ScrollView 技术:长按拖放按钮

如何在 Xamarin UI 测试中触发 Android LongClick