Espresso,当 NestedScrollView 或 RecyclerView 在 CoordinatorLayout 中时滚动不起作用

Posted

技术标签:

【中文标题】Espresso,当 NestedScrollView 或 RecyclerView 在 CoordinatorLayout 中时滚动不起作用【英文标题】:Espresso, scrolling not working when NestedScrollView or RecyclerView is in CoordinatorLayout 【发布时间】:2016-05-18 07:27:48 【问题描述】:

看起来CoordinatorLayout 破坏了 Espresso 操作的行为,例如 scrollTo()RecyclerViewActions.scrollToPosition()

NestedScrollView 的问题

对于这样的布局:

<android.support.design.widget.CoordinatorLayout
    android:layout_
    android:layout_>

    <android.support.v4.widget.NestedScrollView
        android:layout_
        android:layout_
        app:layout_behavior="@string/appbar_scrolling_view_behavior">

        ...

    </android.support.v4.widget.NestedScrollView>

    <android.support.design.widget.AppBarLayout
        android:layout_
        android:layout_ >

        ...

    </android.support.design.widget.AppBarLayout>

</android.support.design.widget.CoordinatorLayout>

如果我尝试使用ViewActions.scrollTo() 滚动到NestedScrollView 中的任何视图,我发现的第一个问题是我得到了PerformException。这是因为这个动作只支持ScrollViewNestedScrollView 没有扩展它。 here解释了这个问题的解决方法,基本上我们可以复制scrollTo()中的代码并更改约束以支持NestedScrollView。如果NestedScrollView 不在CoordinatorLayout 中,这似乎可行,但只要将其放入CoordinatorLayout 中,滚动操作就会失败。

RecyclerView 的问题

对于相同的布局,如果我将NestedScrollView 替换为RecyclerView,滚动也会出现问题。

在这种情况下,我使用的是RecyclerViewAction.scrollToPosition(position)。与NestedScrollView 不同,在这里我可以看到发生了一些滚动。但是,它看起来像滚动到错误的位置。例如,如果我滚动到最后一个位置,它会显示倒数第二个而不是最后一个。当我将RecyclerView 移出CoordinatorLayout 时,滚动工作正常。

由于这个问题,目前我们无法为使用 CoordinatorLayout 的屏幕编写任何 Espresso 测试。有没有遇到同样问题或知道解决方法的人?

【问题讨论】:

我有一个问题,RecycleView 在 NestedScrollview 中。我不能使用 recycleview.scrollToPosition(X); ,它只是不起作用。在过去的 6 天里,我尝试了所有方法,但我可以克服它。有什么建议吗?非常感谢! 【参考方案1】:

我必须测试 recyclerview 项目。我的 RecyclerView 位于 CoordinatorLayout 内的 NestedScrollView 中。

以下是对我有用的解决方案,我觉得这是在 NestedScrollView 中测试 RecyclerView 项目的最合适的解决方案。

第 1 步:复制并粘贴以下函数

以下将从我们即将测试的 recyclerView 返回所需的子视图。

fun atPositionOnView(recyclerViewId: Int, position: Int, childViewIdToTest: Int): Matcher<View?>? 
    return object : TypeSafeMatcher<View?>() 
        var resources: Resources? = null
        var childView: View? = null
        override fun describeTo(description: Description?) 
            var idDescription = Integer.toString(recyclerViewId)
            if (resources != null) 
                idDescription = try 
                    resources!!.getResourceName(recyclerViewId)
                 catch (var4: Resources.NotFoundException) 
                    String.format("%s (resource name not found)",
                            *arrayOf<Any?>(Integer.valueOf(recyclerViewId)))
                
            
            description?.appendText("with id: $idDescription")
        

        override fun matchesSafely(view: View?): Boolean 
            resources = view?.getResources()
            if (childView == null) 
                val recyclerView = view?.getRootView()?.findViewById(recyclerViewId) as RecyclerView
                childView = if (recyclerView != null && recyclerView.id == recyclerViewId) 
                    recyclerView.findViewHolderForAdapterPosition(position)!!.itemView
                 else 
                    return false
                
            

            return if (viewId == -1) 
                view === childView
             else 
                val targetView = childView!!.findViewById<View>(viewId)
                view === targetView
            
        
    

第 2 步:现在复制并粘贴以下函数

以下将检查您的孩子在 recyclerView 中是否正在显示。

fun ViewInteraction.isNotDisplayed(): Boolean 
    return try 
        check(matches(not(isDisplayed())))
        true
     catch (e: Error) 
        false
    

