152

I'm currently rendering HTML input in a TextView like so:

tv.setText(Html.fromHtml("<a href='test'>test</a>"));

The HTML being displayed is provided to me via an external resource, so I cannot change things around as I will, but I can, of course, do some regex tampering with the HTML, to change the href value, say, to something else.

What I want is to be able to handle a link click directly from within the app, rather than having the link open a browser window. Is this achievable at all? I'm guessing it would be possible to set the protocol of the href-value to something like "myApp://", and then register something that would let my app handle that protocol. If this is indeed the best way, I'd like to know how that is done, but I'm hoping there's an easier way to just say, "when a link is clicked in this textview, I want to raise an event that receives the href value of the link as an input parameter"

Swati Garg
  • 995
  • 1
  • 10
  • 21
David Hedlund
  • 128,221
  • 31
  • 203
  • 222
  • I found something else at [Here][1] [1]: http://stackoverflow.com/questions/7255249/how-can-we-open-textviews-links-into-webview Hope that can help you ^^ – Mr_DK Oct 11 '12 at 17:07
  • 1
    David, I am having a case similar to yours, I get the html too from an external source (web), but how do I regex tamper the href value so I can apply this solution. – X09 May 14 '16 at 11:03

13 Answers13

181

Coming at this almost a year later, there's a different manner in which I solved my particular problem. Since I wanted the link to be handled by my own app, there is a solution that is a bit simpler.

Besides the default intent filter, I simply let my target activity listen to ACTION_VIEW intents, and specifically, those with the scheme com.package.name

<intent-filter>
    <category android:name="android.intent.category.DEFAULT" />
    <action android:name="android.intent.action.VIEW" />
    <data android:scheme="com.package.name" />  
</intent-filter>

This means that links starting with com.package.name:// will be handled by my activity.

So all I have to do is construct a URL that contains the information I want to convey:

com.package.name://action-to-perform/id-that-might-be-needed/

In my target activity, I can retrieve this address:

Uri data = getIntent().getData();

In my example, I could simply check data for null values, because when ever it isn't null, I'll know it was invoked by means of such a link. From there, I extract the instructions I need from the url to be able to display the appropriate data.

David Hedlund
  • 128,221
  • 31
  • 203
  • 222
  • 1
    Hey your answer is perfect. Working fine.But further can we send data of passed data with this? – user861973 Aug 07 '12 at 11:44
  • 4
    @user861973: Yes, `getData` gives you the full URI, you could also use `getDataString` that yields a text representation. Either way, you could construct the URL so as to contain all the data you need `com.package.name://my-action/1/2/3/4`, and you could extract the information from that string. – David Hedlund Aug 07 '12 at 12:03
  • Thanks David,I got what you are saying. It's working. My problem Solved. – user861973 Aug 07 '12 at 12:08
  • 3
    It took me a day to understand this idea, but I tell you what - that was well worth it. Well-designed solution – Denys Sep 30 '12 at 22:54
  • 7
    great solution. Dont forget to add this to the Textview so it enables the links. tv.setMovementMethod(LinkMovementMethod.getInstance()); – Daniel Benedykt Oct 18 '13 at 14:33
  • Gee, thanks I was really thinking something way more difficult. – Mathijs Segers Jan 14 '14 at 15:02
  • This is great, I could use Linkify together with it and adding the intent filter on multiple activities lets the user pick the action he wants to perform. In my case clicking on a phone number user can pick the dialer (to call it) or the address book (to add the contact). Thanks – Carlo Moretti Feb 23 '15 at 12:18
  • 3
    Sorry, but in which method in the activity should I say `Uri data = getIntent().getData();` ? I keep receiving `Activity not found to handle intent` error . - Thanks – rgv Jul 28 '15 at 16:05
  • Recommended to call it onCreate(). I actually encountered the same error - please double and triple check your URL. I missed out one colon! com.package.name://action. – Kevin Lee Dec 23 '15 at 20:30
  • Thanks for the great solution! Also if you have multiple activities which you open like this, you may want to specify actions for every activity like: ``. And different activities will be opened for different actions. – Yamashiro Rion Nov 02 '19 at 21:37
  • Magnificent!! Exactly what I was looking for. Much thanks my brother. – Makari Kevin Apr 14 '20 at 17:10
