如何使用浓缩咖啡点击可点击的跨度?

How to click a clickablespan using espresso?

我有一个文本视图,里面有多个可点击的跨度。我希望能够测试点击这些跨度。

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

因此,我无法区分 link 跨度。有更好的方法吗?

添加跨度:

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

这对我有用:

/**
 * 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);
}

这是我的解决方案。它更简单,因为我们不需要找到坐标。找到 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"));

您可以使用 Spannable 而不是 SpannableStringSpannableStringBuilder 兼容。

sorry,我是新人,只有1个Reputation,不能加comment.Even我的英文很差.....

我建议使用:

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

而不是:

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

post 所有代码如下:

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();

            }
        };
    }

}

最好的选择是子类化 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))

这是已接受答案的 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()

        }
    }
} 

稍作改动即可奏效。
只需重新检查 "textToClick" 和变量 "sequence" in:

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

完全一样。

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

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

因为我的 textToClick 值是 "click here" 和我得到的序列值“点击这里”

注:"click"前的space。

我希望这对某人有用。

Espresso 对此有一个单线:

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