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"
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.
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
}
});
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));
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();
}
});
its very simple add this line to your code:
tv.setMovementMethod(LinkMovementMethod.getInstance());
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.
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
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);
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());
}
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.
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...
}
});
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();
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)
}
Related
Android Studio 2.3.1
I am trying to create some text that is not web or html but just some normal text that I want to look like a web link that will be clickable when clicked.
The text is this: Contains 3 reviews
And I want to make it look like a clickable web link.
private void setupTextViewAsLinkClickable() {
if(Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {
mTvReviews.setMovementMethod(LinkMovementMethod.getInstance());
mTvReviews.setText(Html.fromHtml("Contains 3 reviews", Html.FROM_HTML_MODE_LEGACY));
}
else {
mTvReviews.setMovementMethod(LinkMovementMethod.getInstance());
mTvReviews.setText(Html.fromHtml("Contains 3 reviews"));
}
}
I have also tried this as well for my xml:
<TextView
android:id="#+id/tvReviews"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:fontFamily="sans-serif-condensed"
android:autoLink="all"
android:linksClickable="true"
android:clickable="true"
android:textSize="#dimen/runtime_textsize"
android:text="Contains 3 reviews" />
try with this code, its working code in my project.
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);
How to set the part of the text view is clickable
You can use this class to make a textview look like web url and even clickable
public final class LinkUtils {
public static final Pattern URL_PATTERN =
Pattern.compile("((https?|ftp)(:\\/\\/[-_.!~*\\'()a-zA-Z0-9;\\/?:\\#&=+\\$,%#]+))");
public interface OnClickListener {
void onLinkClicked(final String link);
void onClicked();
}
static class SensibleUrlSpan extends URLSpan {
/**
* Pattern to match.
*/
private Pattern mPattern;
public SensibleUrlSpan(String url, Pattern pattern) {
super(url);
mPattern = pattern;
}
public boolean onClickSpan(View widget) {
boolean matched = mPattern.matcher(getURL()).matches();
if (matched) {
super.onClick(widget);
}
return matched;
}
}
static class SensibleLinkMovementMethod extends LinkMovementMethod {
private boolean mLinkClicked;
private String mClickedLink;
#Override
public boolean onTouchEvent(TextView widget, Spannable buffer, MotionEvent event) {
int action = event.getAction();
if (action == MotionEvent.ACTION_UP) {
mLinkClicked = false;
mClickedLink = null;
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) {
SensibleUrlSpan span = (SensibleUrlSpan) link[0];
mLinkClicked = span.onClickSpan(widget);
mClickedLink = span.getURL();
return mLinkClicked;
}
}
super.onTouchEvent(widget, buffer, event);
return false;
}
public boolean isLinkClicked() {
return mLinkClicked;
}
public String getClickedLink() {
return mClickedLink;
}
}
public static void autoLink(final TextView view, final OnClickListener listener) {
autoLink(view, listener, null);
}
public static void autoLink(final TextView view, final OnClickListener listener,
final String patternStr) {
String text = view.getText().toString();
if (TextUtils.isEmpty(text)) {
return;
}
Spannable spannable = new SpannableString(text);
Pattern pattern;
if (TextUtils.isEmpty(patternStr)) {
pattern = URL_PATTERN;
} else {
pattern = Pattern.compile(patternStr);
}
Matcher matcher = pattern.matcher(text);
while (matcher.find()) {
SensibleUrlSpan urlSpan = new SensibleUrlSpan(matcher.group(1), pattern);
spannable.setSpan(urlSpan, matcher.start(1), matcher.end(1),
Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
}
view.setText(spannable, TextView.BufferType.SPANNABLE);
final SensibleLinkMovementMethod method = new SensibleLinkMovementMethod();
view.setMovementMethod(method);
if (listener != null) {
view.setOnClickListener(new View.OnClickListener() {
#Override
public void onClick(View v) {
if (method.isLinkClicked()) {
listener.onLinkClicked(method.getClickedLink());
} else {
listener.onClicked();
}
}
});
}
}
}
And in activity call
String testStr = "Text 。http://www.yahoo.com , Text ";
textView1.setTextSize(20);
textView1.setText(testStr);
LinkUtils.autoLink(textView1, new LinkUtils.OnClickListener() {
#Override
public void onLinkClicked(final String link) {
Log.i("Log", "Log"+link);
}
#Override
public void onClicked() {
Log.i("Log", "Log");
}
});
I have a TextView in which all words are individually clickable. I want to begin with every word unstyled. Upon clicking a word, the word should become and remain underlined. I am able to clear the default underline, but nothing happens upon click. (I am capturing and even processing the click, but I cannot get the Span style to change).
The relevant code is below. Thanks in advance for the help.
Custom ClickableSpan:
class WordSpan extends ClickableSpan {
private TextPaint textpaint;
public boolean clicked = false;
#Override
public void updateDrawState(TextPaint ds) {
textpaint = ds;
ds.setUnderlineText(false);
if (clicked)
ds.setUnderlineText(true);
}
#Override
public void onClick(View v) {}
public void setClicked(boolean c) {
clicked = c;
updateDrawState(textpaint);
}
}
From onCreate() I am parsing a txt file and adding each word to a TextView. Within this parsing loop I have the following code:
SpannableString ss = new SpannableString(word.toString());
WordSpan clickableSpan = new WordSpan() {
#Override
public void onClick(View view) {
setClicked(true);
view.invalidate();
}};
ss.setSpan(clickableSpan, 0, word.toString().length(), Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
tvText.append(ss);
tvText.append(" ");
}
tvText.setMovementMethod(LinkMovementMethod.getInstance());
To make individual word clickable you will have to add multiple clickable span to the spannable string. For example to make "foo" and "bar" individually clickable in single Textview you will have to add two clickable span, one for "foo" and other for "bar" and add them to spannable string.
In the example I have split the string using space for simplicity plus you would have to write logic for click of the span.
Clickable span which removes the underline. Additionally you can configure the background and text color on click. You can remove it if you are not going use it.
import android.text.TextPaint;
import android.text.style.ClickableSpan;
public abstract class TouchableSpan extends ClickableSpan {
private boolean mIsPressed;
private int mPressedBackgroundColor;
private int mNormalTextColor;
private int mPressedTextColor;
private int mBackgroundColor;
public TouchableSpan(int normalTextColor,int backgroundColor, int pressedTextColor, int pressedBackgroundColor) {
mBackgroundColor = backgroundColor;
mNormalTextColor = normalTextColor;
mPressedTextColor = pressedTextColor;
mPressedBackgroundColor = pressedBackgroundColor;
}
public void setPressed(boolean isSelected) {
mIsPressed = isSelected;
}
#Override
public void updateDrawState(TextPaint ds) {
super.updateDrawState(ds);
ds.setColor(mIsPressed ? mPressedTextColor : mNormalTextColor);
ds.bgColor = mIsPressed ? mPressedBackgroundColor : mBackgroundColor;
ds.setUnderlineText(!mIsPressed);
}
}
Create a LinkMovementMethod which will take care of your Span. If you remove the color provision you can alter this as well
import android.text.Layout;
import android.text.Selection;
import android.text.Spannable;
import android.text.method.LinkMovementMethod;
import android.text.method.MovementMethod;
import android.view.MotionEvent;
import android.widget.TextView;
public class LinkTouchMovementMethod extends LinkMovementMethod {
private TouchableSpan mPressedSpan;
#Override
public boolean onTouchEvent(TextView textView, Spannable spannable, MotionEvent event) {
if (event.getAction() == MotionEvent.ACTION_DOWN) {
mPressedSpan = getPressedSpan(textView, spannable, event);
if (mPressedSpan != null) {
mPressedSpan.setPressed(true);
Selection.setSelection(spannable, spannable.getSpanStart(mPressedSpan),
spannable.getSpanEnd(mPressedSpan));
}
} else if (event.getAction() == MotionEvent.ACTION_MOVE) {
TouchableSpan touchedSpan = getPressedSpan(textView, spannable, event);
if (mPressedSpan != null && touchedSpan != mPressedSpan) {
mPressedSpan.setPressed(false);
mPressedSpan = null;
Selection.removeSelection(spannable);
}
} else {
if (mPressedSpan != null) {
mPressedSpan.setPressed(false);
super.onTouchEvent(textView, spannable, event);
}
mPressedSpan = null;
Selection.removeSelection(spannable);
}
return true;
}
private TouchableSpan getPressedSpan(TextView textView, Spannable spannable, MotionEvent event) {
int x = (int) event.getX();
int y = (int) event.getY();
x -= textView.getTotalPaddingLeft();
y -= textView.getTotalPaddingTop();
x += textView.getScrollX();
y += textView.getScrollY();
Layout layout = textView.getLayout();
int line = layout.getLineForVertical(y);
int off = layout.getOffsetForHorizontal(line, x);
TouchableSpan[] link = spannable.getSpans(off, off, TouchableSpan.class);
TouchableSpan touchedSpan = null;
if (link.length > 0) {
touchedSpan = link[0];
}
return touchedSpan;
}
}
Then you can use it in the following way:
TextView textView = (TextView)findViewById(R.id.hello_world);
String fooBar = "asdfasdfasdfasf asdfasfasfasd";
String[] clickSpans = fooBar.split(" ");
int clickSpanLength = clickSpans.length;
SpannableStringBuilder spannableStringBuilder = new SpannableStringBuilder();
int totalLength = 0;
int normalColor = getResources().getColor(android.R.color.black);
int clickColor = getResources().getColor(android.R.color.holo_blue_bright);
String separator = " , ";
int separatorLength = separator.length();
for (int i = 0; i < clickSpanLength; i++) {
int currentWordLength = clickSpans[i].length();
spannableStringBuilder.append(clickSpans[i]);
if (i < clickSpanLength - 1) {
spannableStringBuilder.append(" , ");
}
spannableStringBuilder.setSpan(new TouchableSpan(normalColor, Color.TRANSPARENT, clickColor, Color.TRANSPARENT) {
#Override
public void onClick(View widget) {
}
}, totalLength, totalLength + currentWordLength, Spanned.SPAN_INCLUSIVE_EXCLUSIVE);
totalLength = totalLength + currentWordLength + separatorLength;
}
textView.setText(spannableStringBuilder);
textView.setMovementMethod(new LinkTouchMovementMethod());
Background
before you say it's a repost, i have to say i have tried reading other posts, so please keep reading.
The basic way to show links inside a textView can be done in the next way:
final Spanned text = Html.fromHtml(getString(R.string.test));
textView.setText(text);
textView.setMovementMethod(LinkMovementMethod.getInstance());
and the string.xml file could contain :
<string name="test" formatted="false">
<![CDATA[
This<br />
is<br />
<a<br />
test
]]>
</string>
However, we might want to capture the event of clicking on the link, and handle it ourself. not only that, but the link might not need to be of a real url.
many posts exists to show how to do it, but none that i've tried handle it well.
some let the intent being handled on the app, which is quite cumbersome and might mean other apps could handle it too (right?) and i think it requires that the link would be to a specific pattern.
some let the LinkMovementMethod handle it but ruin the clickability effect of the links. some say to change the color to something, but that's not the same.
The problem
How do you simply add a listener to the textview that will tell you which text area was clicked inside it?
What I've tried
currently, i've used LinkMovementMethod . the issues of my code are:
because i don't know how to mimic the clicking as on normal links, it looks weird when you click on it. the background doesn't look right, and i think i need to use the default one of the device, but can't find out how. as oppsosed to what some may say, it's not by using the next code:
textview.getLinkTextColors().getDefaultColor()
on some cases the text might stay clicked .
here's the code:
MainActivity.java (sample of how to use)
public class MainActivity extends Activity {
#Override
public void onCreate(final Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
final String a = "<a href='/a'>aaaa</a>123456<a href='/b'>bbbb</a>7890";
final TextView textView = (TextView) findViewById(R.id.span);
textView.setText(Html.fromHtml(a));
final LinkClickMovementMethodExt linkMovementMethod = new LinkClickMovementMethodExt();
linkMovementMethod.setOnLinkClickedListener(new OnLinkClickedListener() {
#Override
public void onLinkClicked(final TextView clickedTextView, final URLSpan clickedSpan) {
Toast.makeText(MainActivity.this, clickedSpan.getURL(), Toast.LENGTH_SHORT).show();
}
});
textView.setMovementMethod(linkMovementMethod);
}
}
LinkClickMovementMethodExt.java
/** a class to handle clicking on links of textViews . based on http://stackoverflow.com/a/16182500/878126 */
public class LinkClickMovementMethodExt extends LinkMovementMethod {
// TODO check how to get the default background of a clicked link
private final BackgroundColorSpan LINK_COLOR = new BackgroundColorSpan(0xFFACE0F4);
private final Class<URLSpan> spanClass = URLSpan.class;
private OnLinkClickedListener mOnLinkClickedListener;
public interface OnLinkClickedListener {
public void onLinkClicked(TextView textView, URLSpan clickedSpan);
}
#Override
public boolean onTouchEvent(final TextView textView, final Spannable buffer, final MotionEvent event) {
final int action = event.getAction();
if (action == MotionEvent.ACTION_UP || action == MotionEvent.ACTION_DOWN) {
int x = (int) event.getX();
int y = (int) event.getY();
x -= textView.getTotalPaddingLeft();
y -= textView.getTotalPaddingTop();
x += textView.getScrollX();
y += textView.getScrollY();
final Layout layout = textView.getLayout();
final int line = layout.getLineForVertical(y);
final int off = layout.getOffsetForHorizontal(line, x);
/**
* get you interest span
*/
final Object[] spans = buffer.getSpans(off, off, spanClass);
if (spans.length != 0) {
if (action == MotionEvent.ACTION_DOWN) {
Selection.setSelection(buffer, buffer.getSpanStart(spans[0]), buffer.getSpanEnd(spans[0]));
for (final Object span : spans) {
if (span instanceof URLSpan) {
final int start = Selection.getSelectionStart(textView.getText());
final int end = Selection.getSelectionEnd(textView.getText());
final Spannable selectedSpan = (Spannable) textView.getText();
selectedSpan.setSpan(LINK_COLOR, start, end, Spannable.SPAN_EXCLUSIVE_EXCLUSIVE);
textView.setText(selectedSpan);
if (mOnLinkClickedListener != null)
mOnLinkClickedListener.onLinkClicked(textView, (URLSpan) span);
}
}
return false;
} else if (action == MotionEvent.ACTION_UP) {
final Spannable span = (Spannable) textView.getText();
span.removeSpan(LINK_COLOR);
textView.setText(span);
return false;
}
}
}
return super.onTouchEvent(textView, buffer, event);
}
#Override
public boolean canSelectArbitrarily() {
return true;
}
#Override
public boolean onKeyUp(final TextView widget, final Spannable buffer, final int keyCode, final KeyEvent event) {
return false;
}
public void setOnLinkClickedListener(final OnLinkClickedListener onLinkClickedListener) {
this.mOnLinkClickedListener = onLinkClickedListener;
}
}
In XML:
<TextView
android:id="#+id/msgtext"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_margin="10dip"
android:linksClickable="true"
android:textColor="#000000"
android:textColorLink="#EE4E1D"
android:text="TextView" />
In Java:
TextView tvmsg = (TextView)textEntryView.findViewById(R.id.msgtext);
tvmsg.setMovementMethod(LinkMovementMethod.getInstance());
tvmsg.setText(Html.fromHtml(message));
I have a TextView with multiple ClickableSpans in it. When a ClickableSpan is pressed, I want it to change the color of its text.
I have tried setting a color state list as the textColorLink attribute of the TextView. This does not yield the desired result because this causes all the spans to change color when the user clicks anywhere on the TextView.
Interestingly, using textColorHighlight to change the background color works as expected: Clicking on a span changes only the background color of that span and clicking anywhere else in the TextView does nothing.
I have also tried setting ForegroundColorSpans with the same boundaries as the ClickableSpans where I pass the same color state list as above as the color resource. This doesn't work either. The spans always keep the color of the default state in the color state list and never enter the pressed state.
Does anyone know how to do this?
This is the color state list I used:
<selector xmlns:android="http://schemas.android.com/apk/res/android">
<item android:state_pressed="true" android:color="#color/pressed_color"/>
<item android:color="#color/normal_color"/>
</selector>
I finally found a solution that does everything I wanted. It is based on this answer.
This is my modified LinkMovementMethod that marks a span as pressed on the start of a touch event (MotionEvent.ACTION_DOWN) and unmarks it when the touch ends or when the touch location moves out of the span.
public class LinkTouchMovementMethod extends LinkMovementMethod {
private TouchableSpan mPressedSpan;
#Override
public boolean onTouchEvent(TextView textView, Spannable spannable, MotionEvent event) {
if (event.getAction() == MotionEvent.ACTION_DOWN) {
mPressedSpan = getPressedSpan(textView, spannable, event);
if (mPressedSpan != null) {
mPressedSpan.setPressed(true);
Selection.setSelection(spannable, spannable.getSpanStart(mPressedSpan),
spannable.getSpanEnd(mPressedSpan));
}
} else if (event.getAction() == MotionEvent.ACTION_MOVE) {
TouchableSpan touchedSpan = getPressedSpan(textView, spannable, event);
if (mPressedSpan != null && touchedSpan != mPressedSpan) {
mPressedSpan.setPressed(false);
mPressedSpan = null;
Selection.removeSelection(spannable);
}
} else {
if (mPressedSpan != null) {
mPressedSpan.setPressed(false);
super.onTouchEvent(textView, spannable, event);
}
mPressedSpan = null;
Selection.removeSelection(spannable);
}
return true;
}
private TouchableSpan getPressedSpan(
TextView textView,
Spannable spannable,
MotionEvent event) {
int x = (int) event.getX() - textView.getTotalPaddingLeft() + textView.getScrollX();
int y = (int) event.getY() - textView.getTotalPaddingTop() + textView.getScrollY();
Layout layout = textView.getLayout();
int position = layout.getOffsetForHorizontal(layout.getLineForVertical(y), x);
TouchableSpan[] link = spannable.getSpans(position, position, TouchableSpan.class);
TouchableSpan touchedSpan = null;
if (link.length > 0 && positionWithinTag(position, spannable, link[0])) {
touchedSpan = link[0];
}
return touchedSpan;
}
private boolean positionWithinTag(int position, Spannable spannable, Object tag) {
return position >= spannable.getSpanStart(tag) && position <= spannable.getSpanEnd(tag);
}
}
This needs to be applied to the TextView like so:
yourTextView.setMovementMethod(new LinkTouchMovementMethod());
And this is the modified ClickableSpan that edits the draw state based on the pressed state set by the LinkTouchMovementMethod: (it also removes the underline from the links)
public abstract class TouchableSpan extends ClickableSpan {
private boolean mIsPressed;
private int mPressedBackgroundColor;
private int mNormalTextColor;
private int mPressedTextColor;
public TouchableSpan(int normalTextColor, int pressedTextColor, int pressedBackgroundColor) {
mNormalTextColor = normalTextColor;
mPressedTextColor = pressedTextColor;
mPressedBackgroundColor = pressedBackgroundColor;
}
public void setPressed(boolean isSelected) {
mIsPressed = isSelected;
}
#Override
public void updateDrawState(TextPaint ds) {
super.updateDrawState(ds);
ds.setColor(mIsPressed ? mPressedTextColor : mNormalTextColor);
ds.bgColor = mIsPressed ? mPressedBackgroundColor : 0xffeeeeee;
ds.setUnderlineText(false);
}
}
Much simpler solution, IMO:
final int colorForThisClickableSpan = Color.RED; //Set your own conditional logic here.
final ClickableSpan link = new ClickableSpan() {
#Override
public void onClick(final View view) {
//Do something here!
}
#Override
public void updateDrawState(TextPaint ds) {
super.updateDrawState(ds);
ds.setColor(colorForThisClickableSpan);
}
};
All these solutions are too much work.
Just set android:textColorLink in your TextView to some selector. Then create a clickableSpan with no need to override updateDrawState(...). All done.
here a quick example:
In your strings.xml have a declared string like this:
<string name="mystring">This is my message%1$s these words are highlighted%2$s and awesome. </string>
then in your activity:
private void createMySpan(){
final String token = "#";
String myString = getString(R.string.mystring,token,token);
int start = myString.toString().indexOf(token);
//we do -1 since we are about to remove the tokens afterwards so it shifts
int finish = myString.toString().indexOf(token, start+1)-1;
myString = myString.replaceAll(token, "");
//create your spannable
final SpannableString spannable = new SpannableString(myString);
final ClickableSpan clickableSpan = new ClickableSpan() {
#Override
public void onClick(final View view) {
doSomethingOnClick();
}
};
spannable.setSpan(clickableSpan, start, finish, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
mTextView.setMovementMethod(LinkMovementMethod.getInstance());
mTextView.setText(spannable);
}
and heres the important parts ..declare a selector like this calling it myselector.xml:
<?xml version="1.0" encoding="utf-8"?>
<selector xmlns:android="http://schemas.android.com/apk/res/android">
<item android:state_pressed="true" android:color="#color/gold"/>
<item android:color="#color/pink"/>
</selector>
And last in your TextView in xml do this:
<TextView
android:id="#+id/mytextview"
android:background="#android:color/transparent"
android:text="#string/mystring"
android:textColorLink="#drawable/myselector" />
Now you can have a pressed state on your clickableSpan.
legr3c's answer helped me a lot. And I'd like to add a few remarks.
Remark #1.
TextView myTextView = (TextView) findViewById(R.id.my_textview);
myTextView.setMovementMethod(new LinkTouchMovementMethod());
myTextView.setHighlightColor(getResources().getColor(android.R.color.transparent));
SpannableString mySpannable = new SpannableString(text);
mySpannable.setSpan(new TouchableSpan(), 0, 7, 0);
mySpannable.setSpan(new TouchableSpan(), 15, 18, 0);
myTextView.setText(mySpannable, BufferType.SPANNABLE);
I applied LinkTouchMovementMethod to a TextView with two spans. The spans were highlighted with blue when clicked them.
myTextView.setHighlightColor(getResources().getColor(android.R.color.transparent));
fixed the bug.
Remark #2.
Don't forget to get colors from resources when passing normalTextColor, pressedTextColor, and pressedBackgroundColor.
Should pass resolved color instead of resource id here
try this custom ClickableSpan:
class MyClickableSpan extends ClickableSpan {
private String action;
private int fg;
private int bg;
private boolean selected;
public MyClickableSpan(String action, int fg, int bg) {
this.action = action;
this.fg = fg;
this.bg = bg;
}
#Override
public void onClick(View widget) {
Log.d(TAG, "onClick " + action);
}
#Override
public void updateDrawState(TextPaint ds) {
ds.linkColor = selected? fg : 0xffeeeeee;
super.updateDrawState(ds);
}
}
and this SpanWatcher:
class Watcher implements SpanWatcher {
private TextView tv;
private MyClickableSpan selectedSpan = null;
public Watcher(TextView tv) {
this.tv = tv;
}
private void changeColor(Spannable text, Object what, int start, int end) {
// Log.d(TAG, "changeFgColor " + what);
if (what == Selection.SELECTION_END) {
MyClickableSpan[] spans = text.getSpans(start, end, MyClickableSpan.class);
if (spans != null) {
tv.setHighlightColor(spans[0].bg);
if (selectedSpan != null) {
selectedSpan.selected = false;
}
selectedSpan = spans[0];
selectedSpan.selected = true;
}
}
}
#Override
public void onSpanAdded(Spannable text, Object what, int start, int end) {
changeColor(text, what, start, end);
}
#Override
public void onSpanChanged(Spannable text, Object what, int ostart, int oend, int nstart, int nend) {
changeColor(text, what, nstart, nend);
}
#Override
public void onSpanRemoved(Spannable text, Object what, int start, int end) {
}
}
test it in onCreate:
TextView tv = new TextView(this);
tv.setTextSize(40);
tv.setMovementMethod(LinkMovementMethod.getInstance());
SpannableStringBuilder b = new SpannableStringBuilder();
b.setSpan(new Watcher(tv), 0, 0, Spanned.SPAN_INCLUSIVE_INCLUSIVE);
b.append("this is ");
int start = b.length();
MyClickableSpan link = new MyClickableSpan("link0 action", 0xffff0000, 0x88ff0000);
b.append("link 0");
b.setSpan(link, start, b.length(), Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
b.append("\nthis is ");
start = b.length();
b.append("link 1");
link = new MyClickableSpan("link1 action", 0xff00ff00, 0x8800ff00);
b.setSpan(link, start, b.length(), Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
b.append("\nthis is ");
start = b.length();
b.append("link 2");
link = new MyClickableSpan("link2 action", 0xff0000ff, 0x880000ff);
b.setSpan(link, start, b.length(), Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
tv.setText(b);
setContentView(tv);
This is my solution if you got many click elements (we need an interface):
The Interface:
public interface IClickSpannableListener{
void onClickSpannText(String text,int starts,int ends);
}
The class who manage the event:
public class SpecialClickableSpan extends ClickableSpan{
private IClickSpannableListener listener;
private String text;
private int starts, ends;
public SpecialClickableSpan(String text,IClickSpannableListener who,int starts, int ends){
super();
this.text = text;
this.starts=starts;
this.ends=ends;
listener = who;
}
#Override
public void onClick(View widget) {
listener.onClickSpannText(text,starts,ends);
}
}
In main class:
class Main extends Activity implements IClickSpannableListener{
//Global
SpannableString _spannableString;
Object _backGroundColorSpan=new BackgroundColorSpan(Color.BLUE);
private void setTextViewSpannable(){
_spannableString= new SpannableString("You can click «here» or click «in this position»");
_spannableString.setSpan(new SpecialClickableSpan("here",this,15,18),15,19, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
_spannableString.setSpan(new SpecialClickableSpan("in this position",this,70,86),70,86, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
TextView tv = (TextView)findViewBy(R.id.textView1);
tv.setMovementMethod(LinkMovementMethod.getInstance());
tv.setText(spannableString);
}
#Override
public void onClickSpannText(String text, int inicio, int fin) {
System.out.println("click on "+ text);
_spannableString.removeSpan(_backGroundColorSpan);
_spannableString.setSpan(_backGroundColorSpan, inicio, fin, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
((TextView)findViewById(R.id.textView1)).setText(_spannableString);
}
}
King regards to the Steven's answer, if someone needs, here it's implementation in Kotlin:
abstract class TouchableSpan(
private val mNormalTextColor: Int,
private val mPressedTextColor: Int
) : ClickableSpan() {
private var isPressed = false
fun setPressed(isSelected: Boolean) {
isPressed = isSelected
}
override fun updateDrawState(ds: TextPaint) {
super.updateDrawState(ds)
ds.color = if (isPressed) mPressedTextColor else mNormalTextColor
ds.isUnderlineText = false
}
}
class LinkTouchMovementMethod : LinkMovementMethod() {
private var pressedSpan: TouchableSpan? = null
override fun onTouchEvent(
textView: TextView,
spannable: Spannable,
event: MotionEvent
): Boolean {
if (event.action == MotionEvent.ACTION_DOWN) {
pressedSpan = getPressedSpan(textView, spannable, event)
pressedSpan?.setPressed(true)
Selection.setSelection(
spannable, spannable.getSpanStart(pressedSpan),
spannable.getSpanEnd(pressedSpan)
)
} else if (event.action == MotionEvent.ACTION_MOVE) {
val touchedSpan = getPressedSpan(textView, spannable, event)
if (touchedSpan !== pressedSpan) {
pressedSpan?.setPressed(false)
pressedSpan = null
Selection.removeSelection(spannable)
}
} else {
pressedSpan?.setPressed(false)
super.onTouchEvent(textView, spannable, event)
pressedSpan = null
Selection.removeSelection(spannable)
}
return true
}
private fun getPressedSpan(
textView: TextView,
spannable: Spannable,
event: MotionEvent
): TouchableSpan? {
val x = event.x.toInt() - textView.totalPaddingLeft + textView.scrollX
val y = event.y.toInt() - textView.totalPaddingTop + textView.scrollY
val layout = textView.layout
val position = layout.getOffsetForHorizontal(layout.getLineForVertical(y), x.toFloat())
val link = spannable.getSpans(position, position, TouchableSpan::class.java)
if (link.isNotEmpty() && positionWithinTag(position, spannable, link[0])) {
return link[0]
}
return null
}
private fun positionWithinTag(position: Int, spannable: Spannable, tag: Any) =
position >= spannable.getSpanStart(tag)
&& position <= spannable.getSpanEnd(tag)
companion object {
val instance by lazy {
LinkTouchMovementMethod()
}
}
}
Place the java code as below :
package com.synamegames.orbs;
import android.view.MotionEvent;
import android.view.View;
import android.widget.TextView;
public class CustomTouchListener implements View.OnTouchListener {
public boolean onTouch(View view, MotionEvent motionEvent) {
switch(motionEvent.getAction()){
case MotionEvent.ACTION_DOWN:
((TextView) view).setTextColor(0x4F4F4F);
break;
case MotionEvent.ACTION_CANCEL:
case MotionEvent.ACTION_UP:
((TextView) view).setTextColor(0xCDCDCD);
break;
}
return false;
}
}
In the above code specify wat color you want .
Change the style .xml as you want.
<?xml version="1.0" encoding="utf-8"?>
<resources>
<style name="MenuFont">
<item name="android:textSize">20sp</item>
<item name="android:textColor">#CDCDCD</item>
<item name="android:textStyle">normal</item>
<item name="android:clickable">true</item>
<item name="android:layout_weight">1</item>
<item name="android:gravity">left|center</item>
<item name="android:paddingLeft">35dp</item>
<item name="android:layout_width">175dp</item>
<item name="android:layout_height">fill_parent</item>
</style>
Try it out and say is this you want or something else . update me dude.
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"
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.
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
}
});
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));
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();
}
});
its very simple add this line to your code:
tv.setMovementMethod(LinkMovementMethod.getInstance());
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.
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
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);
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());
}
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.
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...
}
});
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();
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)
}