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

看起来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。这是因为这个动作只支持ScrollViewNestedScrollView 没有扩展它。 here解释了这个问题的解决方法,基本上我们可以复制scrollTo()中的代码并更改约束以支持NestedScrollView。如果NestedScrollView 不在CoordinatorLayout 中,这似乎可行,但只要将其放入CoordinatorLayout 中,滚动操作就会失败。

RecyclerView 的问题

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

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

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

【问题讨论】:

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

标签: android android-recyclerview android-espresso coordinator-layout android-nestedscrollview


【解决方案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();
              }
          });
      

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

      1. 制作自定义 scrollTo() 操作(由 OP 和 Turnsole 引用)
      2. 删除 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 答案可能会帮助您滚动到嵌套滚动视图,因为它包含匹配器的完整代码。
      猜你喜欢
      • 1970-01-01
      • 1970-01-01
      • 2019-09-17
      • 1970-01-01
      • 1970-01-01
      • 2017-01-16
      • 1970-01-01
      • 2016-03-17
      • 2017-02-05
      相关资源
      最近更新 更多