gestisci il link del link testuale nella mia app Android


148

Attualmente sto visualizzando l'input HTML in un TextView in questo modo:

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

L'HTML visualizzato mi viene fornito tramite una risorsa esterna, quindi non posso cambiare le cose come voglio, ma ovviamente posso fare qualche regex manomettendo l'HTML, per cambiare il valore di href, diciamo, con qualcos'altro.

Quello che voglio è essere in grado di gestire un clic sul collegamento direttamente dall'app, piuttosto che avere il collegamento aperto una finestra del browser. Questo è realizzabile? Suppongo che sarebbe possibile impostare il protocollo del valore href su qualcosa come "myApp: //", quindi registrare qualcosa che consenta alla mia app di gestire quel protocollo. Se questo è davvero il modo migliore, mi piacerebbe sapere come è fatto, ma spero che ci sia un modo più semplice per dire semplicemente "quando si fa clic su un collegamento in questa visualizzazione di testo, voglio sollevare un evento che riceve il valore href del collegamento come parametro di input "


Ho trovato qualcos'altro su [Here] [1] [1]: stackoverflow.com/questions/7255249/… Spero che ti possa aiutare ^^
Mr_DK l'

1
David, sto avendo un caso simile al tuo, ricevo anche l'html da una fonte esterna (web), ma come posso regex manomettere il valore href in modo da poter applicare questa soluzione.
X09,

Risposte:


182

Venendo a questo quasi un anno dopo, c'è un modo diverso in cui ho risolto il mio problema particolare. Poiché volevo che il collegamento venisse gestito dalla mia app, esiste una soluzione un po 'più semplice.

Oltre al filtro degli intenti predefinito, lascio semplicemente che la mia attività target ascolti ACTION_VIEWgli intenti, e in particolare quelli con lo schemacom.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>

Ciò significa che i collegamenti che iniziano con com.package.name://saranno gestiti dalla mia attività.

Quindi tutto quello che devo fare è costruire un URL che contenga le informazioni che voglio trasmettere:

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

Nella mia attività di destinazione, posso recuperare questo indirizzo:

Uri data = getIntent().getData();

Nel mio esempio, potrei semplicemente verificare la presenza datadi valori null, perché quando mai non è null, saprò che è stato invocato tramite tale collegamento. Da lì, estraggo dall'URL le istruzioni di cui ho bisogno per poter visualizzare i dati appropriati.


1
Ehi, la tua risposta è perfetta. Funziona bene, ma possiamo anche inviare i dati dei dati passati con questo?
user861973

4
@ user861973: Sì, getDatati dà l'URI completo, che potresti usare anche per getDataStringottenere una rappresentazione testuale. In entrambi i casi, è possibile creare l'URL in modo da contenere tutti i dati necessari com.package.name://my-action/1/2/3/4e estrarre le informazioni da quella stringa.
David Hedlund,

3
Mi ci è voluto un giorno per capire questa idea, ma ti dico una cosa - ne è valsa la pena. Soluzione ben progettata
Dennis

7
ottima soluzione. Non dimenticare di aggiungere questo a Textview in modo che abiliti i collegamenti. tv.setMovementMethod (LinkMovementMethod.getInstance ());
Daniel Benedykt,

3
Scusa, ma in quale metodo devo dire l'attività Uri data = getIntent().getData();? Continuo a ricevere Activity not found to handle intenterrori. - Grazie
rgv,

62

Un altro modo, prende in prestito un po 'da Linkify ma ti consente di personalizzare la tua gestione.

Classe di span personalizzata:

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

Funzione di supporto:

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

Un'altra soluzione è quella di sostituire gli span con i tuoi span personalizzati, visualizzare stackoverflow.com/a/11417498/792677
A-Live

ottima soluzione elegante
Mario Zderic

Ho trovato questa la soluzione più semplice da implementare. Google non ha mai ripulito questo casino per avere un modo per rendere coerenti i collegamenti nelle visualizzazioni di testo cliccabili. È un approccio brutale per forzare un intervallo in esso, ma funziona bene su diverse versioni del sistema operativo .. +1
angryITguy

55

se ci sono più collegamenti nella vista di testo. Ad esempio textview ha "https: //" e "tel no", possiamo personalizzare il metodo LinkMovement e gestire i clic per le parole in base a un modello. In allegato è il metodo di spostamento dei link personalizzato.

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

Questo dovrebbe essere chiamato dalla visualizzazione di testo nel modo seguente:

textViewObject.setMovementMethod(CustomLinkMovementMethod.getInstance(context));

8
Non hai davvero bisogno di passare il contesto così "dietro la schiena" in una variabile statica separata, è un po 'puzzolente. Basta usare widget.getContext()invece.
Sergej Koščejev,

Sì, il contesto può essere rimosso. Grazie per averlo segnalato Sergej
Arun,

