Android TextView z klikalnymi linkami: jak przechwytywać kliknięcia?

117

Mam TextView, który renderuje podstawowy HTML, zawierający 2+ linki. Muszę przechwytywać kliknięcia linków i otwierać je - w moim własnym wewnętrznym WebView (nie w domyślnej przeglądarce).

Najczęstsza metoda obsługi renderowania linków wygląda następująco:

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

Powoduje to jednak, że łącza otwierają się w domyślnej wewnętrznej przeglądarce internetowej (pokazując okno dialogowe „Zakończ działanie za pomocą ...”).

Próbowałem zaimplementować onClickListener, który poprawnie uruchamia się po kliknięciu linku, ale nie wiem, jak ustalić, który link został kliknięty ...

text_view.setOnClickListener(new OnClickListener(){

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

});

Alternatywnie próbowałem utworzyć niestandardową klasę LinkMovementMethod i zaimplementować 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;
}

Pomysły?


Przykładowe rozwiązanie

Wymyśliłem rozwiązanie, które analizuje linki z ciągu HTML i umożliwia ich klikanie, a następnie pozwala odpowiedzieć na adres URL.

Zane Claes
źródło
1
Dlaczego nie używasz Spannable String. ??
Renjith,
1
W rzeczywistości kod HTML jest dostarczany przez zdalny serwer, a nie generowany przez moją aplikację.
Zane Claes
Twoje przykładowe rozwiązanie jest bardzo pomocne; stosując to podejście, ładnie wychwytuję kliknięcia i mogę uruchomić inną Aktywność, z parametrami zależnymi od tego, który link został kliknięty. (Kluczową kwestią do zrozumienia było „Zrób coś z span.getURL()”.) Możesz nawet opublikować to jako odpowiedź, ponieważ jest to lepsze niż obecnie akceptowana odpowiedź!
Jonik

Odpowiedzi:

223

Opierając się na innej odpowiedzi , oto funkcja setTextViewHTML (), która analizuje linki z ciągu HTML i czyni je klikalnymi, a następnie pozwala odpowiedzieć na adres 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
źródło
5
Działało świetnie. Dzięki takiemu podejściu (w przeciwieństwie do innych odpowiedzi) udało mi się 1) uchwycić kliknięcia i 2) uruchomić kolejną Aktywność, z parametrami zależnymi od tego, który link został kliknięty.
Jonik
doskonały ... uratował mi dzień
maverickosama92
Cudownie, ale jeśli zastosujesz go do ListView (to znaczy do wewnętrznego TextView każdego elementu), sprawia, że ​​lista jest niemożliwa do kliknięcia, chociaż linki są nadal klikalne
voghDev
@voghDev dzieje się tak z ListViews, gdy Viewa focusablejest ustawione na true. Zwykle dzieje się to w przypadku Buttons / ImageButtons. Spróbuj zadzwonić setFocusable(false)na swój TextView.
Sufian,
Upewnij się, że używasz, text.setMovementMethod(LinkMovementMethod.getInstance());jeśli nie używasz adresu URLSpan
rajath
21

Zrobiłeś co następuje:

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

czy próbowałeś w odwrotnej kolejności, jak pokazano poniżej?

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

i bez:

