string

android

textview

clickablespan

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.

Solution 1

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"
/>

Solution 2

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

Solution 3

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

Solution 4

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

Solution 5

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

Solution 6

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

Solution 7

 t= (TextView) findViewById(R.id.PP1);

 t.setText(Html.fromHtml("<bThis is normal text </b>" +
                "<a href=\"http://www.xyz-zyyx.com\">This is cliclable text</a> "));
 t.setMovementMethod(LinkMovementMethod.getInstance());

Solution 8

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 <a href="myappscheme://stack">stack</a></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.

Solution 9

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

Solution 10

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

Solution 11

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

Solution 12

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

}

Solution 13

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, Im agree to PAKRISMs 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());
    }

Solution 14

For bold,

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

Solution 15

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

Solution 16

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()
    },
)

Solution 17

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

Solution 18

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

}

}

Solution 19

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


}

Solution 20

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)

Solution 21

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

Solution 22

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

Solution 23

Solution in Java (Updated: 2022)

Feature:

Allows for multiple clickable when there are repeated words.

Specific commands can be tailored for each repeated words.

Special thanks to God for inspiring me to build upon daler445's code to allow for multiple clickable commands for repeated words.

Special thanks to daler445 for the basic code.

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

Solution 24

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.