【问题标题】:How to click a clickablespan using espresso?如何使用浓缩咖啡点击可点击范围?
【发布时间】:2016-07-11 18:49:12
【问题描述】:

我有一个文本视图,其中包含多个可点击的跨度。我希望能够测试单击这些跨度。

我尝试设置一个自定义 ViewAction,它会在 TextView 中找到可点击的跨度,然后将它们的文本与所需的文本匹配,然后单击该文本的 xy 坐标。但是,似乎添加到 TextView 的跨度不是 ClickableSpan 类型,而是添加跨度的片段。

因此,我无法区分链接跨度。有没有更好的方法来做到这一点?

添加跨度:

Util.addClickableSpan(spannableString, string, linkedString, new      ClickableSpan() {
@Override
public void onClick(View textView) {}
});

tvAcceptTc.setText(spannableString);
tvAcceptTc.setMovementMethod(LinkMovementMethod.getInstance());

实用方法:

public static void addClickableSpan(SpannableString spannableString,
                              String text,
                              String subText,
                              ClickableSpan clickableSpan) {
        int start = text.indexOf(subText);
        int end = text.indexOf(subText) + subText.length();
        int flags = Spanned.SPAN_EXCLUSIVE_EXCLUSIVE;

        spannableString.setSpan(clickableSpan, start, end, flags);
}

定义 ViewAction:

