TextView Android avec liens cliquables: comment capturer les clics?

116

J'ai un TextView qui rend le HTML de base, contenant plus de 2 liens. Je dois capturer les clics sur les liens et ouvrir les liens - dans ma propre WebView interne (pas dans le navigateur par défaut).

La méthode la plus courante pour gérer le rendu des liens semble être la suivante:

String str_links = "<a href='http://google.com'>Google</a><br /><a href='http://facebook.com'>Facebook</a>";
text_view.setLinksClickable(true);
text_view.setMovementMethod(LinkMovementMethod.getInstance());
text_view.setText( Html.fromHtml( str_links ) );

Cependant, cela provoque l'ouverture des liens dans le navigateur Web interne par défaut (affichant la boîte de dialogue "Terminer l'action avec ...").

J'ai essayé d'implémenter un onClickListener, qui se déclenche correctement lorsque le lien est cliqué, mais je ne sais pas comment déterminer QUEL lien a été cliqué ...

text_view.setOnClickListener(new OnClickListener(){

    public void onClick(View v) {
        // what now...?
    }

});

Alternativement, j'ai essayé de créer une classe LinkMovementMethod personnalisée et d'implémenter onTouchEvent ...

public boolean onTouchEvent(TextView widget, Spannable text, MotionEvent event) {
    String url = text.toString();
    // this doesn't work because the text is not necessarily a URL, or even a single link... 
    // eg, I don't know how to extract the clicked link from the greater paragraph of text
    return false;
}

Des idées?


Exemple de solution

J'ai proposé une solution qui analyse les liens d'une chaîne HTML et les rend cliquables, puis vous permet de répondre à l'URL.

Zane Claes
la source
1
Pourquoi n'utilisez-vous pas Spannable String. ??
Renjith
1
En réalité, le HTML est fourni par un serveur distant, non généré par mon application.
Zane Claes le
Votre exemple de solution est très utile; en utilisant cette approche, je capture bien les clics et je peux lancer une autre activité, avec des paramètres, en fonction du lien sur lequel vous avez cliqué. (Le point clé à comprendre était "Faites quelque chose avec span.getURL()".) Vous pouvez même l'afficher comme réponse, car c'est mieux que la réponse actuellement acceptée!
Jonik

Réponses:

223

Sur la base d' une autre réponse , voici une fonction setTextViewHTML () qui analyse les liens d'une chaîne HTML et les rend cliquables, puis vous permet de répondre à l'URL.

protected void makeLinkClickable(SpannableStringBuilder strBuilder, final URLSpan span)
{
    int start = strBuilder.getSpanStart(span);
    int end = strBuilder.getSpanEnd(span);
    int flags = strBuilder.getSpanFlags(span);
    ClickableSpan clickable = new ClickableSpan() {
        public void onClick(View view) {
            // Do something with span.getURL() to handle the link click...
        }
    };
    strBuilder.setSpan(clickable, start, end, flags);
    strBuilder.removeSpan(span);
}

protected void setTextViewHTML(TextView text, String html)
{
    CharSequence sequence = Html.fromHtml(html);
    SpannableStringBuilder strBuilder = new SpannableStringBuilder(sequence);
    URLSpan[] urls = strBuilder.getSpans(0, sequence.length(), URLSpan.class);   
    for(URLSpan span : urls) {
        makeLinkClickable(strBuilder, span);
    }
    text.setText(strBuilder);
    text.setMovementMethod(LinkMovementMethod.getInstance());       
}
Zane Claes
la source
5
A très bien fonctionné. Avec cette approche (contrairement aux autres réponses), j'ai réussi à 1) capturer les clics et 2) lancer une autre activité, avec des paramètres, en fonction du lien cliqué.
Jonik
parfait ... a sauvé ma journée
maverickosama92
Merveilleux, mais si vous l'appliquez à un ListView (je veux dire, au TextView interne de chaque élément), rend la liste non cliquable, bien que les liens soient toujours cliquables
voghDev
@voghDev cela se produit avec ListViews lorsqu'un Viewde focusableest réglé vrai. Cela se produit généralement avec Buttons / ImageButtons. Essayez d'appeler setFocusable(false)votre TextView.
Sufian le
Assurez-vous d'utiliser text.setMovementMethod(LinkMovementMethod.getInstance());si vous n'utilisez pas URLSpan
rajath
21

Vous avez fait comme suit:

text_view.setMovementMethod(LinkMovementMethod.getInstance());
text_view.setText( Html.fromHtml( str_links ) );

avez-vous essayé dans l'ordre inverse comme indiqué ci-dessous?

text_view.setText( Html.fromHtml( str_links ) );
text_view.setMovementMethod(LinkMovementMethod.getInstance());

Et sans:

