Android Espresso(四)——RecyclerView

Android Espresso(四)——RecyclerView

RecyclerView是ListView的一个高阶版本,可以被用以实现ListView,GridView的功能。但它本身并非一个AdapterView,因此onData()方式不能用以测试RecyclerView,需要使用onView()测试。同样,在espresso-core库中并没有RecyclerView适用的ViewAction。RecyclerViewAction被定义espresso-contrib库中,因此需要在build.gradle文件的dependencies闭包中添加依赖

dependencies {
    androidTestImplementation "com.android.support.test.espresso:espresso-contrib:3.0.2"
}

RecyclerViewActions API

看下 RecyclerViewActions 的主要API。

可以从上边看到,主要的操作通过 position,viewholder进行匹配,后执行action。

RecyclerView已定义API测试代码

先看下RecyclerView滚动的测试,并执行action。

@RunWith(AndroidJUnit4.class)
public class RecyclerViewTest {

    @Rule
    public ActivityTestRule<RecyclerViewListActivity> activityTestRule = new ActivityTestRule<>(RecyclerViewListActivity.class);

    @LargeTest
    @Test
    public void testRecyclerView() throws InterruptedException {
        // RecyclerView滚动到position是35的位置,这里的scrollTo()方法定义在espresso-core的ViewActioin内
        onView(withId(R.id.recyclerView)).perform(actionOnItemAtPosition(35, scrollTo()));
        Thread.sleep(200); // 为了执行速度慢些设置Thread sleep
        // RecyclerView滚动到position是1的位置,这里的scrollToPosition()方法定义在RecyclerViewActions内
        onView(withId(R.id.recyclerView)).perform(scrollToPosition(1));
        Thread.sleep(500);
        // RecyclerView滚动到positioin是19的位置,并且执行click操作
        onView(withId(R.id.recyclerView)).perform(scrollToPosition(19), actionOnItemAtPosition(19, click()));
    }
}

执行效果如下。

在这里插入图片描述

RecyclerView自定义ViewAction

RecyclerViewActions中定义的方法很有限,在使用过程中很难匹配到所有场景。因此,需要自定义 ViewAction 应用到RecyclerView。例如,在RecyclerView的Item中同时有CheckBoxTextView,在这种场景下,需要选中匹配标题的 CheckBox,这样的场景下,只使用 RecyclerViewActions 无法很好满足。

在这里插入图片描述

如上的效果图中,需要先确定到标题是 This is element #25 的Item,然后选中对应的CheckBox组件。

这种场景查看 RecyclerViewActions 中定义的API,就知道无法直接适用于这样的场景。自定义的 ViewAction 就需要被使用到。

ViewAction

先看看 ViewAction的定义。

public interface ViewAction {

  public Matcher<View> getConstraints();

  public String getDescription();

  public void perform(UiController uiController, View view);
}

ViewAction 定义很简单,就是一个包含三个方法定义的接口。

  • getConstraints() 返回查找匹配组件的Matcher。用以查找需要进行操作的目标View。
  • getDescription() 用以描述当前 ViewAction
  • perform(UiController, View) 用以定义在View上的操作。第二个参数View即是通过 getConstraint() 匹配到的View。

CustomRecyclerViewAction

看下上图效果的 ViewAction.

interface CustomRecyclerViewAction extends ViewAction {

    class ClickRecyclerViewItemCheckBoxWithTitle implements CustomRecyclerViewAction {

        // 通过这个title,确定item在RecyclerView的位置
        private final String itemViewTitle;

        public ClickRecyclerViewItemCheckBoxWithTitle(String title) {
            itemViewTitle = title;
        }

        @Override
        public Matcher<View> getConstraints() { // 匹配进行操作的View。这里查找RecyclerView及其子类,并且已经显示
            return allOf(isAssignableFrom(RecyclerView.class), isDisplayed());
        }

        @Override
        public String getDescription() { // 添加对ViewAction的描述
            return "Click the checkbox widget in the item view matched with specific title";
        }