64

Another way, borrows a bit from Linkify but allows you to customize your handling.

Custom Span Class:

public class ClickSpan extends ClickableSpan {

    private OnClickListener mListener;

    public ClickSpan(OnClickListener listener) {
        mListener = listener;
    }

    @Override
    public void onClick(View widget) {
       if (mListener != null) mListener.onClick();
    }

    public interface OnClickListener {
        void onClick();
    }
}

Helper function:

public static void clickify(TextView view, final String clickableText, 
    final ClickSpan.OnClickListener listener) {

    CharSequence text = view.getText();
    String string = text.toString();
    ClickSpan span = new ClickSpan(listener);

    int start = string.indexOf(clickableText);
    int end = start + clickableText.length();
    if (start == -1) return;

    if (text instanceof Spannable) {
        ((Spannable)text).setSpan(span, start, end, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
    } else {
        SpannableString s = SpannableString.valueOf(text);
        s.setSpan(span, start, end, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
        view.setText(s);
    }

    MovementMethod m = view.getMovementMethod();
    if ((m == null) || !(m instanceof LinkMovementMethod)) {
        view.setMovementMethod(LinkMovementMethod.getInstance());
    }
}

Usage:

 clickify(textView, clickText,new ClickSpan.OnClickListener()
     {
        @Override
        public void onClick() {
            // do something
        }
    });
Jonathan S.
  • 1,540
  • 1
  • 15
  • 26
  • 1
    Another solution is to replace the Spans with your custom Spans, view http://stackoverflow.com/a/11417498/792677 – A-Live Jul 10 '12 at 16:38
  • I found this to be the simplest solution to implement. Google has never cleaned up this mess to have a way to consistently make links in textviews clickable. It's a brutal approach to force a span into it, but it works well on different OS versions.. +1 – angryITguy May 29 '18 at 00:33
56

if there are multiple links in the text view . For example textview has "https://" and "tel no" we can customise the LinkMovement method and handle clicks for words based on a pattern. Attached is the customised Link Movement Method.

public class CustomLinkMovementMethod extends LinkMovementMethod
{

private static Context movementContext;

private static CustomLinkMovementMethod linkMovementMethod = new CustomLinkMovementMethod();

public boolean onTouchEvent(android.widget.TextView widget, android.text.Spannable buffer, android.view.MotionEvent event)
{
    int action = event.getAction();

    if (action == MotionEvent.ACTION_UP)
    {
        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 (link.length != 0)
        {
            String url = link[0].getURL();
            if (url.startsWith("https"))
            {
                Log.d("Link", url);
                Toast.makeText(movementContext, "Link was clicked", Toast.LENGTH_LONG).show();
            } else if (url.startsWith("tel"))
            {
                Log.d("Link", url);
                Toast.makeText(movementContext, "Tel was clicked", Toast.LENGTH_LONG).show();
            } else if (url.startsWith("mailto"))
            {
                Log.d("Link", url);
                Toast.makeText(movementContext, "Mail link was clicked", Toast.LENGTH_LONG).show();
            }
            return true;
        }
    }

    return super.onTouchEvent(widget, buffer, event);
}

public static android.text.method.MovementMethod getInstance(Context c)
{
    movementContext = c;
    return linkMovementMethod;
}

This should be called from the textview in the following manner:

textViewObject.setMovementMethod(CustomLinkMovementMethod.getInstance(context));
Matthew Cordaro
  • 677
  • 1
  • 7
  • 26
Arun
  • 2,800
  • 23
  • 13
  • 8
    You don't really need to pass the context so "behind the back" in a separate static variable, that's kind of smelly. Just use `widget.getContext()` instead. – Sergej Koščejev Jul 23 '13 at 09:06
  • Yes the context can be removed. Thanks for pointing it out Sergej – Arun Jul 29 '13 at 09:00
  • 6
    This works brilliantly but you must call setMovementMethod after setText, otherwise it will overwrite with the default LinkMovementMethod – Ray Britton Oct 03 '13 at 14:08
  • Works, but there's no need to iterate over spans in each click. Also the filter by position looks prone to errors. A similar one-time initialization approach is shown in [this answer](http://stackoverflow.com/a/19903674/813951). – Mister Smith Jan 28 '14 at 12:10
  • @Arun Please update your answer according to the comments. – Behrouz.M Jul 05 '15 at 13:59
  • It's not a good idea to keep a static ref to context. This will cause memory leaks. – алекс кей Jul 25 '16 at 15:44
51

Here is a more generic solution based on @Arun answer

public abstract class TextViewLinkHandler extends LinkMovementMethod {

    public boolean onTouchEvent(TextView widget, Spannable buffer, MotionEvent event) {
        if (event.getAction() != MotionEvent.ACTION_UP)
            return super.onTouchEvent(widget, buffer, event);

        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 (link.length != 0) {
            onLinkClick(link[0].getURL());
        }
        return true;
    }

    abstract public void onLinkClick(String url);
}

To use it just implement onLinkClick of TextViewLinkHandler class. For instance:

    textView.setMovementMethod(new TextViewLinkHandler() {
        @Override
        public void onLinkClick(String url) {
            Toast.makeText(textView.getContext(), url, Toast.LENGTH_SHORT).show();
        }
    });
Dariusz Bacinski
  • 8,324
  • 9
  • 38
  • 47
ruX
  • 7,224
  • 3
  • 39
  • 33
  • 2
    works great. note : do not forget to add android:autoLink="web" to your textview. without autolink attribute, this does not work. – okarakose May 13 '16 at 12:07
  • I have tried all the solutions listed here. This one is the best for me. It is clear, simple to use, and powerful. Hint: you need to work with Html.fromHtml to get the most of it. – Kai Wang Jul 26 '16 at 16:16
  • 1
    Best answer! Thanks! Don't forget to add android:autoLink="web" in xml or in code LinkifyCompat.addLinks(textView, Linkify.WEB_URLS); – Nikita Axyonov Jan 24 '17 at 09:53
  • 1
    What would be a seemingly simple requirement is quite a complex affair in Android. This solution takes alot of that pain away. Have found that autoLink="web" is not required for this solution to work on Android N.. +1 – angryITguy Feb 16 '17 at 22:18
  • I am on API 19 (KitKat). Does anyone know why putting `android:autoLink="web"` does not work at all? I was tearing my hair out and decided to remove `android:autoLink="web"` and the link start to show and it worked! Any idea why? – Sam Dec 30 '17 at 05:56
  • 1
    Why in the world a class like this doesn't exist natively - after all these years - is beyond me. Thanks Android for LinkMovementMethod, but I'd usually like control of my own UI. You can report UI events to me and so MY controller can handle them. – methodsignature Oct 04 '19 at 15:18
9

its very simple add this line to your code:

tv.setMovementMethod(LinkMovementMethod.getInstance());
Lucifer
  • 29,392
  • 25
  • 90
  • 143
jonathan
  • 123
  • 1
  • 2
  • 2
    Thanks for your reply, jonathan. Yes, I knew about MovementMethod; what I wasn't sure about was how to specify that my own app should handle the link click, rather than just opening a browser, as the default movement method would (see the accepted answer). Thanks anyway. – David Hedlund Aug 15 '10 at 20:49
6

Solution

I have implemented a small class with the help of which you can handle long clicks on TextView itself and Taps on the links in the TextView.

Layout

TextView android:id="@+id/text"
                android:layout_width="wrap_content"
                android:layout_height="wrap_content"
                android:autoLink="all"/>

TextViewClickMovement.java

import android.content.Context;
import android.text.Layout;
import android.text.Spannable;
import android.text.method.LinkMovementMethod;
import android.text.style.ClickableSpan;
import android.util.Patterns;
import android.view.GestureDetector;
import android.view.MotionEvent;
import android.widget.TextView;

public class TextViewClickMovement extends LinkMovementMethod {

    private final String TAG = TextViewClickMovement.class.getSimpleName();

    private final OnTextViewClickMovementListener mListener;
    private final GestureDetector                 mGestureDetector;
    private TextView                              mWidget;
    private Spannable                             mBuffer;

    public enum LinkType {

        /** Indicates that phone link was clicked */
        PHONE,

        /** Identifies that URL was clicked */
        WEB_URL,

        /** Identifies that Email Address was clicked */
        EMAIL_ADDRESS,

        /** Indicates that none of above mentioned were clicked */
        NONE
    }

    /**
     * Interface used to handle Long clicks on the {@link TextView} and taps
     * on the phone, web, mail links inside of {@link TextView}.
     */
    public interface OnTextViewClickMovementListener {

        /**
         * This method will be invoked when user press and hold
         * finger on the {@link TextView}
         *
         * @param linkText Text which contains link on which user presses.
         * @param linkType Type of the link can be one of {@link LinkType} enumeration
         */
        void onLinkClicked(final String linkText, final LinkType linkType);

        /**
         *
         * @param text Whole text of {@link TextView}
         */
        void onLongClick(final String text);
    }


    public TextViewClickMovement(final OnTextViewClickMovementListener listener, final Context context) {
        mListener        = listener;
        mGestureDetector = new GestureDetector(context, new SimpleOnGestureListener());
    }

    @Override
    public boolean onTouchEvent(final TextView widget, final Spannable buffer, final MotionEvent event) {

        mWidget = widget;
        mBuffer = buffer;
        mGestureDetector.onTouchEvent(event);

        return false;
    }

    /**
     * Detects various gestures and events.
     * Notify users when a particular motion event has occurred.
     */
    class SimpleOnGestureListener extends GestureDetector.SimpleOnGestureListener {
        @Override
        public boolean onDown(MotionEvent event) {
            // Notified when a tap occurs.
            return true;
        }

        @Override
        public void onLongPress(MotionEvent e) {
            // Notified when a long press occurs.
            final String text = mBuffer.toString();

            if (mListener != null) {
                Log.d(TAG, "----> Long Click Occurs on TextView with ID: " + mWidget.getId() + "\n" +
                                  "Text: " + text + "\n<----");

                mListener.onLongClick(text);
            }
        }

        @Override
        public boolean onSingleTapConfirmed(MotionEvent event) {
            // Notified when tap occurs.
            final String linkText = getLinkText(mWidget, mBuffer, event);

            LinkType linkType = LinkType.NONE;

            if (Patterns.PHONE.matcher(linkText).matches()) {
                linkType = LinkType.PHONE;
            }
            else if (Patterns.WEB_URL.matcher(linkText).matches()) {
                linkType = LinkType.WEB_URL;
            }
            else if (Patterns.EMAIL_ADDRESS.matcher(linkText).matches()) {
                linkType = LinkType.EMAIL_ADDRESS;
            }

            if (mListener != null) {
                Log.d(TAG, "----> Tap Occurs on TextView with ID: " + mWidget.getId() + "\n" +
                                  "Link Text: " + linkText + "\n" +
                                  "Link Type: " + linkType + "\n<----");

                mListener.onLinkClicked(linkText, linkType);
            }

            return false;
        }

        private String getLinkText(final TextView widget, final Spannable buffer, final MotionEvent event) {

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

            ClickableSpan[] link = buffer.getSpans(off, off, ClickableSpan.class);

            if (link.length != 0) {
                return buffer.subSequence(buffer.getSpanStart(link[0]),
                        buffer.getSpanEnd(link[0])).toString();
            }

            return "";
        }
    }
}

Usage

TextView tv = (TextView) v.findViewById(R.id.textview);
tv.setText(Html.fromHtml("<a href='test'>test</a>"));
textView.setMovementMethod(new TextViewClickMovement(this, context));

Links

Hope this helps! You can find code here.

angryITguy
  • 9,332
  • 8
  • 54
  • 82
4

for who looks for more options here is a one

// Set text within a `TextView`
TextView textView = (TextView) findViewById(R.id.textView);
textView.setText("Hey @sarah, where did @jim go? #lost");
// Style clickable spans based on pattern
new PatternEditableBuilder().
    addPattern(Pattern.compile("\\@(\\w+)"), Color.BLUE,
       new PatternEditableBuilder.SpannableClickedListener() {
        @Override
        public void onSpanClicked(String text) {
            Toast.makeText(MainActivity.this, "Clicked username: " + text,
                Toast.LENGTH_SHORT).show();
        }
}).into(textView);

RESOURCE : CodePath

Dasser Basyouni
  • 3,142
  • 5
  • 26
  • 50
3

Just to share an alternative solution using a library I created. With Textoo, this can be achieved like:

TextView locNotFound = Textoo
    .config((TextView) findViewById(R.id.view_location_disabled))
    .addLinksHandler(new LinksHandler() {
        @Override
        public boolean onClick(View view, String url) {
            if ("internal://settings/location".equals(url)) {
                Intent locSettings = new Intent(android.provider.Settings.ACTION_LOCATION_SOURCE_SETTINGS);
                startActivity(locSettings);
                return true;
            } else {
                return false;
            }
        }
    })
    .apply();

Or with dynamic HTML source:

String htmlSource = "Links: <a href='http://www.google.com'>Google</a>";
Spanned linksLoggingText = Textoo
    .config(htmlSource)
    .parseHtml()
    .addLinksHandler(new LinksHandler() {
        @Override
        public boolean onClick(View view, String url) {
            Log.i("MyActivity", "Linking to google...");
            return false; // event not handled.  Continue default processing i.e. link to google
        }
    })
    .apply();
textView.setText(linksLoggingText);
PH88
  • 1,796
  • 12
  • 12
2
public static void setTextViewFromHtmlWithLinkClickable(TextView textView, String text) {
    Spanned result;
    if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.N) {
        result = Html.fromHtml(text, Html.FROM_HTML_MODE_LEGACY);
    } else {
        result = Html.fromHtml(text);
    }
    textView.setText(result);
    textView.setMovementMethod(LinkMovementMethod.getInstance());
}
Kai Wang
  • 3,303
  • 1
  • 31
  • 27
1

This answer extends Jonathan S's excellent solution:

You can use the following method to extract links from the text:

private static ArrayList<String> getLinksFromText(String text) {
        ArrayList links = new ArrayList();

        String regex = "\(?\b((http|https)://www[.])[-A-Za-z0-9+&@#/%?=~_()|!:,.;]*[-A-Za-z0-9+&@#/%=~_()|]";
        Pattern p = Pattern.compile(regex);
        Matcher m = p.matcher(text);
        while (m.find()) {
            String urlStr = m.group();
            if (urlStr.startsWith("(") && urlStr.endsWith(")")) {
                urlStr = urlStr.substring(1, urlStr.length() - 1);
            }
            links.add(urlStr);
        }
        return links;
    }

This can be used to remove one of the parameters in the clickify() method:

public static void clickify(TextView view,
                                final ClickSpan.OnClickListener listener) {

        CharSequence text = view.getText();
        String string = text.toString();


        ArrayList<String> linksInText = getLinksFromText(string);
        if (linksInText.isEmpty()){
            return;
        }


        String clickableText = linksInText.get(0);
        ClickSpan span = new ClickSpan(listener,clickableText);

        int start = string.indexOf(clickableText);
        int end = start + clickableText.length();
        if (start == -1) return;

        if (text instanceof Spannable) {
            ((Spannable) text).setSpan(span, start, end, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
        } else {
            SpannableString s = SpannableString.valueOf(text);
            s.setSpan(span, start, end, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
            view.setText(s);
        }

        MovementMethod m = view.getMovementMethod();
        if ((m == null) || !(m instanceof LinkMovementMethod)) {
            view.setMovementMethod(LinkMovementMethod.getInstance());
        }
    }

A few changes to the ClickSpan:

public static class ClickSpan extends ClickableSpan {

        private String mClickableText;
        private OnClickListener mListener;

        public ClickSpan(OnClickListener listener, String clickableText) {
            mListener = listener;
            mClickableText = clickableText;
        }

        @Override
        public void onClick(View widget) {
            if (mListener != null) mListener.onClick(mClickableText);
        }

        public interface OnClickListener {
            void onClick(String clickableText);
        }
    }

Now you can simply set the text on the TextView and then add a listener to it:

TextViewUtils.clickify(textWithLink,new TextUtils.ClickSpan.OnClickListener(){

@Override
public void onClick(String clickableText){
  //action...
}

});
W.K.S
  • 9,787
  • 15
  • 75
  • 122
1

I changed the TextView's color to blue by using for example:

android:textColor="#3399FF"

in the xml file. How to make it underlined is explained here.

Then use its onClick property to specify a method (I'm guessing you could call setOnClickListener(this) as another way), e.g.:

myTextView.setOnClickListener(new OnClickListener() {
public void onClick(View v) {
    doSomething();
}
});

In that method, I can do whatever I want as normal, such as launch an intent. Note that you still have to do the normal myTextView.setMovementMethod(LinkMovementMethod.getInstance()); thing, like in your acitivity's onCreate() method.

Community
  • 1
  • 1
Tyler Collier
  • 11,489
  • 9
  • 73
  • 80
0

Example: Suppose you have set some text in textview and you want to provide a link on a particular text expression: "Click on #facebook will take you to facebook.com"

In layout xml:

<TextView
            android:id="@+id/testtext"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content" />

In Activity:

String text  =  "Click on #facebook will take you to facebook.com";
tv.setText(text);
Pattern tagMatcher = Pattern.compile("[#]+[A-Za-z0-9-_]+\\b");
String newActivityURL = "content://ankit.testactivity/";
Linkify.addLinks(tv, tagMatcher, newActivityURL);

Also create one tag provider as:

public class TagProvider extends ContentProvider {

    @Override
    public int delete(Uri arg0, String arg1, String[] arg2) {
        // TODO Auto-generated method stub
        return 0;
    }

    @Override
    public String getType(Uri arg0) {
        return "vnd.android.cursor.item/vnd.cc.tag";
    }

    @Override
    public Uri insert(Uri arg0, ContentValues arg1) {
        // TODO Auto-generated method stub
        return null;
    }

    @Override
    public boolean onCreate() {
        // TODO Auto-generated method stub
        return false;
    }

    @Override
    public Cursor query(Uri arg0, String[] arg1, String arg2, String[] arg3,
                        String arg4) {
        // TODO Auto-generated method stub
        return null;
    }

    @Override
    public int update(Uri arg0, ContentValues arg1, String arg2, String[] arg3) {
        // TODO Auto-generated method stub
        return 0;
    }

}

In manifest file make as entry for provider and test activity as:

<provider
    android:name="ankit.TagProvider"
    android:authorities="ankit.testactivity" />

<activity android:name=".TestActivity"
    android:label = "@string/app_name">
    <intent-filter >
        <action android:name="android.intent.action.VIEW" />
        <category android:name="android.intent.category.DEFAULT" />
        <data android:mimeType="vnd.android.cursor.item/vnd.cc.tag" />
    </intent-filter>
</activity>

Now when you click on #facebook, it will invoke testactivtiy. And in test activity you can get the data as:

Uri uri = getIntent().getData();
Ankit Adlakha
  • 1,436
  • 12
  • 15
0

Kotlin version to @user5699130's answer:

Layout

<TextView
    android:layout_width="wrap_content"
    android:layout_height="wrap_content"
    android:autoLink="all"/>

InterceptedLinkMovementMethod

import android.text.Spannable
import android.text.method.LinkMovementMethod
import android.text.style.ClickableSpan
import android.view.GestureDetector
import android.view.MotionEvent
import android.widget.TextView

/**
 * Usage:
 * fooTextView.movementMethod = InterceptedLinkMovementMethod(this)
 * Where 'this' implements [TextViewLinkClickListener]
 */
class InterceptedLinkMovementMethod(
    private val listener: TextViewLinkClickListener,
) : LinkMovementMethod() {

    private lateinit var textView: TextView
    private lateinit var spannable: Spannable
    private val gestureDetector: GestureDetector by lazy {
        GestureDetector(textView.context, SimpleTapListener())
    }

    override fun onTouchEvent(widget: TextView, buffer: Spannable, event: MotionEvent): Boolean {
        textView = widget
        spannable = buffer
        gestureDetector.onTouchEvent(event)
        return false
    }

    inner class SimpleTapListener : GestureDetector.SimpleOnGestureListener() {

        override fun onDown(event: MotionEvent): Boolean = true

        override fun onSingleTapConfirmed(event: MotionEvent): Boolean {
            val linkText = getLinkText(textView, spannable, event)
            val linkType = LinkTypes.getLinkTypeFromText(linkText)
            if (linkType != LinkTypes.NONE) {
                listener.onLinkClicked(linkText, linkType)
            }
            return false
        }

        override fun onLongPress(e: MotionEvent) {
            val linkText = getLinkText(textView, spannable, e)
            val linkType = LinkTypes.getLinkTypeFromText(linkText)
            if (linkType != LinkTypes.NONE) {
                listener.onLinkLongClicked(linkText, linkType)
            }
        }

        private fun getLinkText(widget: TextView, buffer: Spannable, event: MotionEvent): String {
            var x = event.x.toInt()
            var y = event.y.toInt()
            x -= widget.totalPaddingLeft
            y -= widget.totalPaddingTop
            x += widget.scrollX
            y += widget.scrollY
            val layout = widget.layout
            val line = layout.getLineForVertical(y)
            val off = layout.getOffsetForHorizontal(line, x.toFloat())
            val link = buffer.getSpans(off, off, ClickableSpan::class.java)
            if (link.isEmpty()) return ""
            return buffer.subSequence(buffer.getSpanStart(link[0]), buffer.getSpanEnd(link[0]))
                .toString()
        }
    }
}

LinkTypes

import android.util.Patterns

enum class LinkTypes {
    PHONE,
    WEB_URL,
    EMAIL_ADDRESS,
    NONE;

    companion object {
        fun getLinkTypeFromText(text: String): LinkTypes =
            when {
                Patterns.PHONE.matcher(text).matches() -> PHONE
                Patterns.WEB_URL.matcher(text).matches() -> WEB_URL
                Patterns.EMAIL_ADDRESS.matcher(text).matches() -> EMAIL_ADDRESS
                else -> NONE
            }
    }
}

TextViewLinkClickListener

interface TextViewLinkClickListener {
    fun onLinkClicked(linkText: String, linkTypes: LinkTypes)

    fun onLinkLongClicked(linkText: String, linkTypes: LinkTypes)
}
Sami
  • 669
  • 5
  • 20