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

148

Actualmente estoy representando la entrada HTML en un TextView así:

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

El HTML que se muestra se me proporciona a través de un recurso externo, por lo que no puedo cambiar las cosas como lo haré, pero, por supuesto, puedo alterar la expresión regular con el HTML, para cambiar el valor href, por ejemplo, a otra cosa.

Lo que quiero es poder manejar un clic en el enlace directamente desde la aplicación, en lugar de que el enlace abra una ventana del navegador. ¿Es esto posible en absoluto? Supongo que sería posible establecer el protocolo del valor href en algo como "myApp: //", y luego registrar algo que permita que mi aplicación maneje ese protocolo. Si esta es realmente la mejor manera, me gustaría saber cómo se hace, pero espero que haya una manera más fácil de decir, "cuando se hace clic en un enlace en esta vista de texto, quiero generar un evento que reciba el valor href del enlace como parámetro de entrada "

David Hedlund
fuente
Encontré algo más en [Aquí] [1] [1]: stackoverflow.com/questions/7255249/… Espero que pueda ayudarlo ^^
Mr_DK
1
David, estoy teniendo un caso similar al tuyo, también obtengo el html de una fuente externa (web), pero ¿cómo regex manipulo el valor href para poder aplicar esta solución?
X09

Respuestas:

182

Llegando a esto casi un año después, hay una manera diferente de resolver mi problema particular. Como quería que el enlace fuera manejado por mi propia aplicación, hay una solución que es un poco más simple.

Además del filtro de intención predeterminado, simplemente dejo que mi actividad objetivo escuche las ACTION_VIEWintenciones, y específicamente, aquellas con el esquemacom.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>

Esto significa que los enlaces que comiencen com.package.name://serán manejados por mi actividad.

Entonces, todo lo que tengo que hacer es construir una URL que contenga la información que quiero transmitir:

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

En mi actividad objetivo, puedo recuperar esta dirección:

Uri data = getIntent().getData();

En mi ejemplo, simplemente podría verificar datalos valores nulos, porque cuando alguna vez no sea nulo, sabré que se invocó mediante dicho enlace. A partir de ahí, extraigo las instrucciones que necesito de la url para poder mostrar los datos apropiados.

David Hedlund
fuente
1
Hola, tu respuesta es perfecta. Funciona bien, pero ¿podemos enviar datos de datos pasados ​​con esto?
user861973
44
@ user861973: Sí, getDatale proporciona el URI completo, también podría usarlo para getDataStringobtener una representación de texto. De cualquier manera, puede construir la URL para que contenga todos los datos que necesita com.package.name://my-action/1/2/3/4, y puede extraer la información de esa cadena.
David Hedlund
3
Me llevó un día entender esta idea, pero les digo qué, valió la pena. Solución bien diseñada
Dennis
77
Gran solución. No olvide agregar esto a la vista de texto para que habilite los enlaces. tv.setMovementMethod (LinkMovementMethod.getInstance ());
Daniel Benedykt
3
Lo siento, pero ¿en qué método de la actividad debo decir Uri data = getIntent().getData();? Sigo recibiendo Activity not found to handle intenterrores. - Gracias
rgv
62

Otra forma, toma prestado un poco de Linkify, pero le permite personalizar su manejo.

Clase de alcance personalizado:

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

Función auxiliar:

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

Uso:

 clickify(textView, clickText,new ClickSpan.OnClickListener()
     {
        @Override
        public void onClick() {
            // do something
        }
    });
Jonathan S.
fuente
Otra solución es reemplazar los Spans con sus Spans personalizados, ver stackoverflow.com/a/11417498/792677
A-Live
gran solución elegante
Mario Zderic
Encontré que esta es la solución más simple de implementar. Google nunca ha limpiado este desastre para tener una manera de hacer que los enlaces en las vistas de texto sean clicables. Es un enfoque brutal para forzar un espacio en él, pero funciona bien en diferentes versiones del sistema operativo. +1
angryITguy
55

si hay múltiples enlaces en la vista de texto. Por ejemplo, textview tiene "https: //" y "tel no". Podemos personalizar el método LinkMovement y manejar los clics de las palabras según un patrón. Se adjunta el método de movimiento de enlace personalizado.

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

Esto debería llamarse desde la vista de texto de la siguiente manera:

textViewObject.setMovementMethod(CustomLinkMovementMethod.getInstance(context));
Arun
fuente
8
Realmente no necesita pasar el contexto, por lo que "detrás de la espalda" en una variable estática separada, es algo maloliente. Solo use widget.getContext()en su lugar.
Sergej Koščejev
Sí, el contexto se puede eliminar. Gracias por señalarlo Sergej
Arun
66
Esto funciona de manera brillante, pero debe llamar a setMovementMethod después de setText, de lo contrario se sobrescribirá con el LinkMovementMethod predeterminado
Ray Britton
Funciona, pero no es necesario iterar sobre tramos en cada clic. Además, el filtro por posición parece propenso a errores. Un enfoque similar de inicialización única se muestra en esta respuesta .
Señor Smith
@Arun Actualice su respuesta de acuerdo con los comentarios.
Behrouz.M
45

