Espresso,在 CoordinatorLayout 中使用 NestedScrollView 或 RecyclerView 时无法正常滚动

44

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

与NestedScrollView相关的问题

对于这样的布局:

<android.support.design.widget.CoordinatorLayout
    android:layout_width="match_parent"
    android:layout_height="match_parent">

    <android.support.v4.widget.NestedScrollView
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        app:layout_behavior="@string/appbar_scrolling_view_behavior">

        ...

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

    <android.support.design.widget.AppBarLayout
        android:layout_width="match_parent"
        android:layout_height="wrap_content" >

        ...

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

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

如果我尝试使用 ViewActions.scrollTo() 滚动到嵌套在 NestedScrollView 中的任何视图,我遇到的第一个问题是出现了 PerformException。这是因为此操作仅支持 ScrollView ,而 NestedScrollView 并没有继承它。解决此问题的方法在此处here解释,基本上我们可以复制 scrollTo() 中的代码并更改限制以支持 NestedScrollView。如果 NestedScrollView 不在 CoordinatorLayout 中,则似乎可以正常工作,但是一旦将其放入 CoordinatorLayout 中,滚动操作将失败。

RecyclerView 的问题

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

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

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


1
我有一个问题,RecycleView在NestedScrollview中。我无法使用recycleview.scrollToPosition(X);,它就是不起作用。我已经尝试了过去6天的所有方法,但我无法解决它。有什么建议吗?非常感谢! - narancs
8个回答

31

这是由于Espresso的scrollTo()方法显式检查布局类,并且仅适用于ScrollView和HorizontalScrollView。在内部,它使用View.requestRectangleOnScreen(...),因此我预计它实际上可以适用于许多布局。

我的妥协方案是修改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());
}

7
你的NestedScrollView是否在一个CoordinatorLayout中?我尝试过,只有当NestedScrollView不在CoordinatorLayout中时才有效。 - ivacf
1
@ivacf 在 CoordinatorLayout 中对我有效。 - Rab Ross
这个答案可能会帮助您滚动到嵌套的ScrollView,因为它包含了匹配器的完整代码。 - Francislainy Campos

28

这里是我用 Kotlin 做与 @miszmaniac 相同的事情的方法。 使用 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
  )
}

5
这对我很有效。我还包括了一个scrollTo()方法。fun scrollTo(): ViewAction = actionWithAssertions(ScrollToAction()) - moyheen
5
对于androidx变体:private val original: androidx.test.espresso.action.ScrollToAction = androidx.test.espresso.action.ScrollToAction() - Rule
希望有一个scrollViewToTop函数。 - undefined

17

我曾经遇到了使用CoordinatorLayout->ViewPager->NestedScrollView的问题,从我的解决方案来看,获取相同的scrollTo()行为的简单方法就是在屏幕上向上滑动:

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

4
如果屏幕底部有一个按钮,这个解决方案将无法使用 =/ - Leandro Borges Ferreira
这个解决方法对我有效,但只能与另一个方法结合使用。我复制了 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())); - Ignacio Tomas Crespo

6
先生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());
    }
}

创建返回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

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

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


4
这个问题已经被报告了(也许是由原帖作者?),请参见问题203684。该问题的一个评论提供了一个解决方法,当NestedScrollView在CoordinatorLayout中时,可以解决这个问题:

你需要删除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();
        }
    });

通过以下步骤,我成功使我的测试工作:

  1. 创建自定义的scrollTo()方法(参考了OP和Turnsole的建议)
  2. 按照此处所示的方式删除NestedScrollView的布局参数

这对我很有效。在我的“NestedScrollView”和“CoordinatorLayout”之间还有另一个视图,所以我必须删除此视图的布局参数,它是“CoordinatorLayout”的直接子项。 - Diego Malone

2

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

1
我需要翻译以下内容:

我必须测试recyclerview项目。我的RecyclerView在NestedScrollView中,而这个NestedScrollView又在CoordinatorLayout中。

以下是适用于我的解决方案,我认为这是最适合测试嵌套在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
    }
}

步骤三:测试您的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来自哪里?它不在第一个函数的范围内。 - Alex Conner

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接