第 3 步:测试您的 recyclerView 项目并滚动它们是否不在屏幕上

以下将滚动未显示的孩子并使其出现在屏幕上。

if (onView(atPositionOnView(R.id.rv_items, pos, R.id.tv_item_name)).isNotDisplayed()) 
            val appViews = UiScrollable(UiSelector().scrollable(true))
            appViews.scrollForward() //appViews.scrollBackward()
        

一旦您的视图显示出来,您就可以执行您的测试用例了。

【讨论】:

变量viewId从何而来?它不在第一个函数的范围内。【参考方案2】:

这就是我在 Kotlin 中所做的与 @miszmaniac 所做的相同的事情。 使用delegation in Kotlin,它更简洁、更容易,因为我不必重写我不需要的方法。

class ScrollToAction(
    private val original: android.support.test.espresso.action.ScrollToAction = android.support.test.espresso.action.ScrollToAction()
) : ViewAction by original 

  override fun getConstraints(): Matcher<View> = anyOf(
      allOf(
          withEffectiveVisibility(Visibility.VISIBLE),
          isDescendantOfA(isAssignableFrom(NestedScrollView::class.java))),
      original.constraints
  )

【讨论】:

这对我有用。我还包括了一个scrollTo() 方法。 fun scrollTo(): ViewAction = actionWithAssertions(ScrollToAction()) 对于 androidx 变种:private val original: androidx.test.espresso.action.ScrollToAction = androidx.test.espresso.action.ScrollToAction()【参考方案3】:

Barista 的 scrollTo(R.id.button) 适用于各种可滚动视图,也适用于 NestedScrollView

使用 Espresso 解决此类问题非常有用。我们开发和使用它只是为了以快速可靠的方式编写 Espresso 测试。这是一个链接:https://github.com/SchibstedSpain/Barista

【讨论】:

【参考方案4】:

Mido 先生的解决方案可能在某些情况下有效,但并非总是如此。如果您在屏幕底部有一些视图,则 RecyclerView 的滚动将不会发生,因为单击将在 RecyclerView 之外开始。

解决此问题的一种方法是编写自定义 SwipeAction。像这样:

1 - 创建 CenterSwipeAction

public class CenterSwipeAction implements ViewAction 

    private final Swiper swiper;
    private final CoordinatesProvider startCoordProvide;
    private final CoordinatesProvider endCoordProvide;
    private final PrecisionDescriber precDesc;

    public CenterSwipeAction(Swiper swiper, CoordinatesProvider startCoordProvide,
                             CoordinatesProvider endCoordProvide, PrecisionDescriber precDesc) 
        this.swiper = swiper;
        this.startCoordProvide = startCoordProvide;
        this.endCoordProvide = endCoordProvide;
        this.precDesc = precDesc;
    

    @Override public Matcher<View> getConstraints() 
        return withEffectiveVisibility(ViewMatchers.Visibility.VISIBLE);
    

    @Override public String getDescription() 
        return "swipe from middle of screen";
    

    @Override
    public void perform(UiController uiController, View view) 
        float[] startCoord = startCoordProvide.calculateCoordinates(view);
        float[] finalCoord = endCoordProvide.calculateCoordinates(view);
        float[] precision =  precDesc.describePrecision();

        // you could try this for several times until Swiper.Status is achieved or try count is reached
        try 
            swiper.sendSwipe(uiController, startCoord, finalCoord, precision);
         catch (RuntimeException re) 
            throw new PerformException.Builder()
                    .withActionDescription(this.getDescription())
                    .withViewDescription(HumanReadables.describe(view))
                    .withCause(re)
                    .build();
        

        // ensures that the swipe has been run.
        uiController.loopMainThreadForAtLeast(ViewConfiguration.getPressedStateDuration());
    

2 - 创建返回 ViewAction 的方法

    private static ViewAction swipeFromCenterToTop() 
        return new CenterSwipeAction(Swipe.FAST,
                GeneralLocation.CENTER,
                view -> 
                    float[] coordinates =  GeneralLocation.CENTER.calculateCoordinates(view);
                    coordinates[1] = 0;
                    return coordinates;
                ,
                Press.FINGER);
    

3 - 然后用它来滚动屏幕:

onView(withId(android.R.id.content)).perform(swipeFromCenterToTop());

就是这样!通过这种方式,您可以控制屏幕上的滚动方式。

【讨论】:

【参考方案5】:

我在使用 CoordinatorLayout->ViewPager->NestedScrollView 时遇到了这个问题,我很容易获得相同的 scrollTo() 行为,只需在屏幕上向上滑动即可:

onView(withId(android.R.id.content)).perform(ViewActions.swipeUp());

【讨论】:

如果屏幕底部有按钮,此解决方案将不起作用 =/ 这个解决方法对我有用,但只能与另一个结合使用。我复制了ActionOnItemAtPositionViewAction 并禁用了滚动删除行new ScrollToPositionViewAction(position).perform(uiController, view); 所以我有:onView(withId(android.R.id.content)).perform(ViewActions.swipeUp()); onView(withId(R.id.my_list)).perform(new CopyOfActionOnItemAtPositionViewAction(13, click()));【参考方案6】:

我已经创建了一个 NestedScrollViewScrollToAction 类。

我认为在那里制作特定于活动的东西会更好。

唯一值得一提的是代码搜索父级nestedScrollView 并移除了它的CoordinatorLayout 行为。

https://gist.github.com/miszmaniac/12f720b7e898ece55d2464fe645e1f36

【讨论】:

在我的情况下,我有 CoordinatorLayout->ViewPager->NestedScrollView 和 scrollTo() 不起作用。我更新了脚本,它现在可以滚动了,谢谢@miszmaniac。更新脚本:gist.github.com/maydin/677c983c11d6a75c90186c09366fef2f 您可以像下面这样使用 ViewInteraction appCompatButton3 = onView( allOf(withId(R.id.profile_btn_save), withText("Save Changes"))); appCompatButton3.perform(NestedScrollViewScrollToAction.scrollTo(),click());【参考方案7】:

此问题已报告(可能由 OP?),请参阅 Issue 203684

当 NestedScrollView 位于 CoordinatorLayout 内部时,该问题的其中一个 cmets 建议解决该问题:

您需要删除 ScrollingView 或包含此 ScrollingView 的任何父视图的 @string/appbar_scrolling_view_behavior 布局行为

这是该解决方法的一个实现:

    activity.runOnUiThread(new Runnable() 
        @Override
        public void run() 
            // remove CoordinatorLayout.LayoutParams from NestedScrollView
            NestedScrollView nestedScrollView = (NestedScrollView)activity.findViewById(scrollViewId);
            CoordinatorLayout.LayoutParams params =
                    (CoordinatorLayout.LayoutParams)nestedScrollView.getLayoutParams();
            params.setBehavior(null);
            nestedScrollView.requestLayout();
        
    );

我能够通过以下方式进行测试:

    制作自定义 scrollTo() 操作(由 OP 和 Turnsole 引用) 删除 NestedScrollView 的布局参数,如下所示

【讨论】:

这对我来说效果很好。我在NestedScrollView 和我的CoordinatorLayout 之间有另一个视图,所以我不得不删除这个视图的布局参数,CoordinatorLayout 的直接子视图。【参考方案8】:

这是因为 Espresso scrollTo() 方法显式检查布局类并且仅适用于 ScrollView 和 Horizo​​ntalScrollView。在内部它使用 View.requestRectangleOnScreen(...) 所以我希望它实际上适用于许多布局。

我对 NestedScrollView 的解决方法是采用 ScrollToAction 并修改该约束。修改后的操作对 NestedScrollView 的更改效果很好。

ScrollToAction 类中的更改方法:

public Matcher<View> getConstraints() 
    return allOf(withEffectiveVisibility(Visibility.VISIBLE), isDescendantOfA(anyOf(
            isAssignableFrom(ScrollView.class), isAssignableFrom(HorizontalScrollView.class), isAssignableFrom(NestedScrollView.class))));

方便方法:

public static ViewAction betterScrollTo() 
    return ViewActions.actionWithAssertions(new NestedScrollToAction());

【讨论】:

您的NestedScrollViewCoordinatorLayout 内吗?我试过这个,它似乎只有在 NestedScrollView 不在 CoordinatorLayout 中时才有效。 @ivacf 在CoordinatorLayout 中为我工作 This 答案可能会帮助您滚动到嵌套滚动视图,因为它包含匹配器的完整代码。

以上是关于Espresso,当 NestedScrollView 或 RecyclerView 在 CoordinatorLayout 中时滚动不起作用的主要内容,如果未能解决你的问题,请参考以下文章

使用 Espresso 在索引处选择子视图

Android Marshmallow:使用 Espresso 测试权限?

Android Espresso,RecyclerViewActions 请求权限

如何使用 Espresso 执行多点触控滑动?

使用 Android-Espresso 运行多个测试时出现内存不足异常

Android Espresso 在 GridView 上使用 onData() 匹配