@Override
        public void perform(UiController uiController, View view) {
            uiController.loopMainThreadUntilIdle();
            if (view instanceof TextView) {

                TextView textView = (TextView) view;
                Layout textViewLayout = textView.getLayout();


                SpannableString fullSpannable = new SpannableString(textView.getText());

                Object[] spans = fullSpannable.getSpans(0, fullSpannable.length(), Object.class);

                ClickableSpan span = null;
                for (Object object : spans) {
                    if (object instanceof BaseFragment) {
                        ClickableSpan foundSpan = (ClickableSpan)object;
                        int spanStart = fullSpannable.getSpanStart(foundSpan);
                        int spanEnd = fullSpannable.getSpanEnd(foundSpan);
                        if (fullSpannable.subSequence(spanStart, spanEnd).equals(aSubstring)) {
                            //Found the correct span!
                            span = foundSpan;
                        }
                    }
                } ... go on to click the xy-coordinates

【问题讨论】:

  • 如何添加跨度?你试过打电话给TextUtils#dumpSpans吗?
  • 我添加了用于添加跨度的代码。如果我删除 instanceof 检查和强制转换,它现在实际上可以工作,但它会找到带有文本的任何跨度,而不仅仅是 ClickableSpan。我查看了调试器中的跨度,它们都不是 ClickableSpan 类型,而是来自添加跨度的片段。
  • 查看getSpans的最后一个参数
  • 你是说把 ClickableSpan 放在那里? spans 数组并没有带回任何 ClickableSpans,所以它不会只返回一个空数组吗?
  • 你尝试调用 dumpSpans 了吗?

标签: android testing android-espresso


【解决方案1】:

这是我的解决方案。它更简单,因为我们不需要找到坐标。找到 ClickableSpan 后,我们只需点击它:

public static ViewAction clickClickableSpan(final CharSequence textToClick) {
    return new ViewAction() {
        @Override
        public Matcher<View> getConstraints() {
            return Matchers.instanceOf(TextView.class);
        }

        @Override
        public String getDescription() {
            return "clicking on a ClickableSpan";
        }

        @Override
        public void perform(UiController uiController, View view) {
            TextView textView = (TextView) view;
            SpannableString spannableString = (SpannableString) textView.getText();

            if (spannableString.length() == 0) {
                // TextView is empty, nothing to do
                throw new NoMatchingViewException.Builder()
                        .includeViewHierarchy(true)
                        .withRootView(textView)
                        .build();
            }

            // Get the links inside the TextView and check if we find textToClick
            ClickableSpan[] spans = spannableString.getSpans(0, spannableString.length(), ClickableSpan.class);
            if (spans.length > 0) {
                ClickableSpan spanCandidate;
                for (ClickableSpan span : spans) {
                    spanCandidate = span;
                    int start = spannableString.getSpanStart(spanCandidate);
                    int end = spannableString.getSpanEnd(spanCandidate);
                    CharSequence sequence = spannableString.subSequence(start, end);
                    if (textToClick.toString().equals(sequence.toString())) {
                        span.onClick(textView);
                        return;
                    }
                }
            }

            // textToClick not found in TextView
            throw new NoMatchingViewException.Builder()
                    .includeViewHierarchy(true)
                    .withRootView(textView)
                    .build();

        }
    };
}

现在您可以像这样使用我们的自定义 ViewAction:

    onView(withId(R.id.myTextView)).perform(clickClickableSpan("myLink"));

【讨论】:

【解决方案2】:

这是接受答案的 Kotlin 版本

fun clickClickableSpan(textToClick: CharSequence): ViewAction {
    return object : ViewAction {

        override fun getConstraints(): Matcher<View> {
            return Matchers.instanceOf(TextView::class.java)
        }

        override fun getDescription(): String {
            return "clicking on a ClickableSpan";
        }

        override fun perform(uiController: UiController, view: View) {
            val textView = view as TextView
            val spannableString = textView.text as SpannableString

            if (spannableString.isEmpty()) {
                // TextView is empty, nothing to do
                throw NoMatchingViewException.Builder()
                        .includeViewHierarchy(true)
                        .withRootView(textView)
                        .build();
            }

            // Get the links inside the TextView and check if we find textToClick
            val spans = spannableString.getSpans(0, spannableString.length, ClickableSpan::class.java)
            if (spans.isNotEmpty()) {
                var spanCandidate: ClickableSpan
                for (span: ClickableSpan in spans) {
                    spanCandidate = span
                    val start = spannableString.getSpanStart(spanCandidate)
                    val end = spannableString.getSpanEnd(spanCandidate)
                    val sequence = spannableString.subSequence(start, end)
                    if (textToClick.toString().equals(sequence.toString())) {
                        span.onClick(textView)
                        return;
                    }
                }
            }

            // textToClick not found in TextView
            throw NoMatchingViewException.Builder()
                    .includeViewHierarchy(true)
                    .withRootView(textView)
                    .build()

        }
    }
} 

【讨论】:

    【解决方案3】:

    最好的选择是继承 ViewAction。这是在 Kotlin 中的做法:

    class SpannableTextClickAction(val text: String) : ViewAction {
        override fun getDescription(): String = "SpannableText click action"
    
        override fun getConstraints(): Matcher<View> =
                isAssignableFrom(TextView::class.java)
    
        override fun perform(uiController: UiController?, view: View?) {
            val textView = view as TextView
            val spannableString = textView.text as SpannableString
            val spans = spannableString.getSpans(0, spannableString.count(), ClickableSpan::class.java)
            val spanToLocate = spans.firstOrNull { span: ClickableSpan ->
                val start = spannableString.getSpanStart(span)
                val end = spannableString.getSpanEnd(span)
                val spanText = spannableString.subSequence(start, end).toString()
                spanText == text
            }
            if (spanToLocate != null) {
                spanToLocate.onClick(textView)
                return
            }
            // textToClick not found in TextView
            throw NoMatchingViewException.Builder()
                    .includeViewHierarchy(true)
                    .withRootView(textView)
                    .build()
        }
    }
    

    并将其用作:

    onView(withId(<view_id>)).perform(scrollTo(), SpannableTextClickAction(text))
    

    【讨论】:

      【解决方案4】:

      它做了一个小的改动。
      只需重新检查“textToClick”和变量“sequence”:

      CharSequence sequence = spannableString.subSequence(start, end);
      

      完全一样。

      我必须像这样使用 trim():

      textToClick.toString() == sequence.trim().toString()
      

      因为我的 textToClick 值是“单击此处”,而序列值是“单击此处”

      注意:“点击”前的空格。

      我希望这对某人有用。

      【讨论】:

        【解决方案5】:

        这对我有用:

        /**
         * Clicks the first ClickableSpan in the TextView
         */
        public static ViewAction clickFirstClickableSpan() {
            return new GeneralClickAction(
                    Tap.SINGLE,
                    new CoordinatesProvider() {
                        @Override
                        public float[] calculateCoordinates(View view) {
                            //https://leons.im/posts/how-to-get-coordinate-of-a-clickablespan-inside-a-textview/
                            TextView textView = (TextView) view;
                            Rect parentTextViewRect = new Rect();
        
                            SpannableString spannableString = (SpannableString) textView.getText();
                            Layout textViewLayout = textView.getLayout();
                            ClickableSpan spanToLocate = null;
        
                            if (spannableString.length() == 0) {
                                return new float[2];
                            }
        
                            ClickableSpan[] spans = spannableString.getSpans(0, spannableString.length(), ClickableSpan.class);
                            if (spans.length > 0) {
                                spanToLocate = spans[0];
                            }
        
                            if (spanToLocate == null) {
                                // no specific view found
                                throw new NoMatchingViewException.Builder()
                                        .includeViewHierarchy(true)
                                        .withRootView(textView)
                                        .build();
                            }
        
                            double startOffsetOfClickedText = spannableString.getSpanStart(spanToLocate);
                            double endOffsetOfClickedText = spannableString.getSpanEnd(spanToLocate);
                            double startXCoordinatesOfClickedText = textViewLayout.getPrimaryHorizontal((int) startOffsetOfClickedText);
                            double endXCoordinatesOfClickedText = textViewLayout.getPrimaryHorizontal((int) endOffsetOfClickedText);
        
                            // Get the rectangle of the clicked text
                            int currentLineStartOffset = textViewLayout.getLineForOffset((int) startOffsetOfClickedText);
                            int currentLineEndOffset = textViewLayout.getLineForOffset((int) endOffsetOfClickedText);
                            boolean keywordIsInMultiLine = currentLineStartOffset != currentLineEndOffset;
                            textViewLayout.getLineBounds(currentLineStartOffset, parentTextViewRect);
        
                            // Update the rectangle position to his real position on screen
                            int[] parentTextViewLocation = {0, 0};
                            textView.getLocationOnScreen(parentTextViewLocation);
        
                            double parentTextViewTopAndBottomOffset = (
                                    parentTextViewLocation[1] -
                                            textView.getScrollY() +
                                            textView.getCompoundPaddingTop()
                            );
                            parentTextViewRect.top += parentTextViewTopAndBottomOffset;
                            parentTextViewRect.bottom += parentTextViewTopAndBottomOffset;
                            parentTextViewRect.left += (
                                    parentTextViewLocation[0] +
                                            startXCoordinatesOfClickedText +
                                            textView.getCompoundPaddingLeft() -
                                            textView.getScrollX()
                            );
                            parentTextViewRect.right = (int) (
                                    parentTextViewRect.left +
                                            endXCoordinatesOfClickedText -
                                            startXCoordinatesOfClickedText
                            );
        
                            int screenX = (parentTextViewRect.left + parentTextViewRect.right) / 2;
                            int screenY = (parentTextViewRect.top + parentTextViewRect.bottom) / 2;
                            if (keywordIsInMultiLine) {
                                screenX = parentTextViewRect.left;
                                screenY = parentTextViewRect.top;
                            }
                            return new float[]{screenX, screenY};
                        }
                    },
                    Press.FINGER);
        }
        

        【讨论】:

          【解决方案6】:

          您可以使用与SpannableStringBuilder 兼容的Spannable 而不是SpannableString

          对不起,我是新人,只有1个声望,不能加评论。连我的英文都很差.....

          我建议使用:

          Spannable spannableString = (Spannable) textView.getText();
          

          而不是:

          SpannableString spannableString = (SpannableString) textView.getText();
          

          在下面发布所有代码:

          public class CustomViewActions {
          
              /**
               * click specific spannableString
               */
              public static ViewAction clickClickableSpan(final CharSequence textToClick) {
                  return clickClickableSpan(-1, textToClick);
              }
          
              /**
               * click the first spannableString
               */
              public static ViewAction clickClickableSpan() {
                  return clickClickableSpan(0, null);
              }
          
              /**
               * click the nth spannableString
               */
              public static ViewAction clickClickableSpan(final int index) {
                  return clickClickableSpan(index, null);
              }
          
              public static ViewAction clickClickableSpan(final int index,final CharSequence textToClick) {
                  return new ViewAction() {
                      @Override
                      public Matcher<View> getConstraints() {
                          return instanceOf(TextView.class);
                      }
          
                      @Override
                      public String getDescription() {
                          return "clicking on a ClickableSpan";
                      }
          
                      @Override
                      public void perform(UiController uiController, View view) {
                          TextView textView = (TextView) view;
                          Spannable spannableString = (Spannable) textView.getText();
                          ClickableSpan spanToLocate = null;
                          if (spannableString.length() == 0) {
                              // TextView is empty, nothing to do
                              throw new NoMatchingViewException.Builder()
                                      .includeViewHierarchy(true)
                                      .withRootView(textView)
                                      .build();
                          }
          
                          // Get the links inside the TextView and check if we find textToClick
                          ClickableSpan[] spans = spannableString.getSpans(0, spannableString.length(), ClickableSpan.class);
          
                          if (spans.length > 0) {
                              if(index >=spans.length){
                                  throw new NoMatchingViewException.Builder()
                                      .includeViewHierarchy(true)
                                      .withRootView(textView)
                                      .build();
                              }else if (index >= 0) {
                                  spanToLocate = spans[index];
                                  spanToLocate.onClick(textView);
                                  return;
                              }
                              for (int i = 0; i < spans.length; i++) {
                                  int start = spannableString.getSpanStart(spans[i]);
                                  int end = spannableString.getSpanEnd(spans[i]);
                                  CharSequence sequence = spannableString.subSequence(start, end);
                                  if (textToClick.toString().equals(sequence.toString())) {
                                      spanToLocate = spans[i];
                                      spanToLocate.onClick(textView);
                                      return;
                                  }
                              }
                          }
          
                          // textToClick not found in TextView
                          throw new NoMatchingViewException.Builder()
                                  .includeViewHierarchy(true)
                                  .withRootView(textView)
                                  .build();
          
                      }
                  };
              }
          
          }
          

          【讨论】:

          • 虽然这可能是解决问题的宝贵提示,但一个好的答案也可以证明解决方案。请EDIT 提供示例代码来说明您的意思。或者,考虑将其写为评论
          • 顺便说一句,直接onClick 可能会错过一些错误。例如,textView 被其他可点击区域屏蔽。这是我未经测试的猜测。
          【解决方案7】:

          Espresso 有一个专门的方法:

          onView(withId(R.id.textView)).perform(openLinkWithText("..."))
          

          【讨论】:

            猜你喜欢
            • 2015-10-03
            • 1970-01-01
            • 1970-01-01
            • 1970-01-01
            • 1970-01-01
            • 1970-01-01
            • 1970-01-01
            • 1970-01-01
            • 1970-01-01
            相关资源
            最近更新 更多