text_view.setLinksClickable(true);
ademar111190
źródło
3
Działa, ale bez żadnego sygnału, że użytkownik kliknął łącze, potrzebuję wskazówki / animacji / podświetlenia po kliknięciu łącza ... co mam zrobić?
miecz świetlny
@StarWars możesz użyć (StateList) [ developer.android.com/intl/pt-br/guide/topics/resources/ ... w czystym Androidzie , ale z HTML nie wiem.
ademar111190
20

Można to po prostu rozwiązać za pomocą Spannable String. To, co naprawdę chcesz zrobić (wymagania biznesowe), jest dla mnie trochę niejasne, więc poniższy kod nie da dokładnej odpowiedzi na twoją sytuację, ale jestem drobna pewność, że da ci to pewien pomysł i będziesz w stanie rozwiązać swój problem w oparciu o poniższy kod.

Jak to robisz, otrzymuję również dane za pośrednictwem odpowiedzi HTTP i dodałem dodatkowy podkreślony tekst w moim przypadku „więcej”, a ten podkreślony tekst otworzy przeglądarkę internetową po kliknięciu. Mam nadzieję, że to ci pomoże.

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
źródło
Wspaniały! Zmodyfikowałem to dla mojego przypadku. Zmodyfikuję swój post, aby zawierał kod.
Zane Claes
czy istnieje podobne rozwiązanie, którego można użyć w XML?
programista Androida
Mam to działające ze zmodyfikowaną wersją OP (w pytaniu), a nie z tym. (W tej wersji kliknięcia prowadziły bezpośrednio do okna dialogowego „
dokończ
Użyłem tej logiki, jednak musiałem zamienić UnderlineSpan na URLSpan, a także usunąć stare span z SpannableStringBuilder.
Ray
4
Co jest tutaj zmienną „d”?
Salman Khan,
15

Miałem ten sam problem, ale dużo tekstu pomieszanego z kilkoma linkami i e-mailami. Myślę, że użycie „autoLink” jest prostszym i czystszym sposobem na zrobienie tego:

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

Możesz ustawić Linkify.EMAIL_ADDRESSES lub Linkify.WEB_URLS, jeśli jest tylko jeden z nich, którego chcesz użyć lub ustawić w układzie XML

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

Dostępne opcje to: brak, internet, e-mail, telefon, mapa, wszystko

Jordi
źródło
1
Cześć, czy jest jakiś sposób na przechwycenie intencji uruchomionej w momencie kliknięcia linku
Manmohan Soni
1
Minęło 6 lat od tej odpowiedzi .. Oczywiście może to się zmieniło w najnowszej wersji Androida ^^ To nie znaczy, że nie pracowała wtedy ^^
Jordi
8

Rozwiązanie

Zaimplementowałem małą klasę, za pomocą której można obsługiwać długie kliknięcia samego TextView i kliknięć w linki w TextView.

Układ

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

Stosowanie

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

Spinki do mankietów

Mam nadzieję, że to helops! Możesz znaleźć kod tutaj .

Victor Apoyan
źródło
Sprawdź ponownie swój kod, zaczynając od linii text_view.setMovementMethod(new TextViewClickMovement(this, context));; Android Studio skarży się, że contextnie można go rozwiązać.
X09
Jeśli kopiujesz kod źródłowy z bitbucket, powinieneś zmienić miejsce kontekstu i nasłuchiwania jak ten text_view.setMovementMethod (nowy TextViewClickMovement (context. This));
Victor Apoyan
To będzie analiza dwóch kontekstów parametrów. Nie działa Sir. Chociaż zaakceptowana odpowiedź działa teraz dla mnie
X09
Dziękuję za odpowiedź sir, najlepszy na świecie, dziękuję!
Vulovic Vukasin
8

O wiele czystsze i lepsze rozwiązanie, wykorzystujące natywną bibliotekę Linkify .

Przykład:

Linkify.addLinks(mTextView, Linkify.ALL);
Fidel Montesino
źródło
1
Czy za pomocą tego można zastąpić zdarzenie onClick?
Milind Mevada
7

Stworzyłem łatwą funkcję rozszerzenia w Kotlin, aby przechwytywać kliknięcia linków url w TextView, stosując nowe wywołanie zwrotne do elementów URLSpan.

strings.xml (przykładowy link w tekście)

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

Upewnij się, że Twój łączony tekst jest ustawiony na TextView przed wywołaniem „handleUrlClicks”

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

To jest funkcja rozszerzenia:

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

Tak to nazywam:

textView.handleUrlClicks { url ->
    Timber.d("click on found span: $url")
}
nastylion
źródło
Świetnie! _______
Valentin Yuryev
5

Jeśli używasz Kotlina, napisałem proste rozszerzenie dla tego przypadku:

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

Stosowanie:

yourTextView.setTextWithLinkSupport("click on me: https://www.google.fr") {
   Log.e("URL is $it")
}
Phil
źródło
1

Alternatywne, o wiele prostsze podejście (dla leniwych programistów takich jak ja;)

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 
}

W razie potrzeby możesz również sprawdzić schemat / typ MIME intencji

user1806772
źródło
0

Używam tylko textView i ustawiam zakres dla kliknięcia adresu URL i uchwytu.

Znalazłem tutaj bardzo eleganckie rozwiązanie, bez linków - zgodnie z tym wiem, którą część łańcucha chcę dodać do linkify

obsłużyć kliknięcie linku Textview w mojej aplikacji na Androida

w kotlinie:

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

i użycie: linkify (yourTextView, urlString, context)

Piotr
źródło