A better way to handle links in TextView

There are two ways of “linkifying” URLs in a TextView. First, as an XML attribute:

<TextView    
    ...
    android:autoLink="phone|web" />

and second, programmatically:

TextView textView = (TextView) findViewById(R.id.text1);
Linkify.addLinks(textView, Linkify.PHONE_NUMBERS | LINKIFY.WEB_URLS);

In both the cases, the framework internally registers a LinkMovementMethod on the TextView that handles dispatching a ACTION_VIEW Intent when any link is clicked. This is why phone-numbers open in a dialer when clicked, web URLs open in a browser, map URLs open in Google Maps and so on.

The source can be seen in URLSpan.class (line #63):

@Override
public void onClick(View widget) {
    Uri uri = Uri.parse(getURL());
    Context context = widget.getContext();
    Intent intent = new Intent(Intent.ACTION_VIEW, uri);
    intent.putExtra(Browser.EXTRA_APPLICATION_ID, context.getPackageName());
    try {
        context.startActivity(intent);
    }
    ...
}

The problem with the default LinkMovementMethod is that it’s buggy and non-customizable:

1. Incorrect touch areas

It incorrectly calculates a URL’s bounds when the URL is present at any horizontal/vertical end and there’s space available in that direction inside the TextView (including its padding). This is like having ghost links in our app and that’s not good.

2. Unreliable highlighting

LinkMovementMethod highlights a URL only the first time it’s clicked and it stops working randomly after that. It also does not correctly track the touch pointer to unhighlight the URL once it’s no longer being touched.

3. No support for custom URL handling

We’re also out of luck if we want to show contextual options when a phone-number is clicked instead of simply redirecting to the dialer.

BetterLinkMovementMethod

Introducing BetterLinkMovementMethod, a.. uh.. better a version of LinkMovementMethod that solves all our problems. It’s designed to be a drop-in replacement for LinkMovementMethod:

TextView textView = (TextView) findViewById(R.id.text1);
textView.setMovementMethod(BetterLinkMovementMethod.newInstance());
Linkify.addLinks(textView, Linkify.PHONE_NUMBERS);

However, the easiest way to get started is by using one of its linkify() methods:

BetterLinkMovementMethod.linkify(int linkifyMask, Activity);
BetterLinkMovementMethod.linkify(int linkifyMask, ViewGroup);
BetterLinkMovementMethod.linkify(int linkifyMask, TextView...);

// Where linkifyMask can be one of Linkify.ALL, Linkify.PHONE_NUMBERS, 
// Linkify.MAP_ADDRESSES, Linkify.WEB_URLS and Linkify.EMAIL_ADDRESSES.

Update: v1.1 introduces a new method for fixing its compatibility with links inserted using Html.fromHtml():

BetterLinkMovementMethod.linkifyHtml();

Download

Add this to your module’s build.gradle:

repositories {
    jcenter()
}

dependencies {
    compile 'me.saket:better-link-movement-method:1.0'
}

Examples

Registering a BetterLinkMovementMethod on a TextView:

BetterLinkMovementMethod.linkify(Linkify.ALL, textView);

or on infinite TextViews:

BetterLinkMovementMethod.linkify(Linkify.ALL, textView1, textView2, textView3, ...);

Adding a click listener:

BetterLinkMovementMethod method = BetterLinkMovementMethod.linkify(Linkify.ALL, this);
method.setOnLinkClickListener((textView, url) -> {
    // Do something with the URL and return true to indicate that this URL was handled.
    // Otherwise, return false to let the framework handle the URL.
    return true;
});

// Or the less verbose way
BetterLinkMovementMethod
        .linkify(Linkify.ALL, this)
        .setOnLinkClickListener((textView, url) -> {
            // Do something.
            return true;
        });

You can also choose to go the shorter route of registering BetterLinkMovementMethod on all TextViews in your Activity’s layout in one go:

@Override
protected void onCreate(Bundle savedInstanceState) {
    super.onCreate(savedInstanceState);
    setContentView(R.layout.activity_main);

    BetterLinkMovementMethod.linkify(Linkify.ALL, this);
}

When using in a non-Activity context (e.g., Fragments), you can also pass a `ViewGroup` as the 2nd param:

@Nullable
@Override
public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {
    View view = inflater.inflate(R.layout.your_fragment, container, false);

    BetterLinkMovementMethod.linkify(Linkify.ALL, ((ViewGroup) view));

    return view;
}

Source

BetterLinkMovementMethod is available on Github. Feel free to raise issues, send contributions or fork it for your own usage.

https://github.com/Saketme/Better-Link-Movement-Method

View All