text_view.setLinksClickable(true);
ademar111190
la source
3
Cela fonctionne mais sans aucun signal que l'utilisateur a cliqué sur le lien, j'ai besoin d'un repère / animation / mise en évidence lorsque le lien est cliqué ... que dois-je faire?
sabre laser
@StarWars vous pouvez utiliser (StateList) [ developer.android.com/intl/pt-br/guide/topics/resources/… en pur Android, mais avec HTML je ne sais pas.
ademar111190
20

Cela peut être simplement résolu en utilisant Spannable String.Ce que vous voulez vraiment faire (Exigence commerciale) n'est pas clair pour moi, donc le code suivant ne donnera pas une réponse exacte à votre situation, mais je suis peu sûr que cela vous donnera une idée et vous pourrez résoudre votre problème sur la base du code suivant.

Comme vous le faites, j'obtiens également des données via la réponse HTTP et j'ai ajouté du texte souligné supplémentaire dans mon cas "plus" et ce texte souligné ouvrira le navigateur Web sur l'événement de clic. J'espère que cela vous aidera.

TextView decription = (TextView)convertView.findViewById(R.id.library_rss_expan_chaild_des_textView);
String dec=d.get_description()+"<a href='"+d.get_link()+"'><u>more</u></a>";
CharSequence sequence = Html.fromHtml(dec);
SpannableStringBuilder strBuilder = new SpannableStringBuilder(sequence);
UnderlineSpan[] underlines = strBuilder.getSpans(0, 10, UnderlineSpan.class);   
for(UnderlineSpan span : underlines) {
    int start = strBuilder.getSpanStart(span);
    int end = strBuilder.getSpanEnd(span);
    int flags = strBuilder.getSpanFlags(span);
    ClickableSpan myActivityLauncher = new ClickableSpan() {
        public void onClick(View view) {
            Log.e(TAG, "on click");
            Intent intent = new Intent(Intent.ACTION_VIEW, Uri.parse(d.get_link()));
            mContext.startActivity(intent);         
        }
    };
    strBuilder.setSpan(myActivityLauncher, start, end, flags);
}
decription.setText(strBuilder);
decription.setLinksClickable(true);
decription.setMovementMethod(LinkMovementMethod.getInstance());
Dinesh Anuruddha
la source
Génial! J'ai modifié cela pour mon cas. Je modifierai mon message pour inclure le code.
Zane Claes
existe-t-il une solution similaire qui peut être utilisée dans xml?
développeur android
Je l'ai fait fonctionner avec la version modifiée d'OP (dans la question), pas avec ça. (Avec cette version, les clics sont allés directement à la boîte de dialogue "
Terminer l'
J'ai utilisé cette logique, mais j'ai dû remplacer le UnderlineSpan par URLSpan.Aussi nécessaire pour supprimer les anciennes travées du SpannableStringBuilder.
Ray
4
Quelle est la variable «d» ici?
Salman Khan
15

J'ai eu le même problème mais beaucoup de texte mélangé avec quelques liens et courriels. Je pense qu'utiliser 'autoLink' est un moyen plus simple et plus propre de le faire:

  text_view.setText( Html.fromHtml( str_links ) );
  text_view.setLinksClickable(true);
  text_view.setAutoLinkMask(Linkify.ALL); //to open links

Vous pouvez définir Linkify.EMAIL_ADDRESSES ou Linkify.WEB_URLS s'il n'y en a qu'un seul que vous souhaitez utiliser ou définir à partir de la mise en page XML

  android:linksClickable="true"
  android:autoLink="web|email"

Les options disponibles sont: aucune, Web, e-mail, téléphone, carte, toutes

Jordi
la source
1
Salut, existe-t-il un moyen d'intercepter l'intention tirée au moment du clic sur un lien
Manmohan Soni
1
Cela fait 6 ans depuis cette réponse. Bien sûr, cela a peut-être changé dans la dernière version d'Android ^^ Cela ne veut pas dire que cela ne fonctionnait pas à l'époque ^^
Jordi
8

Solution

J'ai implémenté une petite classe à l'aide de laquelle vous pouvez gérer de longs clics sur TextView lui-même et Taps sur les liens dans TextView.

Disposition

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

String str_links = "<a href='http://google.com'>Google</a><br /><a href='http://facebook.com'>Facebook</a>";
text_view.setText( Html.fromHtml( str_links ) );
text_view.setMovementMethod(new TextViewClickMovement(this, context));

Liens

J'espère que ça aide! Vous pouvez trouver le code ici .

Victor Apoyan
la source
Veuillez vérifier à nouveau votre code, à partir de la ligne text_view.setMovementMethod(new TextViewClickMovement(this, context));; Android Studio se plaint que cela contextn'a pas pu être résolu.
X09
Si vous copiez le code source de bitbucket, vous devez changer le lieu du contexte et de l'auditeur comme ceci text_view.setMovementMethod (new TextViewClickMovement (context. This));
Victor Apoyan
Cela analysera deux contextes pour les paramètres. Cela n'a pas fonctionné Monsieur. Bien que la réponse acceptée fonctionne pour moi maintenant
X09
Merci pour votre réponse monsieur, le meilleur là-bas pour ce genre, merci!
Vulovic Vukasin le
8

Une solution plus propre et meilleure, utilisant la bibliothèque Linkify native .

Exemple:

Linkify.addLinks(mTextView, Linkify.ALL);
Fidel Montesino
la source
1
Est-il possible de remplacer un événement onClick en utilisant ceci?
Milind Mevada
7

J'ai créé une fonction d'extension simple dans Kotlin pour capturer les clics sur les liens URL dans un TextView en appliquant un nouveau rappel aux éléments URLSpan.

strings.xml (exemple de lien dans le texte)

<string name="link_string">this is my link: <a href="https://www.google.com/">CLICK</a></string>

Assurez-vous que votre texte fractionné est défini sur TextView avant d'appeler "handleUrlClicks"

textView.text = getString(R.string.link_string)

C'est la fonction d'extension:

/**
 * Searches for all URLSpans in current text replaces them with our own ClickableSpans
 * forwards clicks to provided function.
 */
fun TextView.handleUrlClicks(onClicked: ((String) -> Unit)? = null) {
    //create span builder and replaces current text with it
    text = SpannableStringBuilder.valueOf(text).apply {
        //search for all URL spans and replace all spans with our own clickable spans
        getSpans(0, length, URLSpan::class.java).forEach {
            //add new clickable span at the same position
            setSpan(
                object : ClickableSpan() {
                    override fun onClick(widget: View) {
                        onClicked?.invoke(it.url)
                    }
                },
                getSpanStart(it),
                getSpanEnd(it),
                Spanned.SPAN_INCLUSIVE_EXCLUSIVE
            )
            //remove old URLSpan
            removeSpan(it)
        }
    }
    //make sure movement method is set
    movementMethod = LinkMovementMethod.getInstance()
}

Voici comment je l'appelle:

textView.handleUrlClicks { url ->
    Timber.d("click on found span: $url")
}
nastylion
la source
Awesome! _______
Valentin Yuryev il y a
5

Si vous utilisez Kotlin, j'ai écrit une extension simple pour ce cas:

/**
 * Enables click support for a TextView from a [fullText] String, which one containing one or multiple URLs.
 * The [callback] will be called when a click is triggered.
 */
fun TextView.setTextWithLinkSupport(
    fullText: String,
    callback: (String) -> Unit
) {
    val spannable = SpannableString(fullText)
    val matcher = Patterns.WEB_URL.matcher(spannable)
    while (matcher.find()) {
        val url = spannable.toString().substring(matcher.start(), matcher.end())
        val urlSpan = object : URLSpan(fullText) {
            override fun onClick(widget: View) {
                callback(url)
            }
        }
        spannable.setSpan(urlSpan, matcher.start(), matcher.end(), Spanned.SPAN_EXCLUSIVE_EXCLUSIVE)
    }
    text = spannable
    movementMethod = LinkMovementMethod.getInstance() // Make link clickable
}

Usage:

yourTextView.setTextWithLinkSupport("click on me: https://www.google.fr") {
   Log.e("URL is $it")
}
Phil
la source
1

Une approche alternative, à mon avis plus simple (pour les développeurs paresseux comme moi;)

abstract class LinkAwareActivity : AppCompatActivity() {

    override fun startActivity(intent: Intent?) {
        if(Intent.ACTION_VIEW.equals(intent?.action) && onViewLink(intent?.data.toString(), intent)){
            return
        }

        super.startActivity(intent)
    }

    // return true to consume the link (meaning to NOT call super.startActivity(intent))
    abstract fun onViewLink(url: String?, intent: Intent?): Boolean 
}

Si nécessaire, vous pouvez également vérifier le schéma / type mime de l'intention

user1806772
la source
0

Im utilisant uniquement textView, et définissez la durée pour l'url et le clic de poignée.

J'ai trouvé une solution très élégante ici, sans linkify - d'après cela je sais quelle partie de la chaîne je veux linkify

gérer le lien textview cliquer dans mon application Android

à kotlin:

fun linkify(view: TextView, url: String, context: Context) {

    val text = view.text
    val string = text.toString()
    val span = ClickSpan(object : ClickSpan.OnClickListener {
        override fun onClick() {
            // handle your click
        }
    })

    val start = string.indexOf(url)
    val end = start + url.length
    if (start == -1) return

    if (text is Spannable) {
        text.setSpan(span, start, end, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE)
        text.setSpan(ForegroundColorSpan(ContextCompat.getColor(context, R.color.orange)),
                start, end, Spannable.SPAN_EXCLUSIVE_EXCLUSIVE)
    } else {
        val s = SpannableString.valueOf(text)
        s.setSpan(span, start, end, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE)
        s.setSpan(ForegroundColorSpan(ContextCompat.getColor(context, R.color.orange)),
                start, end, Spannable.SPAN_EXCLUSIVE_EXCLUSIVE)
        view.text = s
    }

    val m = view.movementMethod
    if (m == null || m !is LinkMovementMethod) {
        view.movementMethod = LinkMovementMethod.getInstance()
    }
}

class ClickSpan(private val mListener: OnClickListener) : ClickableSpan() {

    override fun onClick(widget: View) {
        mListener.onClick()
    }

    interface OnClickListener {
        fun onClick()
    }
}

et utilisation: linkify (yourTextView, urlString, context)

Peter
la source