如何使用浓缩咖啡点击可点击的跨度?
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
而不是 SpannableString
与 SpannableStringBuilder
兼容。
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("..."))
我有一个文本视图,里面有多个可点击的跨度。我希望能够测试点击这些跨度。
我尝试设置一个自定义 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
而不是 SpannableString
与 SpannableStringBuilder
兼容。
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("..."))