Android TextView con enlaces en los que se puede hacer clic: ¿cómo capturar clics?

116

Tengo un TextView que está renderizando HTML básico, que contiene más de 2 enlaces. Necesito capturar los clics en los enlaces y abrir los enlaces, en mi propio WebView interno (no en el navegador predeterminado).

El método más común para manejar la representación de enlaces parece ser el siguiente:

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

Sin embargo, esto hace que los enlaces se abran en el navegador web interno predeterminado (que muestra el cuadro de diálogo "Completar acción con ...").

Intenté implementar un onClickListener, que se activa correctamente cuando se hace clic en el enlace, pero no sé cómo determinar EN QUÉ enlace se hizo clic ...

text_view.setOnClickListener(new OnClickListener(){

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

});

Alternativamente, intenté crear una clase LinkMovementMethod personalizada e implementar 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;
}

Ideas?


Solución de ejemplo

Se me ocurrió una solución que analiza los enlaces de una cadena HTML y hace que se pueda hacer clic en ellos, y luego te permite responder a la URL.

Zane Claes
fuente
1
¿Por qué no usas Spannable String?
Renjith
1
En realidad, el HTML lo proporciona un servidor remoto, no lo genera mi aplicación.
Zane Claes
Su solución de ejemplo es muy útil; con ese enfoque, capturo clics muy bien y puedo iniciar otra actividad, con parámetros, según el enlace en el que se haya hecho clic. (El punto clave para entender fue "Hacer algo con span.getURL()".) ¡Incluso podría publicarlo como una respuesta, ya que es mejor que la respuesta aceptada actualmente!
Jonik

Respuestas:

223

Basado en otra respuesta , aquí hay una función setTextViewHTML () que analiza los enlaces de una cadena HTML y hace que se pueda hacer clic en ellos, y luego le permite responder a la 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
fuente
5
Funcionó muy bien. Con este enfoque (a diferencia de las otras respuestas), logré 1) capturar clics y 2) lanzar otra Actividad, con parámetros, según el enlace en el que se hizo clic.
Jonik
perfecto ... me salvó el día
maverickosama92
Maravilloso, pero si lo aplica a un ListView (es decir, al TextView interno de cada elemento), no se puede hacer clic en la lista, aunque todavía se puede hacer clic en los enlaces
voghDev
@voghDev esto sucede con ListViews cuando un View's focusablese establece cierto. Esto suele ocurrir con Buttons / ImageButtons. Intente llamar setFocusable(false)a su TextView.
Sufian
Asegúrese de usar text.setMovementMethod(LinkMovementMethod.getInstance());si no usa URLSpan
rajath
21

Has hecho lo siguiente:

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

¿Lo ha intentado en orden inverso como se muestra a continuación?

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

y sin:

text_view.setLinksClickable(true);
ademar111190
fuente
3
Funciona, pero sin ninguna señal de que el usuario hizo clic en el enlace, necesito una señal / animación / resaltado cuando se hace clic en el enlace ... ¿qué debo hacer?
sable de luz
@StarWars puede usar (StateList) [ developer.android.com/intl/pt-br/guide/topics/resources/… en Android puro, pero con HTML no lo sé.
ademar111190
20

Esto se puede resolver simplemente usando Spannable String. Lo que realmente quiere hacer (Requisito comercial) no está claro para mí, por lo que el siguiente código no dará una respuesta exacta a su situación, pero estoy seguro de que le dará una idea y podrá resolver su problema basándose en el siguiente código.

Mientras lo hace, también obtengo algunos datos a través de la respuesta HTTP y he agregado un texto subrayado adicional en mi caso "más" y este texto subrayado abrirá el navegador web en el evento de clic. Espero que esto le ayude.

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
fuente
¡Excelente! Modifiqué esto para mi caso. Editaré mi publicación para incluir el código.
Zane Claes
¿Existe una solución similar que se pueda usar dentro de xml?
desarrollador de Android
Lo hice funcionar con la versión modificada de OP (en la pregunta), no con esto. (Con esta versión, los clics pasaron directamente al cuadro de diálogo "completar la acción usando").
Jonik
Usé esta lógica, sin embargo tuve que reemplazar el UnderlineSpan con URLSpan. También necesitaba eliminar los tramos antiguos del SpannableStringBuilder.
Ray
4
¿Qué es la variable 'd' aquí?
Salman Khan
15

He tenido el mismo problema, pero mucho texto mezclado con pocos enlaces y correos electrónicos. Creo que usar 'autoLink' es una forma más fácil y limpia de hacerlo:

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

Puede configurar Linkify.EMAIL_ADDRESSES o Linkify.WEB_URLS si solo hay uno de ellos que desea usar o configurar desde el diseño XML

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

Las opciones disponibles son: ninguna, web, correo electrónico, teléfono, mapa, todas

Jordi
fuente
1
Hola, ¿hay alguna forma de interceptar la intención disparada en el momento de hacer clic en un enlace?
Manmohan Soni
1
Han pasado 6 años desde esta respuesta ... Por supuesto que puede haber cambiado en la última versión de Android ^^ No significa que no haya funcionado en ese entonces ^^
Jordi
8

Solución

He implementado una pequeña clase con la ayuda de la cual puede manejar clics largos en TextView y Taps en los enlaces en TextView.

Diseño

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

Uso

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

Enlaces

¡Espero que esto ayude! Puede encontrar el código aquí .

Víctor Apoyan
fuente
Por favor, revise su código nuevamente, desde la línea text_view.setMovementMethod(new TextViewClickMovement(this, context));; Android Studio se queja de que contextno se pudo resolver.
X09
Si copia el código fuente de bitbucket, debe cambiar el lugar del contexto y el oyente como este text_view.setMovementMethod (nuevo TextViewClickMovement (context. This));
Victor Apoyan
Eso analizará dos contextos para los parámetros. No funcionó señor. Aunque la respuesta aceptada me funciona ahora
X09
Gracias por su respuesta señor, el mejor para este tipo, ¡gracias!
Vulovic Vukasin
8

Una solución mucho más limpia y mejor, utilizando la biblioteca Linkify nativa .

Ejemplo:

Linkify.addLinks(mTextView, Linkify.ALL);
Fidel Montesino
fuente
1
¿Es posible anular un evento onClick usando esto?
Milind Mevada
7

Hice una función de extensión fácil en Kotlin para capturar los clics de enlaces de URL en un TextView aplicando una nueva devolución de llamada a los elementos URLSpan.

strings.xml (enlace de ejemplo en texto)

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

Asegúrese de que el texto distribuido esté configurado en TextView antes de llamar a "handleUrlClicks"

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

Esta es la función de extensión:

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

Así es como lo llamo:

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

Si está usando Kotlin, escribí una extensión simple para este caso:

/**
 * 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
}

Uso:

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

Un enfoque alternativo, en mi humilde opinión, más simple (para desarrolladores perezosos como yo;)

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 es necesario, también puede verificar el esquema / tipo mime de la intención

usuario1806772
fuente
0

Estoy usando solo textView, y configuro el intervalo para la URL y el control de clic.

Encontré una solución muy elegante aquí, sin vincular, de acuerdo con eso, sé qué parte de la cadena quiero vincular

manejar el enlace de vista de texto, haga clic en mi aplicación de Android

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

y uso: linkify (yourTextView, urlString, context)

Pedro
fuente