        // 这里定义在view上的操作。
        @Override
        public void perform(UiController uiController, View view) {
            RecyclerView recyclerView = (RecyclerView) view;
            RecyclerView.Adapter adapter = recyclerView.getAdapter();

            int itemCount = adapter.getItemCount();

            try {
                // 遍历 RecyclerView 内的item。
                for (int i = 0; i < itemCount; i++) {
                    View itemView = recyclerView.getLayoutManager().findViewByPosition(i);

                    if (itemView != null) {
                        TextView textView = itemView.findViewById(R.id.textView);
                        // 搜索匹配 itemViewTitle 的Item
                        if (textView != null && textView.getText() != null) {
                            if (TextUtils.equals(itemViewTitle, textView.getText())) {
                                CheckBox checkBox = itemView.findViewById(R.id.ckb_selected);
                                checkBox.toggle();
                                break;
                            }
                        }
                    } else {
                        recyclerView.scrollToPosition(i);
                        uiController.loopMainThreadForAtLeast(500); // 向主线程消息队列发送等待500ms的消息,这样可以放慢滚动的速度
                        i--;
                    }
                }
                uiController.loopMainThreadUntilIdle();
            } catch (RuntimeException e) {
                throw new PerformException.Builder()
                    .withActionDescription(this.getDescription())
                    .withViewDescription(HumanReadables.describe(view)).withCause(e)
                    .build();
            }
        }
    }
}

上述这段代码表示在 RecyclerView 中遍历,找到显示标题值等于 itemViewTitle 的item,并点击item中的CheckBox.

在自定义ViewAction中,getConstraints(), getDescription() 方法定义都类似,最重要的就是 perform(UiController uiController, View view) 定义。

遇到的问题

若匹配的item在屏幕之外,那么滚动 RecyclerView 并进行逐条匹配。为了快速滚动,考虑了翻页,每屏显示固定数量,然后进行匹配。方法perform(UiController uiController, View view)定义如下。

@Override
public void perform(UiController uiController, View view) {
    RecyclerView recyclerView = (RecyclerView) view;
    RecyclerView.Adapter adapter = recyclerView.getAdapter();

    int itemCount = adapter.getItemCount();
    int screenCount = 0;
    int screenIndex = 1;

    try {
        for (int i = 0; i < itemCount; i++) {
            View itemView = recyclerView.getLayoutManager().findViewByPosition(i);

            if (itemView != null) {
                TextView textView = itemView.findViewById(R.id.textView);
                if (textView != null && textView.getText() != null) {
                    if (TextUtils.equals(itemViewTitle, textView.getText())) {
                        CheckBox checkBox = itemView.findViewById(R.id.ckb_selected);
                        checkBox.toggle();
                        break;
                    }
                }
            } else {
                if (screenCount == 0) { // 当遍历到itemview等于null时,说明对应的itemview超出屏幕显示范围
                    screenCount = i;    // 每屏显示的数量
                }
                recyclerView.scrollToPosition(i + screenCount); // 滚动到下一屏
                uiController.loopMainThreadForAtLeast(500);
                i--;
            }
        }
        uiController.loopMainThreadUntilIdle();
    } catch (RuntimeException e) {
        throw new PerformException.Builder()
            .withActionDescription(this.getDescription())
            .withViewDescription(HumanReadables.describe(view)).withCause(e)
            .build();
    }
}

美好的想法,但是运行的结果是在直接滚动一屏后,代码View itemView = recyclerView.getLayoutManager().findViewByPosition(i);获取的返回结果是null

因此这个写法是错误的。

总结

  1. RecyclerView 内数据少,itemView布局简单的前提下,使用 RecyclerViewActions 基于 Position 的API 就可以解决问题。
  2. RecyclerView 数据多,无法直接根据 Position 定位 item 的情况下,可以考虑使用 自定义ViewAction 进行匹配找到 itemView。
  • 1
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 1
    评论
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

VoidHope

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值