Align TextView to another multiline TextView bottom line [duplicate] - android

I have the text "Android is a Software stack". In this text i want to set the "stack" text as clickable. So, if you click on that it will redirected to a new activity(not in the browser).
I tried but i am not getting a solution.

android.text.style.ClickableSpan can solve your problem.
SpannableString ss = new SpannableString("Android is a Software stack");
ClickableSpan clickableSpan = new ClickableSpan() {
#Override
public void onClick(View textView) {
startActivity(new Intent(MyActivity.this, NextActivity.class));
}
#Override
public void updateDrawState(TextPaint ds) {
super.updateDrawState(ds);
ds.setUnderlineText(false);
}
};
ss.setSpan(clickableSpan, 22, 27, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
TextView textView = (TextView) findViewById(R.id.hello);
textView.setText(ss);
textView.setMovementMethod(LinkMovementMethod.getInstance());
textView.setHighlightColor(Color.TRANSPARENT);
In XML:
<TextView
...
android:textColorLink="#drawable/your_selector"
/>

My function for make multiple links inside TextView
Update 2020: Now this function able to support multiple same texts link inside 1 TextView, but remember to put the link in the correct order
fun TextView.makeLinks(vararg links: Pair<String, View.OnClickListener>) {
val spannableString = SpannableString(this.text)
var startIndexOfLink = -1
for (link in links) {
val clickableSpan = object : ClickableSpan() {
override fun updateDrawState(textPaint: TextPaint) {
// use this to change the link color
textPaint.color = textPaint.linkColor
// toggle below value to enable/disable
// the underline shown below the clickable text
textPaint.isUnderlineText = true
}
override fun onClick(view: View) {
Selection.setSelection((view as TextView).text as Spannable, 0)
view.invalidate()
link.second.onClick(view)
}
}
startIndexOfLink = this.text.toString().indexOf(link.first, startIndexOfLink + 1)
// if(startIndexOfLink == -1) continue // todo if you want to verify your texts contains links text
spannableString.setSpan(
clickableSpan, startIndexOfLink, startIndexOfLink + link.first.length,
Spanned.SPAN_EXCLUSIVE_EXCLUSIVE
)
}
this.movementMethod =
LinkMovementMethod.getInstance() // without LinkMovementMethod, link can not click
this.setText(spannableString, TextView.BufferType.SPANNABLE)
}
USING
my_text_view.makeLinks(
Pair("Terms of Service", View.OnClickListener {
Toast.makeText(applicationContext, "Terms of Service Clicked", Toast.LENGTH_SHORT).show()
}),
Pair("Privacy Policy", View.OnClickListener {
Toast.makeText(applicationContext, "Privacy Policy Clicked", Toast.LENGTH_SHORT).show()
}))
XML
<TextView
android:id="#+id/my_text_view"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="Please accept Terms of Service and Privacy Policy"
android:textColorHighlight="#f00" // background color when pressed
android:textColorLink="#0f0"
android:textSize="20sp" />
DEMO
Reference
Solution for clear the link highlight selection follow https://stackoverflow.com/a/19445108/5381331

You can use ClickableSpan as described in this post
Sample code:
TextView myTextView = new TextView(this);
String myString = "Some text [clickable]";
int i1 = myString.indexOf("[");
int i2 = myString.indexOf("]");
myTextView.setMovementMethod(LinkMovementMethod.getInstance());
myTextView.setText(myString, BufferType.SPANNABLE);
Spannable mySpannable = (Spannable)myTextView.getText();
ClickableSpan myClickableSpan = new ClickableSpan() {
#Override
public void onClick(View widget) { /* do something */ }
};
mySpannable.setSpan(myClickableSpan, i1, i2 + 1, Spannable.SPAN_EXCLUSIVE_EXCLUSIVE);
Reference

You can use sample code. You want to learn detail about ClickableSpan. Please check this documentaion
SpannableString myString = new SpannableString("This is example");
ClickableSpan clickableSpan = new ClickableSpan() {
#Override
public void onClick(View textView) {
ToastUtil.show(getContext(),"Clicked Smile ");
}
};
//For Click
myString.setSpan(clickableSpan,startIndex,lastIndex,Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
//For UnderLine
myString.setSpan(new UnderlineSpan(),startIndex,lastIndex,0);
//For Bold
myString.setSpan(new StyleSpan(Typeface.BOLD),startIndex,lastIndex,0);
//Finally you can set to textView.
TextView textView = (TextView) findViewById(R.id.txtSpan);
textView.setText(myString);
textView.setMovementMethod(LinkMovementMethod.getInstance());

I made this helper method in case someone need start and end position from a String.
public static TextView createLink(TextView targetTextView, String completeString,
String partToClick, ClickableSpan clickableAction) {
SpannableString spannableString = new SpannableString(completeString);
// make sure the String is exist, if it doesn't exist
// it will throw IndexOutOfBoundException
int startPosition = completeString.indexOf(partToClick);
int endPosition = completeString.lastIndexOf(partToClick) + partToClick.length();
spannableString.setSpan(clickableAction, startPosition, endPosition,
Spanned.SPAN_INCLUSIVE_EXCLUSIVE);
targetTextView.setText(spannableString);
targetTextView.setMovementMethod(LinkMovementMethod.getInstance());
return targetTextView;
}
And here is how you use it
private void initSignUp() {
String completeString = "New to Reddit? Sign up here.";
String partToClick = "Sign up";
ClickableTextUtil
.createLink(signUpEditText, completeString, partToClick,
new ClickableSpan() {
#Override
public void onClick(View widget) {
// your action
Toast.makeText(activity, "Start Sign up activity",
Toast.LENGTH_SHORT).show();
}
#Override
public void updateDrawState(TextPaint ds) {
super.updateDrawState(ds);
// this is where you set link color, underline, typeface etc.
int linkColor = ContextCompat.getColor(activity, R.color.blumine);
ds.setColor(linkColor);
ds.setUnderlineText(false);
}
});
}

Here is a Kotlin method to make parts of a TextView clickable:
private fun makeTextLink(textView: TextView, str: String, underlined: Boolean, color: Int?, action: (() -> Unit)? = null) {
val spannableString = SpannableString(textView.text)
val textColor = color ?: textView.currentTextColor
val clickableSpan = object : ClickableSpan() {
override fun onClick(textView: View) {
action?.invoke()
}
override fun updateDrawState(drawState: TextPaint) {
super.updateDrawState(drawState)
drawState.isUnderlineText = underlined
drawState.color = textColor
}
}
val index = spannableString.indexOf(str)
spannableString.setSpan(clickableSpan, index, index + str.length, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE)
textView.text = spannableString
textView.movementMethod = LinkMovementMethod.getInstance()
textView.highlightColor = Color.TRANSPARENT
}
It can be called multiple times to create several links within a TextView:
makeTextLink(myTextView, str, false, Color.RED, action = { Log.d("onClick", "link") })
makeTextLink(myTextView, str1, true, null, action = { Log.d("onClick", "link1") })

t= (TextView) findViewById(R.id.PP1);
t.setText(Html.fromHtml("<bThis is normal text </b>" +
"This is cliclable text "));
t.setMovementMethod(LinkMovementMethod.getInstance());

I would suggest a different approach that I think requires less code and is more "localization-friendly".
Supposing that your destination activity is called "ActivityStack", define in the manifest an intent filter for it with a custom scheme (e.g. "myappscheme") in AndroidManifest.xml:
<activity
android:name=".ActivityStack">
<intent-filter>
<action android:name="android.intent.action.VIEW" />
<category android:name="android.intent.category.DEFAULT" />
<data android:host="stack"/>
<data android:scheme="myappscheme" />
</intent-filter>
</activity>
Define the TextView without any special tag (it is important to NOT use the "android:autoLink" tag, see: https://stackoverflow.com/a/20647011/1699702):
<TextView
android:id="#+id/stackView"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="#string/stack_string" />
then use a link with custom scheme and host in the text of the TextView as (in String.xml):
<string name="stack_string">Android is a Software stack</string>
and "activate" the link with setMovementMethod() (in onCreate() for activities or onCreateView() for fragments):
TextView stack = findViewById(R.id.stackView);
stack.setMovementMethod(LinkMovementMethod.getInstance());
This will open the stack activity with a tap on the "stack" word.

Kotlin Version of Phan Van Linh's answer.
Please note it has some minor modifications.
fun makeLinks(textView: TextView, links: Array<String>, clickableSpans: Array<ClickableSpan>) {
val spannableString = SpannableString(textView.text)
for (i in links.indices) {
val clickableSpan = clickableSpans[i]
val link = links[i]
val startIndexOfLink = textView.text.indexOf(link)
spannableString.setSpan(clickableSpan, startIndexOfLink, startIndexOfLink + link.length,
Spanned.SPAN_EXCLUSIVE_EXCLUSIVE)
}
textView.movementMethod = LinkMovementMethod.getInstance()
textView.setText(spannableString, TextView.BufferType.SPANNABLE)
}
fun setupClickableTextView() {
val termsOfServicesClick = object : ClickableSpan() {
override fun onClick(p0: View?) {
Toast.makeText(applicationContext, "ToS clicked", Toast.LENGTH_SHORT).show()
}
}
val privacyPolicyClick = object : ClickableSpan() {
override fun onClick(p0: View?) {
Toast.makeText(applicationContext, "PP clicked", Toast.LENGTH_SHORT).show()
}
}
makeLinks(termsTextView, arrayOf("terms", "privacy policy"), arrayOf(termsOfServicesClick, privacyPolicyClick))
}

You can you this method to set the clickable value
public void setClickableString(String clickableValue, String wholeValue, TextView yourTextView){
String value = wholeValue;
SpannableString spannableString = new SpannableString(value);
int startIndex = value.indexOf(clickableValue);
int endIndex = startIndex + clickableValue.length();
spannableString.setSpan(new ClickableSpan() {
#Override
public void updateDrawState(TextPaint ds) {
super.updateDrawState(ds);
ds.setUnderlineText(false); // <-- this will remove automatic underline in set span
}
#Override
public void onClick(View widget) {
// do what you want with clickable value
}
}, startIndex, endIndex, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
yourTextView.setText(spannableString);
yourTextView.setMovementMethod(LinkMovementMethod.getInstance()); // <-- important, onClick in ClickableSpan won't work without this
}
This is how to use it:
TextView myTextView = findViewById(R.id.myTextView);
setClickableString("stack", "Android is a Software stack", myTextView);

For those that are looking for a solution in Kotlin here is what worked for me:
private fun setupTermsAndConditions() {
val termsAndConditions = resources.getString(R.string.terms_and_conditions)
val spannableString = SpannableString(termsAndConditions)
val clickableSpan = object : ClickableSpan() {
override fun onClick(widget: View) {
if (checkForWifiAndMobileInternet()) {
// binding.viewModel!!.openTermsAndConditions()
showToast("Good, open the link!!!")
} else {
showToast("Cannot open this file because of internet connection!")
}
}
override fun updateDrawState(textPaint : TextPaint) {
super.updateDrawState(textPaint)
textPaint.color = resources.getColor(R.color.colorGrey)
textPaint.isFakeBoldText = true
}
}
spannableString.setSpan(clickableSpan, 34, 86, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE)
binding.tvTermsAndConditions.text = spannableString
binding.tvTermsAndConditions.movementMethod = LinkMovementMethod.getInstance()
binding.tvTermsAndConditions.setHighlightColor(Color.TRANSPARENT);
}

Created elegant Kotlin way with extension:
fun TextView.setClickableText(text: Spanned,
clickableText: String,
#ColorInt clickableColor: Int,
clickListener: () -> Unit) {
val spannableString = SpannableString(text)
val startingPosition: Int = text.indexOf(clickableText)
if (startingPosition > -1) {
val clickableSpan: ClickableSpan = object : ClickableSpan() {
override fun onClick(textView: View) {
clickListener()
}
override fun updateDrawState(textPaint: TextPaint) {
super.updateDrawState(textPaint)
textPaint.isUnderlineText = false
}
}
val endingPosition: Int = startingPosition + clickableText.length
spannableString.setSpan(clickableSpan, startingPosition,
endingPosition, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE)
spannableString.setSpan(ForegroundColorSpan(clickableColor), startingPosition,
endingPosition, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE)
movementMethod = LinkMovementMethod.getInstance()
highlightColor = Color.TRANSPARENT
}
setText(spannableString)
}

Boom Check this for java Lovers :D
We can modify it according to our need:
List<Pair<String, View.OnClickListener>> pairsList = new ArrayList<>();
pairsList.add(new Pair<>("38,50", v -> {
Intent intent = new Intent(SignUpActivity.this, WebActivity.class);
intent.putExtra("which", "tos");
startActivity(intent);
}));
pairsList.add(new Pair<>("81,95", v -> {
Intent intent = new Intent(SignUpActivity.this, WebActivity.class);
intent.putExtra("which", "policy");
startActivity(intent);
}));
makeLinks(pairsList); // Method calling
private void makeLinks(List<Pair<String, View.OnClickListener>> pairsList) {
SpannableString ss = new SpannableString(By signing up, I’m agree to PAKRISM’s Terms of Use and confirms that I have read Privacy Policy);
for (Pair<String, View.OnClickListener> pair : pairsList) {
ClickableSpan clickableSpan = new ClickableSpan() {
#Override
public void onClick(View textView) {
//Toast.makeText(MyApplication.getAppContext(), "Clicked!", Toast.LENGTH_SHORT).show();
pair.second.onClick(textView);
}
#Override
public void updateDrawState(TextPaint ds) {
ds.linkColor = ContextCompat.getColor(SignUpActivity.this, R.color.primary_main);
ds.setUnderlineText(true);
super.updateDrawState(ds);
}
};
String[] indexes = pair.first.split(",");
ss.setSpan(clickableSpan, Integer.parseInt(indexes[0]), Integer.parseInt(indexes[1]), Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
}
TextView tv = findViewById(R.id.txtView);
tv.setText(ss);
tv.setMovementMethod(LinkMovementMethod.getInstance());
}

For kotlin use this extension
fun TextView.makeLinks(vararg links: Pair<String, View.OnClickListener>) {
val spannableString = SpannableString(this.text)
for (link in links) {
val clickableSpan = object : ClickableSpan() {
override fun onClick(view: View) {
Selection.setSelection((view as TextView).text as Spannable, 0)
view.invalidate()
link.second.onClick(view)
}
}
val startIndexOfLink = this.text.toString().indexOf(link.first)
spannableString.setSpan(
clickableSpan, startIndexOfLink, startIndexOfLink + link.first.length,
Spanned.SPAN_EXCLUSIVE_EXCLUSIVE
)
spannableString.setSpan(
ForegroundColorSpan(Color.parseColor("#46C2CC")),
startIndexOfLink,
startIndexOfLink + link.first.length,
Spanned.SPAN_EXCLUSIVE_EXCLUSIVE
)
}
this.movementMethod =
LinkMovementMethod.getInstance() // without LinkMovementMethod, link can not click
this.setText(spannableString, TextView.BufferType.SPANNABLE)
}
Call this like this
binding.agreeText.makeLinks(Pair(getString(R.string.terms_conditionsClick),View.OnClickListener {
startActivity(TermsAndConditionActivity.getIntent(this))
}))

For bold,
mySpannable.setSpan(new StyleSpan(Typeface.BOLD),termStart,termStop,Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);

i coded an example to solve your question in Kotlin.
This is the Code:
val completeText = getString(R.string.terms_description)
val textToFind = getString(R.string.show_terms)
val spannableString: Spannable = SpannableString(completeText)
val startFocus = completeText.indexOf(textToFind)
val endFocus = startFocus + textToFind.length
spannableString.setSpan(object: ClickableSpan() {
override fun onClick(p0: View) {
showMessage()
}
}, startFocus, endFocus, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE)
show_terms.text = spannableString
show_terms.movementMethod = LinkMovementMethod.getInstance();
show_terms.highlightColor = Color.TRANSPARENT;
This is the XML
<CheckBox
android:id="#+id/check_agree_terms"
android:layout_width="wrap_content"
android:layout_height="wrap_content"/>
<TextView
android:id="#+id/show_terms"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:textColorLink="#color/colorPrimary"
android:layout_toEndOf="#id/check_agree_terms"/>
This is how it looks
enter image description here

Using URLSpan class to get urls
val spans: Array<URLSpan> = result.getSpans(0, result.length, URLSpan::class.java)
Method
fun TextView.createClickable(string: String, action:(String)->Unit ) {
text = HtmlCompat.fromHtml(string, HtmlCompat.FROM_HTML_MODE_LEGACY)
val result = SpannableString(text)
val spans = result.getSpans(0, result.length, URLSpan::class.java)
for (span in spans) {
val link:Pair<String, View.OnClickListener> = Pair(span.url, View.OnClickListener {
action(span.url)
})
val start = result.getSpanStart(span)
val end = result.getSpanEnd(span)
val flags = result.getSpanFlags(span)
result.removeSpan(span)
val clickableSpan: ClickableSpan = object : ClickableSpan() {
override fun onClick(textView: View) {
textView.invalidate()
link.second.onClick(textView)
}
override fun updateDrawState(textPaint: TextPaint) {
super.updateDrawState(textPaint)
textPaint.isUnderlineText = false
}
}
result.setSpan(clickableSpan, start, end, flags)
this.movementMethod = LinkMovementMethod.getInstance()
this.setText(result, TextView.BufferType.SPANNABLE)
}
}
Use
Example Text : Android is a Software Stack and it' Awesome
Wrapper your clickable text inside anchor tag
Like: Android is a Software <a href='https://example.com/stack'> Stack </a> and it' <a href='https://example.com/awesome'> Awesome </a>.
val str = "Android is a Software <a href='https://example.com/stack'> Stack </a> and it' <a href='https://example.com/awesome'> Awesome </a>."
textView.createClickable(str) {
when(it) {
"https://example.com/stack"->{
startActivity(Intent(this,StackActivity::class.java))
}
"https://example.com/awesom"->{
startActivity(Intent(this,AwesomeActivity::class.java))
}
}
}

Here a Kotlin solution that work better with localization:
data class LinkedText(#StringRes val textRes: Int, val clickListener: View.OnClickListener? = null)
fun TextView.setPartiallyLinkedText(vararg texts: LinkedText) {
this.text = texts.joinToString(" ") { context.getString(it.textRes) }
val spannableString = SpannableString(this.text)
var startIndexOfLink = -1
texts.forEach { text ->
val string = context.getString(text.textRes)
if (text.clickListener != null) {
val clickableSpan = object : ClickableSpan() {
override fun updateDrawState(textPaint: TextPaint) {
textPaint.color = textPaint.linkColor
textPaint.isUnderlineText = true
}
override fun onClick(view: View) {
Selection.setSelection((view as TextView).text as Spannable, 0)
view.invalidate()
text.clickListener.onClick(view)
}
}
startIndexOfLink = this.text.toString().indexOf(string, startIndexOfLink + 1)
spannableString.setSpan(
clickableSpan, startIndexOfLink, startIndexOfLink + string.length,
Spanned.SPAN_EXCLUSIVE_EXCLUSIVE
)
}
}
this.movementMethod = LinkMovementMethod.getInstance()
this.setText(spannableString, TextView.BufferType.SPANNABLE)
}
And use it like this:
textView.setPartiallyLinkedText(
LinkedText(R.string.not_linked_text),
LinkedText(R.string.linked_text) {
Toast.makeText(context, "You clicked", Toast.LENGTH_LONG).show()
},
)

Here is a kotlin extension (not works if text repeats)
textView.makeTextClickable(
contentText = "Hello world",
clickableText = "world",
isBoldText = true,
onClick = {
openPageInBrowser(BuildConfig.PRIVACY_POLICY_URL)
}
)
internal fun TextView.makeTextClickable(
contentText: String,
clickableText: String,
onClick: () -> Unit,
isUnderlineText: Boolean = false,
isBoldText: Boolean = false
) {
val spannableString = SpannableString(contentText)
val clickableSpan = object : ClickableSpan() {
override fun onClick(widget: View) {
onClick()
}
override fun updateDrawState(ds: TextPaint) {
super.updateDrawState(ds)
ds.isUnderlineText = isUnderlineText
ds.isFakeBoldText = isBoldText
}
}
val clickableTextStartIndex = text.indexOf(clickableText)
val clickableTextEndIndex = clickableTextStartIndex + clickableText.length
spannableString.setSpan(
clickableSpan,
clickableTextStartIndex,
clickableTextEndIndex,
Spanned.SPAN_EXCLUSIVE_EXCLUSIVE
)
this.text = spannableString
this.movementMethod = LinkMovementMethod.getInstance()
}

This is my MovementMethod for detecting link/text/image clicks. It is modified LinkMovementMethod.
import android.text.Layout;
import android.text.NoCopySpan;
import android.text.Selection;
import android.text.Spannable;
import android.text.method.ScrollingMovementMethod;
import android.text.style.ClickableSpan;
import android.text.style.ImageSpan;
import android.text.style.URLSpan;
import android.view.KeyEvent;
import android.view.MotionEvent;
import android.view.View;
import android.widget.TextView;
public class ClickMovementMethod extends ScrollingMovementMethod {
private Object FROM_BELOW = new NoCopySpan.Concrete();
private static final int CLICK = 1;
private static final int UP = 2;
private static final int DOWN = 3;
private Listener listener;
public void setListener(Listener listener) {
this.listener = listener;
}
#Override
public boolean canSelectArbitrarily() {
return true;
}
#Override
protected boolean handleMovementKey(TextView widget, Spannable buffer, int keyCode,
int movementMetaState, KeyEvent event) {
switch (keyCode) {
case KeyEvent.KEYCODE_DPAD_CENTER:
case KeyEvent.KEYCODE_ENTER:
if (KeyEvent.metaStateHasNoModifiers(movementMetaState)) {
if (event.getAction() == KeyEvent.ACTION_DOWN &&
event.getRepeatCount() == 0 && action(CLICK, widget, buffer)) {
return true;
}
}
break;
}
return super.handleMovementKey(widget, buffer, keyCode, movementMetaState, event);
}
#Override
protected boolean up(TextView widget, Spannable buffer) {
if (action(UP, widget, buffer)) {
return true;
}
return super.up(widget, buffer);
}
#Override
protected boolean down(TextView widget, Spannable buffer) {
if (action(DOWN, widget, buffer)) {
return true;
}
return super.down(widget, buffer);
}
#Override
protected boolean left(TextView widget, Spannable buffer) {
if (action(UP, widget, buffer)) {
return true;
}
return super.left(widget, buffer);
}
#Override
protected boolean right(TextView widget, Spannable buffer) {
if (action(DOWN, widget, buffer)) {
return true;
}
return super.right(widget, buffer);
}
private boolean action(int what, TextView widget, Spannable buffer) {
Layout layout = widget.getLayout();
int padding = widget.getTotalPaddingTop() +
widget.getTotalPaddingBottom();
int areatop = widget.getScrollY();
int areabot = areatop + widget.getHeight() - padding;
int linetop = layout.getLineForVertical(areatop);
int linebot = layout.getLineForVertical(areabot);
int first = layout.getLineStart(linetop);
int last = layout.getLineEnd(linebot);
ClickableSpan[] candidates = buffer.getSpans(first, last, URLSpan.class);
int a = Selection.getSelectionStart(buffer);
int b = Selection.getSelectionEnd(buffer);
int selStart = Math.min(a, b);
int selEnd = Math.max(a, b);
if (selStart < 0) {
if (buffer.getSpanStart(FROM_BELOW) >= 0) {
selStart = selEnd = buffer.length();
}
}
if (selStart > last)
selStart = selEnd = Integer.MAX_VALUE;
if (selEnd < first)
selStart = selEnd = -1;
switch (what) {
case CLICK:
if (selStart == selEnd) {
return false;
}
if (listener != null) {
URLSpan[] link = buffer.getSpans(selStart, selEnd, URLSpan.class);
if (link.length >= 1) {
listener.onClick(link[0].getURL());
} else {
ImageSpan[] image = buffer.getSpans(selStart, selEnd, ImageSpan.class);
if (image.length >= 1) {
listener.onImageClicked(image[0].getSource());
} else {
listener.onTextClicked();
}
}
}
break;
case UP:
int beststart, bestend;
beststart = -1;
bestend = -1;
for (int i = 0; i < candidates.length; i++) {
int end = buffer.getSpanEnd(candidates[i]);
if (end < selEnd || selStart == selEnd) {
if (end > bestend) {
beststart = buffer.getSpanStart(candidates[i]);
bestend = end;
}
}
}
if (beststart >= 0) {
Selection.setSelection(buffer, bestend, beststart);
return true;
}
break;
case DOWN:
beststart = Integer.MAX_VALUE;
bestend = Integer.MAX_VALUE;
for (int i = 0; i < candidates.length; i++) {
int start = buffer.getSpanStart(candidates[i]);
if (start > selStart || selStart == selEnd) {
if (start < beststart) {
beststart = start;
bestend = buffer.getSpanEnd(candidates[i]);
}
}
}
if (bestend < Integer.MAX_VALUE) {
Selection.setSelection(buffer, beststart, bestend);
return true;
}
break;
}
return false;
}
#Override
public boolean onTouchEvent(TextView widget, Spannable buffer,
MotionEvent event) {
int action = event.getAction();
if (action == MotionEvent.ACTION_UP ||
action == MotionEvent.ACTION_DOWN) {
int x = (int) event.getX();
int y = (int) event.getY();
x -= widget.getTotalPaddingLeft();
y -= widget.getTotalPaddingTop();
x += widget.getScrollX();
y += widget.getScrollY();
Layout layout = widget.getLayout();
int line = layout.getLineForVertical(y);
int off = layout.getOffsetForHorizontal(line, x);
URLSpan[] link = buffer.getSpans(off, off, URLSpan.class);
if (action == MotionEvent.ACTION_UP) {
if (listener != null) {
if (link.length >= 1) {
listener.onClick(link[0].getURL());
} else {
ImageSpan[] image = buffer.getSpans(off, off, ImageSpan.class);
if (image.length >= 1) {
listener.onImageClicked(image[0].getSource());
} else if (Selection.getSelectionStart(buffer) == Selection.getSelectionEnd(buffer)) {
listener.onTextClicked();
}
}
}
}
if (action == MotionEvent.ACTION_DOWN && link.length != 0) {
Selection.setSelection(buffer,
buffer.getSpanStart(link[0]),
buffer.getSpanEnd(link[0]));
return true;
}
if (link.length == 0) {
Selection.removeSelection(buffer);
}
}
return super.onTouchEvent(widget, buffer, event);
}
#Override
public void initialize(TextView widget, Spannable text) {
Selection.removeSelection(text);
text.removeSpan(FROM_BELOW);
}
#Override
public void onTakeFocus(TextView view, Spannable text, int dir) {
Selection.removeSelection(text);
if ((dir & View.FOCUS_BACKWARD) != 0) {
text.setSpan(FROM_BELOW, 0, 0, Spannable.SPAN_POINT_POINT);
} else {
text.removeSpan(FROM_BELOW);
}
}
public interface Listener {
void onClick(String clicked);
void onTextClicked();
void onImageClicked(String source);
}
}

more generic answer in kotlin
fun setClickableText(view: TextView, firstSpan: String, secondSpan: String) {
val context = view.context
val builder = SpannableStringBuilder()
val unClickableSpan = SpannableString(firstSpan)
val span = SpannableString(" "+secondSpan)
builder.append(unClickableSpan);
val clickableSpan: ClickableSpan = object : ClickableSpan() {
override fun onClick(textView: View) {
val intent = Intent(context, HomeActivity::class.java)
context.startActivity(intent)
}
override fun updateDrawState(ds: TextPaint) {
super.updateDrawState(ds)
ds.isUnderlineText = true
ds.setTypeface(Typeface.create(Typeface.DEFAULT, Typeface.ITALIC));
}
}
builder.append(span);
builder.setSpan(clickableSpan, firstSpan.length, firstSpan.length+secondSpan.length+1, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE)
view.setText(builder,TextView.BufferType.SPANNABLE)
view.setMovementMethod(LinkMovementMethod.getInstance());
}

The solutions provided are pretty decent. However, I generally use a more simple solution.
Here is a linkify utility function
/**
* Method is used to Linkify words in a TextView
*
* #param textView TextView who's text you want to change
* #param textToLink The text to turn into a link
* #param url The url you want to send the user to
*/
fun linkify(textView: TextView, textToLink: String, url: String) {
val pattern = Pattern.compile(textToLink)
Linkify.addLinks(textView, pattern, url, { _, _, _ -> true })
{ _, _ -> "" }
}
Using this function is pretty simple. Here is an example
// terms and privacy
val tvTerms = findViewById<TextView>(R.id.tv_terms)
val tvPrivacy = findViewById<TextView>(R.id.tv_privacy)
Utils.linkify(tvTerms, resources.getString(R.string.terms),
Constants.TERMS_URL)
Utils.linkify(tvPrivacy, resources.getString(R.string.privacy),
Constants.PRIVACY_URL)

Complicated but universal solution on Kotlin
/*
* Receive Pair of Text and Action and set it clickable and appearing as link
* */
fun TextView.setClickableText(vararg textToSpanAndClickAction: Pair<String, (String) -> Unit>) {
val builder = SpannableStringBuilder(text.toString())
textToSpanAndClickAction.forEach { argPair ->
val clickableSpan = object : ClickableSpan() {
override fun onClick(widget: View) {
argPair.second.invoke(argPair.first)
}
}
this.text.toString().let { fullText ->
val indexOfFirst = fullText.indexOf(argPair.first)
val indexOfLast = indexOfFirst + argPair.first.length
if (indexOfFirst < 0){
//No match found
return
}else{
builder.setSpan(
clickableSpan,
indexOfFirst,
indexOfLast,
Spanned.SPAN_EXCLUSIVE_EXCLUSIVE
)
}
}
}
this.text = builder
this.movementMethod = LinkMovementMethod.getInstance()
}
kotlin spannable

Solution in Java (Updated 2022)
Features:
Allows for multiple clickable when there are repeated words.
Specific commands can be tailored for each repeated words.
I built upon daler445's code to allow for multiple clickable commands for repeated words.
At Java class:
public class MainActivity extends AppCompatActivity {
SharedPreferences sp;
#Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
sp = getSharedPreferences("MyUserPrefs", Context.MODE_PRIVATE);
SharedPreferences.Editor editor = sp.edit();
TextView fulltext = (TextView) findViewById(R.id.fulltext);
//replace setText("") to setText("Android is a Software stack") for his case
fulltext.setText("Search [1] this full [1] text with repeated strings. Search [2] Search [3] full [2] full [3]");
List<Pair<String, View.OnClickListener>> links = new ArrayList<>();
//replace "Search" with "stack" for his case
String stringtohyperlink = "Search";
String stringtohyperlink2 = "full";
links.add(new Pair<>(stringtohyperlink, new View.OnClickListener() {
#Override
public void onClick(View v) {
String position = sp.getString("position","0");
if (position.equals("1")) {
Toast.makeText(MainActivity.this, "Search 1 has been Clicked.", Toast.LENGTH_SHORT).show();
editor.putString("position","0");
editor.apply();
}
if (position.equals("2")) {
Toast.makeText(MainActivity.this, "Search 2 has been Clicked.", Toast.LENGTH_SHORT).show();
editor.putString("position","0");
editor.apply();
}
if (position.equals("3")) {
Toast.makeText(MainActivity.this, "Search 3 has been Clicked.", Toast.LENGTH_SHORT).show();
editor.putString("position","0");
editor.apply();
}
}
}));
links.add(new Pair<>(stringtohyperlink2, new View.OnClickListener() {
#Override
public void onClick(View v) {
String position = sp.getString("position","0");
if (position.equals("1")) {
Toast.makeText(MainActivity.this, "full 1 has been Clicked.", Toast.LENGTH_SHORT).show();
editor.putString("position","0");
editor.apply();
}
if (position.equals("2")) {
Toast.makeText(MainActivity.this, "full 2 has been Clicked.", Toast.LENGTH_SHORT).show();
editor.putString("position","0");
editor.apply();
}
if (position.equals("3")) {
Toast.makeText(MainActivity.this, "full 3 has been Clicked.", Toast.LENGTH_SHORT).show();
editor.putString("position","0");
editor.apply();
}
}
}));
makeLinks(fulltext, links);
}
public void makeLinks(TextView textView, List<Pair<String, View.OnClickListener>> links) {
SpannableString spannableString = new SpannableString(textView.getText().toString());
int startIndexState = -1;
SharedPreferences.Editor editor = sp.edit();
for (Pair<String, View.OnClickListener> link : links) {
ClickableSpan clickableSpan = new ClickableSpan() {
#Override
public void onClick(#NonNull View widget) {
editor.putString("position","1");
editor.apply();
widget.invalidate();
assert link.second != null;
link.second.onClick(widget);
}
};
ClickableSpan clickableSpan2 = new ClickableSpan() {
#Override
public void onClick(#NonNull View widget) {
editor.putString("position","2");
editor.apply();
widget.invalidate();
assert link.second != null;
link.second.onClick(widget);
}
};
ClickableSpan clickableSpan3 = new ClickableSpan() {
#Override
public void onClick(#NonNull View widget) {
editor.putString("position","3");
editor.apply();
widget.invalidate();
assert link.second != null;
link.second.onClick(widget);
}
};
//... This only allows for 3 repeated words
//... Add more of it, if there are more repeated words.
assert link.first != null;
int startIndexOfLink = textView.getText().toString().indexOf(link.first, startIndexState + 1);
spannableString.setSpan(clickableSpan, startIndexOfLink, startIndexOfLink + link.first.length(), Spanned.SPAN_EXCLUSIVE_INCLUSIVE);
int startIndexOfLink2 = textView.getText().toString().indexOf(link.first, startIndexOfLink + 1);
if (startIndexOfLink2 != -1) {
spannableString.setSpan(clickableSpan2, startIndexOfLink2, startIndexOfLink2 + link.first.length(), Spanned.SPAN_EXCLUSIVE_INCLUSIVE);
}
int startIndexOfLink3 = textView.getText().toString().indexOf(link.first, startIndexOfLink2 + 1);
if (startIndexOfLink3 != -1) {
spannableString.setSpan(clickableSpan3, startIndexOfLink3, startIndexOfLink3 + link.first.length(), Spanned.SPAN_EXCLUSIVE_INCLUSIVE);
}
//... This only allows for 3 repeated words
//... Add more of it, if there are more repeated words.
textView.setMovementMethod(LinkMovementMethod.getInstance());
textView.setText(spannableString, TextView.BufferType.SPANNABLE);
}
}
}
At .xml
<TextView
android:id="#+id/fulltext"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="TextView"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent" />
Demo

It really helpful for the clickable part for some portion of the text.
The dot is a special character in the regular expression. If you want to spanable the dot need to escape dot as \\. instead of just passing "." to the spanable text method. Alternatively, you can also use the regular expression [.] to spanable the String by a dot in Java.

Related

Android SpannableString click listener not working [duplicate]

I am creating a ClickableSpan, and it is displaying properly with
the proper text underlined. However, the clicks are not registering.
Do you know what I am doing wrong???
Thanks, Victor
Here is the code snippet:
view.setText("This is a test");
ClickableSpan span = new ClickableSpan() {
#Override
public void onClick(View widget) {
log("Clicked");
}
};
view.getText().setSpan(span, 0, view.getText().length(), Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
Have you tried setting the MovementMethod on the TextView that contains the span? You need to do that to make the clicking work...
tv.setMovementMethod(LinkMovementMethod.getInstance());
Kotlin util function:
fun setClickable(textView: TextView, subString: String, handler: () -> Unit, drawUnderline: Boolean = false) {
val text = textView.text
val start = text.indexOf(subString)
val end = start + subString.length
val span = SpannableString(text)
span.setSpan(ClickHandler(handler, drawUnderline), start, end, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE)
textView.linksClickable = true
textView.isClickable = true
textView.movementMethod = LinkMovementMethod.getInstance()
textView.text = span
}
class ClickHandler(
private val handler: () -> Unit,
private val drawUnderline: Boolean
) : ClickableSpan() {
override fun onClick(widget: View?) {
handler()
}
override fun updateDrawState(ds: TextPaint?) {
if (drawUnderline) {
super.updateDrawState(ds)
} else {
ds?.isUnderlineText = false
}
}
}
Usage:
Utils.setClickable(textView, subString, {handleClick()})
After some trial and error, the sequence of setting the tv.setMovementMethod(LinkMovementMethod.getInstance()); does matter.
Here's my full code
String stringTerms = getString(R.string.sign_up_terms);
Spannable spannable = new SpannableString(stringTerms);
int indexTermsStart = stringTerms.indexOf("Terms");
int indexTermsEnd = indexTermsStart + 18;
spannable.setSpan(new UnderlineSpan(), indexTermsStart, indexTermsEnd, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
spannable.setSpan(new ForegroundColorSpan(getColor(R.color.theme)), indexTermsStart, indexTermsEnd, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
spannable.setSpan(new ClickableSpan() {
#Override
public void onClick(View widget) {
Log.d(TAG, "TODO onClick.. Terms and Condition");
}
}, indexTermsStart, indexTermsEnd, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
int indexPolicyStart = stringTerms.indexOf("Privacy");
int indexPolicyEnd = indexPolicyStart + 14;
spannable.setSpan(new UnderlineSpan(), indexPolicyStart, indexPolicyEnd, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
spannable.setSpan(new ForegroundColorSpan(getColor(R.color.theme)), indexPolicyStart, indexPolicyEnd, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
spannable.setSpan(new ClickableSpan() {
#Override
public void onClick(View widget) {
Log.d(TAG, "TODO onClick.. Privacy Policy");
}
}, indexPolicyStart, indexPolicyEnd, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
TextView textViewTerms = (TextView) findViewById(R.id.sign_up_terms_text);
textViewTerms.setText(spannable);
textViewTerms.setClickable(true);
textViewTerms.setMovementMethod(LinkMovementMethod.getInstance());
Direct Approach in Kotlin
val textHeadingSpannable = SpannableString(resources.getString(R.string.travel_agent))
val clickSpan = object : ClickableSpan(){
override fun onClick(widget: View) {
// Handel your click
}
}
textHeadingSpannable.setSpan(clickSpan,104,136,Spannable.SPAN_INCLUSIVE_EXCLUSIVE)
tv_contact_us_inquire_travel_agent.movementMethod = LinkMovementMethod.getInstance()
tv_contact_us_inquire_travel_agent.text = textHeadingSpannable

How to click a clickablespan using espresso?

I have a textview with multiple clickable spans in it. I want to be able to test clicking these spans.
I tried setting up a custom ViewAction that would find the clickablespans in the TextView and then match their text with the desired text and then click on the xy coordinates of that text. However, it seems the spans added to the TextView aren't of type ClickableSpan and are instead the the fragment that added the span.
Therefore, I am not able to distinguish the link spans. Is there a better way to do this?
Adding the spans:
Util.addClickableSpan(spannableString, string, linkedString, new ClickableSpan() {
#Override
public void onClick(View textView) {}
});
tvAcceptTc.setText(spannableString);
tvAcceptTc.setMovementMethod(LinkMovementMethod.getInstance());
Utility method:
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);
}
Defining the 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
This is my solution. It's simpler because we don't need to find the coordinates. Once we have found the ClickableSpan, we just click on it:
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();
}
};
}
Now you can use our custom ViewAction just like that:
onView(withId(R.id.myTextView)).perform(clickClickableSpan("myLink"));
Here is the Kotlin version of accepted answer
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()
}
}
}
The best option would be to subclass a ViewAction. Here is the way of doing it in 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()
}
}
and use it as:
onView(withId(<view_id>)).perform(scrollTo(), SpannableTextClickAction(text))
It worked with a minor change.
just recheck the "textToClick" and the variable "sequence" in:
CharSequence sequence = spannableString.subSequence(start, end);
are exactly same.
I have to use trim() like this:
textToClick.toString() == sequence.trim().toString()
because my textToClick value is "click here" and sequence value that I got " click here"
Note: The space before the "click".
I hope this is useful for someone.
This works for me:
/**
* 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);
}
you may use Spannable instead of SpannableString compatible with SpannableStringBuilder.
sorry, i am a new man , have only 1 Reputation , can not add a comment.Even my English is very poor.....
i suggest to use:
Spannable spannableString = (Spannable) textView.getText();
instead of :
SpannableString spannableString = (SpannableString) textView.getText();
post all the code below:
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();
}
};
}
}
Espresso has a one-liner for this:
onView(withId(R.id.textView)).perform(openLinkWithText("..."))

How to set the part of the text view is clickable

I have the text "Android is a Software stack". In this text i want to set the "stack" text as clickable. So, if you click on that it will redirected to a new activity(not in the browser).
I tried but i am not getting a solution.
android.text.style.ClickableSpan can solve your problem.
SpannableString ss = new SpannableString("Android is a Software stack");
ClickableSpan clickableSpan = new ClickableSpan() {
#Override
public void onClick(View textView) {
startActivity(new Intent(MyActivity.this, NextActivity.class));
}
#Override
public void updateDrawState(TextPaint ds) {
super.updateDrawState(ds);
ds.setUnderlineText(false);
}
};
ss.setSpan(clickableSpan, 22, 27, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
TextView textView = (TextView) findViewById(R.id.hello);
textView.setText(ss);
textView.setMovementMethod(LinkMovementMethod.getInstance());
textView.setHighlightColor(Color.TRANSPARENT);
In XML:
<TextView
...
android:textColorLink="#drawable/your_selector"
/>
My function for make multiple links inside TextView
Update 2020: Now this function able to support multiple same texts link inside 1 TextView, but remember to put the link in the correct order
fun TextView.makeLinks(vararg links: Pair<String, View.OnClickListener>) {
val spannableString = SpannableString(this.text)
var startIndexOfLink = -1
for (link in links) {
val clickableSpan = object : ClickableSpan() {
override fun updateDrawState(textPaint: TextPaint) {
// use this to change the link color
textPaint.color = textPaint.linkColor
// toggle below value to enable/disable
// the underline shown below the clickable text
textPaint.isUnderlineText = true
}
override fun onClick(view: View) {
Selection.setSelection((view as TextView).text as Spannable, 0)
view.invalidate()
link.second.onClick(view)
}
}
startIndexOfLink = this.text.toString().indexOf(link.first, startIndexOfLink + 1)
// if(startIndexOfLink == -1) continue // todo if you want to verify your texts contains links text
spannableString.setSpan(
clickableSpan, startIndexOfLink, startIndexOfLink + link.first.length,
Spanned.SPAN_EXCLUSIVE_EXCLUSIVE
)
}
this.movementMethod =
LinkMovementMethod.getInstance() // without LinkMovementMethod, link can not click
this.setText(spannableString, TextView.BufferType.SPANNABLE)
}
USING
my_text_view.makeLinks(
Pair("Terms of Service", View.OnClickListener {
Toast.makeText(applicationContext, "Terms of Service Clicked", Toast.LENGTH_SHORT).show()
}),
Pair("Privacy Policy", View.OnClickListener {
Toast.makeText(applicationContext, "Privacy Policy Clicked", Toast.LENGTH_SHORT).show()
}))
XML
<TextView
android:id="#+id/my_text_view"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="Please accept Terms of Service and Privacy Policy"
android:textColorHighlight="#f00" // background color when pressed
android:textColorLink="#0f0"
android:textSize="20sp" />
DEMO
Reference
Solution for clear the link highlight selection follow https://stackoverflow.com/a/19445108/5381331
You can use ClickableSpan as described in this post
Sample code:
TextView myTextView = new TextView(this);
String myString = "Some text [clickable]";
int i1 = myString.indexOf("[");
int i2 = myString.indexOf("]");
myTextView.setMovementMethod(LinkMovementMethod.getInstance());
myTextView.setText(myString, BufferType.SPANNABLE);
Spannable mySpannable = (Spannable)myTextView.getText();
ClickableSpan myClickableSpan = new ClickableSpan() {
#Override
public void onClick(View widget) { /* do something */ }
};
mySpannable.setSpan(myClickableSpan, i1, i2 + 1, Spannable.SPAN_EXCLUSIVE_EXCLUSIVE);
Reference
You can use sample code. You want to learn detail about ClickableSpan. Please check this documentaion
SpannableString myString = new SpannableString("This is example");
ClickableSpan clickableSpan = new ClickableSpan() {
#Override
public void onClick(View textView) {
ToastUtil.show(getContext(),"Clicked Smile ");
}
};
//For Click
myString.setSpan(clickableSpan,startIndex,lastIndex,Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
//For UnderLine
myString.setSpan(new UnderlineSpan(),startIndex,lastIndex,0);
//For Bold
myString.setSpan(new StyleSpan(Typeface.BOLD),startIndex,lastIndex,0);
//Finally you can set to textView.
TextView textView = (TextView) findViewById(R.id.txtSpan);
textView.setText(myString);
textView.setMovementMethod(LinkMovementMethod.getInstance());
I made this helper method in case someone need start and end position from a String.
public static TextView createLink(TextView targetTextView, String completeString,
String partToClick, ClickableSpan clickableAction) {
SpannableString spannableString = new SpannableString(completeString);
// make sure the String is exist, if it doesn't exist
// it will throw IndexOutOfBoundException
int startPosition = completeString.indexOf(partToClick);
int endPosition = completeString.lastIndexOf(partToClick) + partToClick.length();
spannableString.setSpan(clickableAction, startPosition, endPosition,
Spanned.SPAN_INCLUSIVE_EXCLUSIVE);
targetTextView.setText(spannableString);
targetTextView.setMovementMethod(LinkMovementMethod.getInstance());
return targetTextView;
}
And here is how you use it
private void initSignUp() {
String completeString = "New to Reddit? Sign up here.";
String partToClick = "Sign up";
ClickableTextUtil
.createLink(signUpEditText, completeString, partToClick,
new ClickableSpan() {
#Override
public void onClick(View widget) {
// your action
Toast.makeText(activity, "Start Sign up activity",
Toast.LENGTH_SHORT).show();
}
#Override
public void updateDrawState(TextPaint ds) {
super.updateDrawState(ds);
// this is where you set link color, underline, typeface etc.
int linkColor = ContextCompat.getColor(activity, R.color.blumine);
ds.setColor(linkColor);
ds.setUnderlineText(false);
}
});
}
Here is a Kotlin method to make parts of a TextView clickable:
private fun makeTextLink(textView: TextView, str: String, underlined: Boolean, color: Int?, action: (() -> Unit)? = null) {
val spannableString = SpannableString(textView.text)
val textColor = color ?: textView.currentTextColor
val clickableSpan = object : ClickableSpan() {
override fun onClick(textView: View) {
action?.invoke()
}
override fun updateDrawState(drawState: TextPaint) {
super.updateDrawState(drawState)
drawState.isUnderlineText = underlined
drawState.color = textColor
}
}
val index = spannableString.indexOf(str)
spannableString.setSpan(clickableSpan, index, index + str.length, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE)
textView.text = spannableString
textView.movementMethod = LinkMovementMethod.getInstance()
textView.highlightColor = Color.TRANSPARENT
}
It can be called multiple times to create several links within a TextView:
makeTextLink(myTextView, str, false, Color.RED, action = { Log.d("onClick", "link") })
makeTextLink(myTextView, str1, true, null, action = { Log.d("onClick", "link1") })
t= (TextView) findViewById(R.id.PP1);
t.setText(Html.fromHtml("<bThis is normal text </b>" +
"This is cliclable text "));
t.setMovementMethod(LinkMovementMethod.getInstance());
I would suggest a different approach that I think requires less code and is more "localization-friendly".
Supposing that your destination activity is called "ActivityStack", define in the manifest an intent filter for it with a custom scheme (e.g. "myappscheme") in AndroidManifest.xml:
<activity
android:name=".ActivityStack">
<intent-filter>
<action android:name="android.intent.action.VIEW" />
<category android:name="android.intent.category.DEFAULT" />
<data android:host="stack"/>
<data android:scheme="myappscheme" />
</intent-filter>
</activity>
Define the TextView without any special tag (it is important to NOT use the "android:autoLink" tag, see: https://stackoverflow.com/a/20647011/1699702):
<TextView
android:id="#+id/stackView"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="#string/stack_string" />
then use a link with custom scheme and host in the text of the TextView as (in String.xml):
<string name="stack_string">Android is a Software stack</string>
and "activate" the link with setMovementMethod() (in onCreate() for activities or onCreateView() for fragments):
TextView stack = findViewById(R.id.stackView);
stack.setMovementMethod(LinkMovementMethod.getInstance());
This will open the stack activity with a tap on the "stack" word.
Kotlin Version of Phan Van Linh's answer.
Please note it has some minor modifications.
fun makeLinks(textView: TextView, links: Array<String>, clickableSpans: Array<ClickableSpan>) {
val spannableString = SpannableString(textView.text)
for (i in links.indices) {
val clickableSpan = clickableSpans[i]
val link = links[i]
val startIndexOfLink = textView.text.indexOf(link)
spannableString.setSpan(clickableSpan, startIndexOfLink, startIndexOfLink + link.length,
Spanned.SPAN_EXCLUSIVE_EXCLUSIVE)
}
textView.movementMethod = LinkMovementMethod.getInstance()
textView.setText(spannableString, TextView.BufferType.SPANNABLE)
}
fun setupClickableTextView() {
val termsOfServicesClick = object : ClickableSpan() {
override fun onClick(p0: View?) {
Toast.makeText(applicationContext, "ToS clicked", Toast.LENGTH_SHORT).show()
}
}
val privacyPolicyClick = object : ClickableSpan() {
override fun onClick(p0: View?) {
Toast.makeText(applicationContext, "PP clicked", Toast.LENGTH_SHORT).show()
}
}
makeLinks(termsTextView, arrayOf("terms", "privacy policy"), arrayOf(termsOfServicesClick, privacyPolicyClick))
}
You can you this method to set the clickable value
public void setClickableString(String clickableValue, String wholeValue, TextView yourTextView){
String value = wholeValue;
SpannableString spannableString = new SpannableString(value);
int startIndex = value.indexOf(clickableValue);
int endIndex = startIndex + clickableValue.length();
spannableString.setSpan(new ClickableSpan() {
#Override
public void updateDrawState(TextPaint ds) {
super.updateDrawState(ds);
ds.setUnderlineText(false); // <-- this will remove automatic underline in set span
}
#Override
public void onClick(View widget) {
// do what you want with clickable value
}
}, startIndex, endIndex, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
yourTextView.setText(spannableString);
yourTextView.setMovementMethod(LinkMovementMethod.getInstance()); // <-- important, onClick in ClickableSpan won't work without this
}
This is how to use it:
TextView myTextView = findViewById(R.id.myTextView);
setClickableString("stack", "Android is a Software stack", myTextView);
For those that are looking for a solution in Kotlin here is what worked for me:
private fun setupTermsAndConditions() {
val termsAndConditions = resources.getString(R.string.terms_and_conditions)
val spannableString = SpannableString(termsAndConditions)
val clickableSpan = object : ClickableSpan() {
override fun onClick(widget: View) {
if (checkForWifiAndMobileInternet()) {
// binding.viewModel!!.openTermsAndConditions()
showToast("Good, open the link!!!")
} else {
showToast("Cannot open this file because of internet connection!")
}
}
override fun updateDrawState(textPaint : TextPaint) {
super.updateDrawState(textPaint)
textPaint.color = resources.getColor(R.color.colorGrey)
textPaint.isFakeBoldText = true
}
}
spannableString.setSpan(clickableSpan, 34, 86, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE)
binding.tvTermsAndConditions.text = spannableString
binding.tvTermsAndConditions.movementMethod = LinkMovementMethod.getInstance()
binding.tvTermsAndConditions.setHighlightColor(Color.TRANSPARENT);
}
Created elegant Kotlin way with extension:
fun TextView.setClickableText(text: Spanned,
clickableText: String,
#ColorInt clickableColor: Int,
clickListener: () -> Unit) {
val spannableString = SpannableString(text)
val startingPosition: Int = text.indexOf(clickableText)
if (startingPosition > -1) {
val clickableSpan: ClickableSpan = object : ClickableSpan() {
override fun onClick(textView: View) {
clickListener()
}
override fun updateDrawState(textPaint: TextPaint) {
super.updateDrawState(textPaint)
textPaint.isUnderlineText = false
}
}
val endingPosition: Int = startingPosition + clickableText.length
spannableString.setSpan(clickableSpan, startingPosition,
endingPosition, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE)
spannableString.setSpan(ForegroundColorSpan(clickableColor), startingPosition,
endingPosition, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE)
movementMethod = LinkMovementMethod.getInstance()
highlightColor = Color.TRANSPARENT
}
setText(spannableString)
}
Boom Check this for java Lovers :D
We can modify it according to our need:
List<Pair<String, View.OnClickListener>> pairsList = new ArrayList<>();
pairsList.add(new Pair<>("38,50", v -> {
Intent intent = new Intent(SignUpActivity.this, WebActivity.class);
intent.putExtra("which", "tos");
startActivity(intent);
}));
pairsList.add(new Pair<>("81,95", v -> {
Intent intent = new Intent(SignUpActivity.this, WebActivity.class);
intent.putExtra("which", "policy");
startActivity(intent);
}));
makeLinks(pairsList); // Method calling
private void makeLinks(List<Pair<String, View.OnClickListener>> pairsList) {
SpannableString ss = new SpannableString(By signing up, I’m agree to PAKRISM’s Terms of Use and confirms that I have read Privacy Policy);
for (Pair<String, View.OnClickListener> pair : pairsList) {
ClickableSpan clickableSpan = new ClickableSpan() {
#Override
public void onClick(View textView) {
//Toast.makeText(MyApplication.getAppContext(), "Clicked!", Toast.LENGTH_SHORT).show();
pair.second.onClick(textView);
}
#Override
public void updateDrawState(TextPaint ds) {
ds.linkColor = ContextCompat.getColor(SignUpActivity.this, R.color.primary_main);
ds.setUnderlineText(true);
super.updateDrawState(ds);
}
};
String[] indexes = pair.first.split(",");
ss.setSpan(clickableSpan, Integer.parseInt(indexes[0]), Integer.parseInt(indexes[1]), Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
}
TextView tv = findViewById(R.id.txtView);
tv.setText(ss);
tv.setMovementMethod(LinkMovementMethod.getInstance());
}
For kotlin use this extension
fun TextView.makeLinks(vararg links: Pair<String, View.OnClickListener>) {
val spannableString = SpannableString(this.text)
for (link in links) {
val clickableSpan = object : ClickableSpan() {
override fun onClick(view: View) {
Selection.setSelection((view as TextView).text as Spannable, 0)
view.invalidate()
link.second.onClick(view)
}
}
val startIndexOfLink = this.text.toString().indexOf(link.first)
spannableString.setSpan(
clickableSpan, startIndexOfLink, startIndexOfLink + link.first.length,
Spanned.SPAN_EXCLUSIVE_EXCLUSIVE
)
spannableString.setSpan(
ForegroundColorSpan(Color.parseColor("#46C2CC")),
startIndexOfLink,
startIndexOfLink + link.first.length,
Spanned.SPAN_EXCLUSIVE_EXCLUSIVE
)
}
this.movementMethod =
LinkMovementMethod.getInstance() // without LinkMovementMethod, link can not click
this.setText(spannableString, TextView.BufferType.SPANNABLE)
}
Call this like this
binding.agreeText.makeLinks(Pair(getString(R.string.terms_conditionsClick),View.OnClickListener {
startActivity(TermsAndConditionActivity.getIntent(this))
}))
For bold,
mySpannable.setSpan(new StyleSpan(Typeface.BOLD),termStart,termStop,Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
i coded an example to solve your question in Kotlin.
This is the Code:
val completeText = getString(R.string.terms_description)
val textToFind = getString(R.string.show_terms)
val spannableString: Spannable = SpannableString(completeText)
val startFocus = completeText.indexOf(textToFind)
val endFocus = startFocus + textToFind.length
spannableString.setSpan(object: ClickableSpan() {
override fun onClick(p0: View) {
showMessage()
}
}, startFocus, endFocus, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE)
show_terms.text = spannableString
show_terms.movementMethod = LinkMovementMethod.getInstance();
show_terms.highlightColor = Color.TRANSPARENT;
This is the XML
<CheckBox
android:id="#+id/check_agree_terms"
android:layout_width="wrap_content"
android:layout_height="wrap_content"/>
<TextView
android:id="#+id/show_terms"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:textColorLink="#color/colorPrimary"
android:layout_toEndOf="#id/check_agree_terms"/>
This is how it looks
enter image description here
Using URLSpan class to get urls
val spans: Array<URLSpan> = result.getSpans(0, result.length, URLSpan::class.java)
Method
fun TextView.createClickable(string: String, action:(String)->Unit ) {
text = HtmlCompat.fromHtml(string, HtmlCompat.FROM_HTML_MODE_LEGACY)
val result = SpannableString(text)
val spans = result.getSpans(0, result.length, URLSpan::class.java)
for (span in spans) {
val link:Pair<String, View.OnClickListener> = Pair(span.url, View.OnClickListener {
action(span.url)
})
val start = result.getSpanStart(span)
val end = result.getSpanEnd(span)
val flags = result.getSpanFlags(span)
result.removeSpan(span)
val clickableSpan: ClickableSpan = object : ClickableSpan() {
override fun onClick(textView: View) {
textView.invalidate()
link.second.onClick(textView)
}
override fun updateDrawState(textPaint: TextPaint) {
super.updateDrawState(textPaint)
textPaint.isUnderlineText = false
}
}
result.setSpan(clickableSpan, start, end, flags)
this.movementMethod = LinkMovementMethod.getInstance()
this.setText(result, TextView.BufferType.SPANNABLE)
}
}
Use
Example Text : Android is a Software Stack and it' Awesome
Wrapper your clickable text inside anchor tag
Like: Android is a Software <a href='https://example.com/stack'> Stack </a> and it' <a href='https://example.com/awesome'> Awesome </a>.
val str = "Android is a Software <a href='https://example.com/stack'> Stack </a> and it' <a href='https://example.com/awesome'> Awesome </a>."
textView.createClickable(str) {
when(it) {
"https://example.com/stack"->{
startActivity(Intent(this,StackActivity::class.java))
}
"https://example.com/awesom"->{
startActivity(Intent(this,AwesomeActivity::class.java))
}
}
}
Here a Kotlin solution that work better with localization:
data class LinkedText(#StringRes val textRes: Int, val clickListener: View.OnClickListener? = null)
fun TextView.setPartiallyLinkedText(vararg texts: LinkedText) {
this.text = texts.joinToString(" ") { context.getString(it.textRes) }
val spannableString = SpannableString(this.text)
var startIndexOfLink = -1
texts.forEach { text ->
val string = context.getString(text.textRes)
if (text.clickListener != null) {
val clickableSpan = object : ClickableSpan() {
override fun updateDrawState(textPaint: TextPaint) {
textPaint.color = textPaint.linkColor
textPaint.isUnderlineText = true
}
override fun onClick(view: View) {
Selection.setSelection((view as TextView).text as Spannable, 0)
view.invalidate()
text.clickListener.onClick(view)
}
}
startIndexOfLink = this.text.toString().indexOf(string, startIndexOfLink + 1)
spannableString.setSpan(
clickableSpan, startIndexOfLink, startIndexOfLink + string.length,
Spanned.SPAN_EXCLUSIVE_EXCLUSIVE
)
}
}
this.movementMethod = LinkMovementMethod.getInstance()
this.setText(spannableString, TextView.BufferType.SPANNABLE)
}
And use it like this:
textView.setPartiallyLinkedText(
LinkedText(R.string.not_linked_text),
LinkedText(R.string.linked_text) {
Toast.makeText(context, "You clicked", Toast.LENGTH_LONG).show()
},
)
Here is a kotlin extension (not works if text repeats)
textView.makeTextClickable(
contentText = "Hello world",
clickableText = "world",
isBoldText = true,
onClick = {
openPageInBrowser(BuildConfig.PRIVACY_POLICY_URL)
}
)
internal fun TextView.makeTextClickable(
contentText: String,
clickableText: String,
onClick: () -> Unit,
isUnderlineText: Boolean = false,
isBoldText: Boolean = false
) {
val spannableString = SpannableString(contentText)
val clickableSpan = object : ClickableSpan() {
override fun onClick(widget: View) {
onClick()
}
override fun updateDrawState(ds: TextPaint) {
super.updateDrawState(ds)
ds.isUnderlineText = isUnderlineText
ds.isFakeBoldText = isBoldText
}
}
val clickableTextStartIndex = text.indexOf(clickableText)
val clickableTextEndIndex = clickableTextStartIndex + clickableText.length
spannableString.setSpan(
clickableSpan,
clickableTextStartIndex,
clickableTextEndIndex,
Spanned.SPAN_EXCLUSIVE_EXCLUSIVE
)
this.text = spannableString
this.movementMethod = LinkMovementMethod.getInstance()
}
This is my MovementMethod for detecting link/text/image clicks. It is modified LinkMovementMethod.
import android.text.Layout;
import android.text.NoCopySpan;
import android.text.Selection;
import android.text.Spannable;
import android.text.method.ScrollingMovementMethod;
import android.text.style.ClickableSpan;
import android.text.style.ImageSpan;
import android.text.style.URLSpan;
import android.view.KeyEvent;
import android.view.MotionEvent;
import android.view.View;
import android.widget.TextView;
public class ClickMovementMethod extends ScrollingMovementMethod {
private Object FROM_BELOW = new NoCopySpan.Concrete();
private static final int CLICK = 1;
private static final int UP = 2;
private static final int DOWN = 3;
private Listener listener;
public void setListener(Listener listener) {
this.listener = listener;
}
#Override
public boolean canSelectArbitrarily() {
return true;
}
#Override
protected boolean handleMovementKey(TextView widget, Spannable buffer, int keyCode,
int movementMetaState, KeyEvent event) {
switch (keyCode) {
case KeyEvent.KEYCODE_DPAD_CENTER:
case KeyEvent.KEYCODE_ENTER:
if (KeyEvent.metaStateHasNoModifiers(movementMetaState)) {
if (event.getAction() == KeyEvent.ACTION_DOWN &&
event.getRepeatCount() == 0 && action(CLICK, widget, buffer)) {
return true;
}
}
break;
}
return super.handleMovementKey(widget, buffer, keyCode, movementMetaState, event);
}
#Override
protected boolean up(TextView widget, Spannable buffer) {
if (action(UP, widget, buffer)) {
return true;
}
return super.up(widget, buffer);
}
#Override
protected boolean down(TextView widget, Spannable buffer) {
if (action(DOWN, widget, buffer)) {
return true;
}
return super.down(widget, buffer);
}
#Override
protected boolean left(TextView widget, Spannable buffer) {
if (action(UP, widget, buffer)) {
return true;
}
return super.left(widget, buffer);
}
#Override
protected boolean right(TextView widget, Spannable buffer) {
if (action(DOWN, widget, buffer)) {
return true;
}
return super.right(widget, buffer);
}
private boolean action(int what, TextView widget, Spannable buffer) {
Layout layout = widget.getLayout();
int padding = widget.getTotalPaddingTop() +
widget.getTotalPaddingBottom();
int areatop = widget.getScrollY();
int areabot = areatop + widget.getHeight() - padding;
int linetop = layout.getLineForVertical(areatop);
int linebot = layout.getLineForVertical(areabot);
int first = layout.getLineStart(linetop);
int last = layout.getLineEnd(linebot);
ClickableSpan[] candidates = buffer.getSpans(first, last, URLSpan.class);
int a = Selection.getSelectionStart(buffer);
int b = Selection.getSelectionEnd(buffer);
int selStart = Math.min(a, b);
int selEnd = Math.max(a, b);
if (selStart < 0) {
if (buffer.getSpanStart(FROM_BELOW) >= 0) {
selStart = selEnd = buffer.length();
}
}
if (selStart > last)
selStart = selEnd = Integer.MAX_VALUE;
if (selEnd < first)
selStart = selEnd = -1;
switch (what) {
case CLICK:
if (selStart == selEnd) {
return false;
}
if (listener != null) {
URLSpan[] link = buffer.getSpans(selStart, selEnd, URLSpan.class);
if (link.length >= 1) {
listener.onClick(link[0].getURL());
} else {
ImageSpan[] image = buffer.getSpans(selStart, selEnd, ImageSpan.class);
if (image.length >= 1) {
listener.onImageClicked(image[0].getSource());
} else {
listener.onTextClicked();
}
}
}
break;
case UP:
int beststart, bestend;
beststart = -1;
bestend = -1;
for (int i = 0; i < candidates.length; i++) {
int end = buffer.getSpanEnd(candidates[i]);
if (end < selEnd || selStart == selEnd) {
if (end > bestend) {
beststart = buffer.getSpanStart(candidates[i]);
bestend = end;
}
}
}
if (beststart >= 0) {
Selection.setSelection(buffer, bestend, beststart);
return true;
}
break;
case DOWN:
beststart = Integer.MAX_VALUE;
bestend = Integer.MAX_VALUE;
for (int i = 0; i < candidates.length; i++) {
int start = buffer.getSpanStart(candidates[i]);
if (start > selStart || selStart == selEnd) {
if (start < beststart) {
beststart = start;
bestend = buffer.getSpanEnd(candidates[i]);
}
}
}
if (bestend < Integer.MAX_VALUE) {
Selection.setSelection(buffer, beststart, bestend);
return true;
}
break;
}
return false;
}
#Override
public boolean onTouchEvent(TextView widget, Spannable buffer,
MotionEvent event) {
int action = event.getAction();
if (action == MotionEvent.ACTION_UP ||
action == MotionEvent.ACTION_DOWN) {
int x = (int) event.getX();
int y = (int) event.getY();
x -= widget.getTotalPaddingLeft();
y -= widget.getTotalPaddingTop();
x += widget.getScrollX();
y += widget.getScrollY();
Layout layout = widget.getLayout();
int line = layout.getLineForVertical(y);
int off = layout.getOffsetForHorizontal(line, x);
URLSpan[] link = buffer.getSpans(off, off, URLSpan.class);
if (action == MotionEvent.ACTION_UP) {
if (listener != null) {
if (link.length >= 1) {
listener.onClick(link[0].getURL());
} else {
ImageSpan[] image = buffer.getSpans(off, off, ImageSpan.class);
if (image.length >= 1) {
listener.onImageClicked(image[0].getSource());
} else if (Selection.getSelectionStart(buffer) == Selection.getSelectionEnd(buffer)) {
listener.onTextClicked();
}
}
}
}
if (action == MotionEvent.ACTION_DOWN && link.length != 0) {
Selection.setSelection(buffer,
buffer.getSpanStart(link[0]),
buffer.getSpanEnd(link[0]));
return true;
}
if (link.length == 0) {
Selection.removeSelection(buffer);
}
}
return super.onTouchEvent(widget, buffer, event);
}
#Override
public void initialize(TextView widget, Spannable text) {
Selection.removeSelection(text);
text.removeSpan(FROM_BELOW);
}
#Override
public void onTakeFocus(TextView view, Spannable text, int dir) {
Selection.removeSelection(text);
if ((dir & View.FOCUS_BACKWARD) != 0) {
text.setSpan(FROM_BELOW, 0, 0, Spannable.SPAN_POINT_POINT);
} else {
text.removeSpan(FROM_BELOW);
}
}
public interface Listener {
void onClick(String clicked);
void onTextClicked();
void onImageClicked(String source);
}
}
more generic answer in kotlin
fun setClickableText(view: TextView, firstSpan: String, secondSpan: String) {
val context = view.context
val builder = SpannableStringBuilder()
val unClickableSpan = SpannableString(firstSpan)
val span = SpannableString(" "+secondSpan)
builder.append(unClickableSpan);
val clickableSpan: ClickableSpan = object : ClickableSpan() {
override fun onClick(textView: View) {
val intent = Intent(context, HomeActivity::class.java)
context.startActivity(intent)
}
override fun updateDrawState(ds: TextPaint) {
super.updateDrawState(ds)
ds.isUnderlineText = true
ds.setTypeface(Typeface.create(Typeface.DEFAULT, Typeface.ITALIC));
}
}
builder.append(span);
builder.setSpan(clickableSpan, firstSpan.length, firstSpan.length+secondSpan.length+1, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE)
view.setText(builder,TextView.BufferType.SPANNABLE)
view.setMovementMethod(LinkMovementMethod.getInstance());
}
The solutions provided are pretty decent. However, I generally use a more simple solution.
Here is a linkify utility function
/**
* Method is used to Linkify words in a TextView
*
* #param textView TextView who's text you want to change
* #param textToLink The text to turn into a link
* #param url The url you want to send the user to
*/
fun linkify(textView: TextView, textToLink: String, url: String) {
val pattern = Pattern.compile(textToLink)
Linkify.addLinks(textView, pattern, url, { _, _, _ -> true })
{ _, _ -> "" }
}
Using this function is pretty simple. Here is an example
// terms and privacy
val tvTerms = findViewById<TextView>(R.id.tv_terms)
val tvPrivacy = findViewById<TextView>(R.id.tv_privacy)
Utils.linkify(tvTerms, resources.getString(R.string.terms),
Constants.TERMS_URL)
Utils.linkify(tvPrivacy, resources.getString(R.string.privacy),
Constants.PRIVACY_URL)
Complicated but universal solution on Kotlin
/*
* Receive Pair of Text and Action and set it clickable and appearing as link
* */
fun TextView.setClickableText(vararg textToSpanAndClickAction: Pair<String, (String) -> Unit>) {
val builder = SpannableStringBuilder(text.toString())
textToSpanAndClickAction.forEach { argPair ->
val clickableSpan = object : ClickableSpan() {
override fun onClick(widget: View) {
argPair.second.invoke(argPair.first)
}
}
this.text.toString().let { fullText ->
val indexOfFirst = fullText.indexOf(argPair.first)
val indexOfLast = indexOfFirst + argPair.first.length
if (indexOfFirst < 0){
//No match found
return
}else{
builder.setSpan(
clickableSpan,
indexOfFirst,
indexOfLast,
Spanned.SPAN_EXCLUSIVE_EXCLUSIVE
)
}
}
}
this.text = builder
this.movementMethod = LinkMovementMethod.getInstance()
}
kotlin spannable
Solution in Java (Updated 2022)
Features:
Allows for multiple clickable when there are repeated words.
Specific commands can be tailored for each repeated words.
I built upon daler445's code to allow for multiple clickable commands for repeated words.
At Java class:
public class MainActivity extends AppCompatActivity {
SharedPreferences sp;
#Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
sp = getSharedPreferences("MyUserPrefs", Context.MODE_PRIVATE);
SharedPreferences.Editor editor = sp.edit();
TextView fulltext = (TextView) findViewById(R.id.fulltext);
//replace setText("") to setText("Android is a Software stack") for his case
fulltext.setText("Search [1] this full [1] text with repeated strings. Search [2] Search [3] full [2] full [3]");
List<Pair<String, View.OnClickListener>> links = new ArrayList<>();
//replace "Search" with "stack" for his case
String stringtohyperlink = "Search";
String stringtohyperlink2 = "full";
links.add(new Pair<>(stringtohyperlink, new View.OnClickListener() {
#Override
public void onClick(View v) {
String position = sp.getString("position","0");
if (position.equals("1")) {
Toast.makeText(MainActivity.this, "Search 1 has been Clicked.", Toast.LENGTH_SHORT).show();
editor.putString("position","0");
editor.apply();
}
if (position.equals("2")) {
Toast.makeText(MainActivity.this, "Search 2 has been Clicked.", Toast.LENGTH_SHORT).show();
editor.putString("position","0");
editor.apply();
}
if (position.equals("3")) {
Toast.makeText(MainActivity.this, "Search 3 has been Clicked.", Toast.LENGTH_SHORT).show();
editor.putString("position","0");
editor.apply();
}
}
}));
links.add(new Pair<>(stringtohyperlink2, new View.OnClickListener() {
#Override
public void onClick(View v) {
String position = sp.getString("position","0");
if (position.equals("1")) {
Toast.makeText(MainActivity.this, "full 1 has been Clicked.", Toast.LENGTH_SHORT).show();
editor.putString("position","0");
editor.apply();
}
if (position.equals("2")) {
Toast.makeText(MainActivity.this, "full 2 has been Clicked.", Toast.LENGTH_SHORT).show();
editor.putString("position","0");
editor.apply();
}
if (position.equals("3")) {
Toast.makeText(MainActivity.this, "full 3 has been Clicked.", Toast.LENGTH_SHORT).show();
editor.putString("position","0");
editor.apply();
}
}
}));
makeLinks(fulltext, links);
}
public void makeLinks(TextView textView, List<Pair<String, View.OnClickListener>> links) {
SpannableString spannableString = new SpannableString(textView.getText().toString());
int startIndexState = -1;
SharedPreferences.Editor editor = sp.edit();
for (Pair<String, View.OnClickListener> link : links) {
ClickableSpan clickableSpan = new ClickableSpan() {
#Override
public void onClick(#NonNull View widget) {
editor.putString("position","1");
editor.apply();
widget.invalidate();
assert link.second != null;
link.second.onClick(widget);
}
};
ClickableSpan clickableSpan2 = new ClickableSpan() {
#Override
public void onClick(#NonNull View widget) {
editor.putString("position","2");
editor.apply();
widget.invalidate();
assert link.second != null;
link.second.onClick(widget);
}
};
ClickableSpan clickableSpan3 = new ClickableSpan() {
#Override
public void onClick(#NonNull View widget) {
editor.putString("position","3");
editor.apply();
widget.invalidate();
assert link.second != null;
link.second.onClick(widget);
}
};
//... This only allows for 3 repeated words
//... Add more of it, if there are more repeated words.
assert link.first != null;
int startIndexOfLink = textView.getText().toString().indexOf(link.first, startIndexState + 1);
spannableString.setSpan(clickableSpan, startIndexOfLink, startIndexOfLink + link.first.length(), Spanned.SPAN_EXCLUSIVE_INCLUSIVE);
int startIndexOfLink2 = textView.getText().toString().indexOf(link.first, startIndexOfLink + 1);
if (startIndexOfLink2 != -1) {
spannableString.setSpan(clickableSpan2, startIndexOfLink2, startIndexOfLink2 + link.first.length(), Spanned.SPAN_EXCLUSIVE_INCLUSIVE);
}
int startIndexOfLink3 = textView.getText().toString().indexOf(link.first, startIndexOfLink2 + 1);
if (startIndexOfLink3 != -1) {
spannableString.setSpan(clickableSpan3, startIndexOfLink3, startIndexOfLink3 + link.first.length(), Spanned.SPAN_EXCLUSIVE_INCLUSIVE);
}
//... This only allows for 3 repeated words
//... Add more of it, if there are more repeated words.
textView.setMovementMethod(LinkMovementMethod.getInstance());
textView.setText(spannableString, TextView.BufferType.SPANNABLE);
}
}
}
At .xml
<TextView
android:id="#+id/fulltext"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="TextView"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent" />
Demo
It really helpful for the clickable part for some portion of the text.
The dot is a special character in the regular expression. If you want to spanable the dot need to escape dot as \\. instead of just passing "." to the spanable text method. Alternatively, you can also use the regular expression [.] to spanable the String by a dot in Java.

ClickableSpan not... Clicking [duplicate]

I am creating a ClickableSpan, and it is displaying properly with
the proper text underlined. However, the clicks are not registering.
Do you know what I am doing wrong???
Thanks, Victor
Here is the code snippet:
view.setText("This is a test");
ClickableSpan span = new ClickableSpan() {
#Override
public void onClick(View widget) {
log("Clicked");
}
};
view.getText().setSpan(span, 0, view.getText().length(), Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
Have you tried setting the MovementMethod on the TextView that contains the span? You need to do that to make the clicking work...
tv.setMovementMethod(LinkMovementMethod.getInstance());
Kotlin util function:
fun setClickable(textView: TextView, subString: String, handler: () -> Unit, drawUnderline: Boolean = false) {
val text = textView.text
val start = text.indexOf(subString)
val end = start + subString.length
val span = SpannableString(text)
span.setSpan(ClickHandler(handler, drawUnderline), start, end, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE)
textView.linksClickable = true
textView.isClickable = true
textView.movementMethod = LinkMovementMethod.getInstance()
textView.text = span
}
class ClickHandler(
private val handler: () -> Unit,
private val drawUnderline: Boolean
) : ClickableSpan() {
override fun onClick(widget: View?) {
handler()
}
override fun updateDrawState(ds: TextPaint?) {
if (drawUnderline) {
super.updateDrawState(ds)
} else {
ds?.isUnderlineText = false
}
}
}
Usage:
Utils.setClickable(textView, subString, {handleClick()})
After some trial and error, the sequence of setting the tv.setMovementMethod(LinkMovementMethod.getInstance()); does matter.
Here's my full code
String stringTerms = getString(R.string.sign_up_terms);
Spannable spannable = new SpannableString(stringTerms);
int indexTermsStart = stringTerms.indexOf("Terms");
int indexTermsEnd = indexTermsStart + 18;
spannable.setSpan(new UnderlineSpan(), indexTermsStart, indexTermsEnd, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
spannable.setSpan(new ForegroundColorSpan(getColor(R.color.theme)), indexTermsStart, indexTermsEnd, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
spannable.setSpan(new ClickableSpan() {
#Override
public void onClick(View widget) {
Log.d(TAG, "TODO onClick.. Terms and Condition");
}
}, indexTermsStart, indexTermsEnd, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
int indexPolicyStart = stringTerms.indexOf("Privacy");
int indexPolicyEnd = indexPolicyStart + 14;
spannable.setSpan(new UnderlineSpan(), indexPolicyStart, indexPolicyEnd, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
spannable.setSpan(new ForegroundColorSpan(getColor(R.color.theme)), indexPolicyStart, indexPolicyEnd, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
spannable.setSpan(new ClickableSpan() {
#Override
public void onClick(View widget) {
Log.d(TAG, "TODO onClick.. Privacy Policy");
}
}, indexPolicyStart, indexPolicyEnd, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
TextView textViewTerms = (TextView) findViewById(R.id.sign_up_terms_text);
textViewTerms.setText(spannable);
textViewTerms.setClickable(true);
textViewTerms.setMovementMethod(LinkMovementMethod.getInstance());
Direct Approach in Kotlin
val textHeadingSpannable = SpannableString(resources.getString(R.string.travel_agent))
val clickSpan = object : ClickableSpan(){
override fun onClick(widget: View) {
// Handel your click
}
}
textHeadingSpannable.setSpan(clickSpan,104,136,Spannable.SPAN_INCLUSIVE_EXCLUSIVE)
tv_contact_us_inquire_travel_agent.movementMethod = LinkMovementMethod.getInstance()
tv_contact_us_inquire_travel_agent.text = textHeadingSpannable

Android ClickableSpan not calling onClick

I am creating a ClickableSpan, and it is displaying properly with
the proper text underlined. However, the clicks are not registering.
Do you know what I am doing wrong???
Thanks, Victor
Here is the code snippet:
view.setText("This is a test");
ClickableSpan span = new ClickableSpan() {
#Override
public void onClick(View widget) {
log("Clicked");
}
};
view.getText().setSpan(span, 0, view.getText().length(), Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
Have you tried setting the MovementMethod on the TextView that contains the span? You need to do that to make the clicking work...
tv.setMovementMethod(LinkMovementMethod.getInstance());
Kotlin util function:
fun setClickable(textView: TextView, subString: String, handler: () -> Unit, drawUnderline: Boolean = false) {
val text = textView.text
val start = text.indexOf(subString)
val end = start + subString.length
val span = SpannableString(text)
span.setSpan(ClickHandler(handler, drawUnderline), start, end, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE)
textView.linksClickable = true
textView.isClickable = true
textView.movementMethod = LinkMovementMethod.getInstance()
textView.text = span
}
class ClickHandler(
private val handler: () -> Unit,
private val drawUnderline: Boolean
) : ClickableSpan() {
override fun onClick(widget: View?) {
handler()
}
override fun updateDrawState(ds: TextPaint?) {
if (drawUnderline) {
super.updateDrawState(ds)
} else {
ds?.isUnderlineText = false
}
}
}
Usage:
Utils.setClickable(textView, subString, {handleClick()})
After some trial and error, the sequence of setting the tv.setMovementMethod(LinkMovementMethod.getInstance()); does matter.
Here's my full code
String stringTerms = getString(R.string.sign_up_terms);
Spannable spannable = new SpannableString(stringTerms);
int indexTermsStart = stringTerms.indexOf("Terms");
int indexTermsEnd = indexTermsStart + 18;
spannable.setSpan(new UnderlineSpan(), indexTermsStart, indexTermsEnd, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
spannable.setSpan(new ForegroundColorSpan(getColor(R.color.theme)), indexTermsStart, indexTermsEnd, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
spannable.setSpan(new ClickableSpan() {
#Override
public void onClick(View widget) {
Log.d(TAG, "TODO onClick.. Terms and Condition");
}
}, indexTermsStart, indexTermsEnd, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
int indexPolicyStart = stringTerms.indexOf("Privacy");
int indexPolicyEnd = indexPolicyStart + 14;
spannable.setSpan(new UnderlineSpan(), indexPolicyStart, indexPolicyEnd, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
spannable.setSpan(new ForegroundColorSpan(getColor(R.color.theme)), indexPolicyStart, indexPolicyEnd, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
spannable.setSpan(new ClickableSpan() {
#Override
public void onClick(View widget) {
Log.d(TAG, "TODO onClick.. Privacy Policy");
}
}, indexPolicyStart, indexPolicyEnd, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
TextView textViewTerms = (TextView) findViewById(R.id.sign_up_terms_text);
textViewTerms.setText(spannable);
textViewTerms.setClickable(true);
textViewTerms.setMovementMethod(LinkMovementMethod.getInstance());
Direct Approach in Kotlin
val textHeadingSpannable = SpannableString(resources.getString(R.string.travel_agent))
val clickSpan = object : ClickableSpan(){
override fun onClick(widget: View) {
// Handel your click
}
}
textHeadingSpannable.setSpan(clickSpan,104,136,Spannable.SPAN_INCLUSIVE_EXCLUSIVE)
tv_contact_us_inquire_travel_agent.movementMethod = LinkMovementMethod.getInstance()
tv_contact_us_inquire_travel_agent.text = textHeadingSpannable

Categories

Resources