Как щелкнуть clickablespan с помощью эспрессо?

У меня есть текстовое окно с несколькими кликабельными промежутками. Я хочу, чтобы у меня была возможность проверить нажатие этих интервалов.

Я попробовал настроить пользовательский ViewAction, который найдет области clickables в 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

Ответ 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

Это работает для меня:

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

Ответ 3

вы можете использовать Spannable вместо SpannableString, совместимого с SpannableStringBuilder.

Извините, я новый человек, имею только 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();

            }
        };
    }

}

Ответ 4

Лучшим вариантом будет подкласс ViewAction. Вот как это сделать в Котлине:

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