Aquí hay una solución más genérica basada en la respuesta @Arun

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

Para usarlo simplemente poner en práctica onLinkClickde TextViewLinkHandlerclase. Por ejemplo:

    textView.setMovementMethod(new TextViewLinkHandler() {
        @Override
        public void onLinkClick(String url) {
            Toast.makeText(textView.getContext(), url, Toast.LENGTH_SHORT).show();
        }
    });
ruX
fuente
2
Funciona genial. nota: no olvide agregar android: autoLink = "web" a su vista de texto. sin atributo de enlace automático, esto no funciona.
okarakose
He probado todas las soluciones enumeradas aquí. Este es el mejor para mí. Es claro, fácil de usar y potente. Sugerencia: debe trabajar con Html.fromHtml para aprovecharlo al máximo.
Kai Wang
1
¡La mejor respuesta! ¡Gracias! No olvide agregar android: autoLink = "web" en xml o en el código LinkifyCompat.addLinks (textView, Linkify.WEB_URLS);
Nikita Axyonov
1
Lo que sería un requisito aparentemente simple es un asunto bastante complejo en Android. Esta solución quita mucho de ese dolor. Descubrí que autoLink = "web" no es necesario para que esta solución funcione en Android N .. +1
angryITguy
1
Por qué en el mundo una clase como esta no existe de forma nativa, después de todos estos años, me supera. Gracias Android por LinkMovementMethod, pero generalmente me gustaría controlar mi propia interfaz de usuario. Puede informarme de los eventos de la IU para que MI controlador pueda manejarlos.
methodignature
10

es muy simple agregar esta línea a su código:

tv.setMovementMethod(LinkMovementMethod.getInstance());
Jonathan
fuente
1
Gracias por tu respuesta, Jonathan. Sí, sabía acerca de MovementMethod; de lo que no estaba seguro era de cómo especificar que mi propia aplicación debería manejar el clic en el enlace, en lugar de solo abrir un navegador, como lo haría el método de movimiento predeterminado (vea la respuesta aceptada). Gracias de cualquier manera.
David Hedlund
5

Solución

He implementado una pequeña clase con la ayuda de la cual puedes manejar clics largos en TextView y Toques 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

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

Enlaces

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

Victor Apoyan
fuente
3

Solo para compartir una solución alternativa usando una biblioteca que creé. Con Textoo , esto se puede lograr como:

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

O con fuente HTML dinámica:

String htmlSource = "Links: <a href='http://www.google.com'>Google</a>";
Spanned linksLoggingText = Textoo
    .config(htmlSource)
    .parseHtml()
    .addLinksHandler(new LinksHandler() {
        @Override
        public boolean onClick(View view, String url) {
            Log.i("MyActivity", "Linking to google...");
            return false; // event not handled.  Continue default processing i.e. link to google
        }
    })
    .apply();
textView.setText(linksLoggingText);
PH88
fuente
Esta debería ser la mejor respuesta. Gracias señor por la contribución
Marian Pavel
3

para quien busca más opciones aquí es uno

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

RECURSO: CodePath

Dasser Basyouni
fuente
2
public static void setTextViewFromHtmlWithLinkClickable(TextView textView, String text) {
    Spanned result;
    if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.N) {
        result = Html.fromHtml(text, Html.FROM_HTML_MODE_LEGACY);
    } else {
        result = Html.fromHtml(text);
    }
    textView.setText(result);
    textView.setMovementMethod(LinkMovementMethod.getInstance());
}
Kai Wang
fuente
1

Cambié el color de TextView a azul usando, por ejemplo:

android:textColor="#3399FF"

en el archivo xml. Aquí se explica cómo subrayarlo .

Luego use su propiedad onClick para especificar un método (supongo que podría llamar de setOnClickListener(this)otra manera), por ejemplo:

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

En ese método, puedo hacer lo que quiera de forma normal, como lanzar una intención. Tenga en cuenta que todavía tiene que hacer lo normal myTextView.setMovementMethod(LinkMovementMethod.getInstance());, como en el método onCreate () de su actividad.

Tyler Collier
fuente
1

Esta respuesta extiende la excelente solución de Jonathan S:

Puede usar el siguiente método para extraer enlaces del texto:

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

Esto se puede usar para eliminar uno de los parámetros del clickify()método:

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

Algunos cambios en 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);
        }
    }

Ahora puede simplemente configurar el texto en TextView y luego agregar un oyente:

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

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

});
WKS
fuente
0

La mejor forma que usé y siempre funcionó para mí

android:autoLink="web"
Rohit Mandiwal
fuente
8
Esto no responde a la pregunta en absoluto.
Tom
0

Ejemplo: suponga que ha configurado un texto en la vista de texto y desea proporcionar un enlace a una expresión de texto particular: "Haga clic en #facebook lo llevará a facebook.com"

En diseño xml:

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

En actividad:

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

También cree un proveedor de etiquetas como:

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

}

En el archivo de manifiesto, haga una entrada para el proveedor y pruebe la actividad como:

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

Ahora, cuando haga clic en #facebook, invocará la testactividad. Y en la actividad de prueba puede obtener los datos como:

Uri uri = getIntent().getData();
Ankit Adlakha
fuente