6
Funziona magnificamente ma devi chiamare setMovementMethod dopo setText, altrimenti sovrascriverà con il LinkMovementMethod predefinito
Ray Britton

Funziona, ma non è necessario ripetere le estensioni in ogni clic. Anche il filtro per posizione sembra soggetto a errori. Un simile approccio di inizializzazione una tantum è mostrato in questa risposta .
Mister Smith,

@Arun Aggiorna la tua risposta in base ai commenti.
Behrouz.M,

45

Ecco una soluzione più generica basata sulla risposta @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);
}

Per usarlo basta implementare onLinkClickdi TextViewLinkHandlerclasse. Per esempio:

    textView.setMovementMethod(new TextViewLinkHandler() {
        @Override
        public void onLinkClick(String url) {
            Toast.makeText(textView.getContext(), url, Toast.LENGTH_SHORT).show();
        }
    });

2
funziona alla grande. nota: non dimenticare di aggiungere android: autoLink = "web" alla tua visualizzazione di testo. senza attributo autolink, questo non funziona.
Okarakose,

Ho provato tutte le soluzioni elencate qui. Questo è il migliore per me. È chiaro, semplice da usare e potente. Suggerimento: è necessario lavorare con Html.fromHtml per ottenere il massimo.
Kai Wang,

1
Migliore risposta! Grazie! Non dimenticare di aggiungere Android: autoLink = "web" in xml o nel codice LinkifyCompat.addLinks (textView, Linkify.WEB_URLS);
Nikita Axyonov,

1
Quello che sarebbe un requisito apparentemente semplice è un affare abbastanza complesso in Android. Questa soluzione toglie molto di quel dolore. Ho scoperto che autoLink = "web" non è necessario per far funzionare questa soluzione su Android N .. +1
angryITguy

1
Perché nel mondo una classe come questa non esiste in modo nativo - dopo tutti questi anni - è al di là di me. Grazie Android per LinkMovementMethod, ma di solito mi piacerebbe il controllo della mia interfaccia utente. Puoi segnalarmi eventi dell'interfaccia utente e così il MIO controller può gestirli.
methodignature

10

è molto semplice aggiungere questa riga al tuo codice:

tv.setMovementMethod(LinkMovementMethod.getInstance());

1
Grazie per la tua risposta, Jonathan. Sì, sapevo di MovementMethod; ciò di cui non ero sicuro era come specificare che la mia app avrebbe dovuto gestire il clic del collegamento, piuttosto che aprire semplicemente un browser, come farebbe il metodo di spostamento predefinito (vedi la risposta accettata). Grazie comunque.
David Hedlund,

5

Soluzione

Ho implementato una piccola classe con l'aiuto della quale puoi gestire lunghi clic su TextView stesso e Taps sui collegamenti in TextView.

disposizione

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

link

Spero che questo ti aiuti! Puoi trovare il codice qui .


3

Solo per condividere una soluzione alternativa usando una libreria che ho creato. Con Textoo , questo può essere ottenuto come:

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 sorgente HTML dinamica:

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

Questa dovrebbe essere la risposta migliore. Grazie signore per il contributo
Marian Pavel,

3

per chi cerca più opzioni eccone una

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

RISORSE: CodePath


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

1

Ho cambiato il colore del TextView in blu usando ad esempio:

android:textColor="#3399FF"

nel file xml. Come renderlo sottolineato è spiegato qui .

Quindi usa la sua proprietà onClick per specificare un metodo (suppongo che potresti chiamare setOnClickListener(this)come un altro modo), ad esempio:

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

In quel metodo, posso fare tutto ciò che voglio normalmente, come lanciare un intento. Nota che devi ancora fare la myTextView.setMovementMethod(LinkMovementMethod.getInstance());cosa normale , come nel metodo onCreate () della tua attività.


1

Questa risposta estende l'eccellente soluzione di Jonathan S:

È possibile utilizzare il seguente metodo per estrarre i collegamenti dal testo:

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

Questo può essere usato per rimuovere uno dei parametri nel clickify()metodo:

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

Alcune modifiche al 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);
        }
    }

Ora puoi semplicemente impostare il testo su TextView e quindi aggiungere un ascoltatore ad esso:

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

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

});

0

Il modo migliore che ho usato e ha sempre funzionato per me

android:autoLink="web"

8
Questo non risponde affatto alla domanda.
Tom,

0

Esempio: supponiamo di aver impostato del testo nella visualizzazione di testo e di voler fornire un collegamento su una determinata espressione di testo: "Fai clic su #facebook per accedere a facebook.com"

Nel layout xml:

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

In attività:

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

Crea anche un fornitore di tag come:

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

}

Nel file manifest crea come voce per il provider e attività di test come:

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

Ora quando fai clic su #facebook, invocherà testactivtiy. E nell'attività di test è possibile ottenere i dati come:

Uri uri = getIntent().getData();
Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.