gérer le lien textview cliquez dans mon application Android

148

Je rend actuellement une entrée HTML dans un TextView comme ceci:

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

Le HTML affiché m'est fourni via une ressource externe, donc je ne peux pas changer les choses comme je le ferai, mais je peux, bien sûr, faire des regex falsifier le HTML, pour changer la valeur href, par exemple, en autre chose.

Ce que je veux, c'est pouvoir gérer un clic sur un lien directement à partir de l'application, plutôt que d'avoir le lien ouvrir une fenêtre de navigateur. Est-ce réalisable du tout? Je suppose qu'il serait possible de définir le protocole de la valeur href sur quelque chose comme "myApp: //", puis d'enregistrer quelque chose qui permettrait à mon application de gérer ce protocole. Si c'est effectivement le meilleur moyen, j'aimerais savoir comment cela est fait, mais j'espère qu'il existe un moyen plus simple de dire simplement: "lorsqu'un lien est cliqué dans cette vue de texte, je souhaite déclencher un événement qui reçoit la valeur href du lien comme paramètre d'entrée "

David Hedlund
la source
J'ai trouvé autre chose sur [Ici] [1] [1]: stackoverflow.com/questions/7255249/... J'espère que cela pourra vous aider ^^
Mr_DK
1
David, j'ai un cas similaire au vôtre, j'obtiens aussi le html d'une source externe (web), mais comment puis-je regex altérer la valeur href pour que je puisse appliquer cette solution.
X09

Réponses:

182

Pour en venir à cela presque un an plus tard, il y a une manière différente dont j'ai résolu mon problème particulier. Puisque je voulais que le lien soit géré par ma propre application, il existe une solution un peu plus simple.

Outre le filtre d'intention par défaut, je laisse simplement mon activité cible écouter les ACTION_VIEWintentions, et plus particulièrement celles avec le schémacom.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>

Cela signifie que les liens commençant par com.package.name://seront gérés par mon activité.

Il ne me reste donc qu'à construire une URL contenant les informations que je souhaite transmettre:

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

Dans mon activité cible, je peux récupérer cette adresse:

Uri data = getIntent().getData();

Dans mon exemple, je pourrais simplement vérifier datales valeurs nulles, car chaque fois qu'elle n'est pas nulle, je saurai qu'elle a été invoquée au moyen d'un tel lien. De là, j'extrais les instructions dont j'ai besoin de l'url pour pouvoir afficher les données appropriées.

David Hedlund
la source
1
Hé, ta réponse est parfaite. Cela fonctionne bien, mais pouvons-nous envoyer des données sur les données transmises avec cela?
user861973
4
@ user861973: Oui, getDatavous donne l'URI complet, vous pouvez également utiliser getDataStringqui donne une représentation textuelle. Dans tous les cas, vous pouvez construire l'URL de manière à contenir toutes les données dont vous avez besoin com.package.name://my-action/1/2/3/4, et vous pouvez extraire les informations de cette chaîne.
David Hedlund
3
Il m'a fallu un jour pour comprendre cette idée, mais je vous dis ce que cela en valait vraiment la peine. Solution bien conçue
Dennis
7
excellente solution. N'oubliez pas de l'ajouter à la vue textuelle pour activer les liens. tv.setMovementMethod (LinkMovementMethod.getInstance ());
Daniel Benedykt
3
Pardon, mais dans quelle méthode de l'activité dois-je dire Uri data = getIntent().getData();? Je continue de recevoir des Activity not found to handle intenterreurs. - Merci
rgv
62

Une autre façon, emprunte un peu à Linkify mais vous permet de personnaliser votre prise en main.

Classe de portée personnalisée:

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

Fonction d'assistance:

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

Usage:

 clickify(textView, clickText,new ClickSpan.OnClickListener()
     {
        @Override
        public void onClick() {
            // do something
        }
    });
Jonathan S.
la source
Une autre solution consiste à remplacer les Spans par vos Spans personnalisés, voir stackoverflow.com/a/11417498/792677
A-Live
grande solution élégante
Mario Zderic
J'ai trouvé que c'était la solution la plus simple à mettre en œuvre. Google n'a jamais nettoyé ce désordre pour avoir un moyen de rendre cliquables les liens dans les vues de texte. C'est une approche brutale pour forcer un span, mais cela fonctionne bien sur différentes versions de système d'exploitation. +1
angryITguy
55

s'il y a plusieurs liens dans la vue texte. Par exemple, textview a "https: //" et "tel no", nous pouvons personnaliser la méthode LinkMovement et gérer les clics pour les mots en fonction d'un modèle. Vous trouverez ci-joint la méthode de mouvement de lien personnalisée.

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

Cela doit être appelé à partir de la vue de texte de la manière suivante:

textViewObject.setMovementMethod(CustomLinkMovementMethod.getInstance(context));
Arun
la source
8
Vous n'avez pas vraiment besoin de passer le contexte alors "derrière le dos" dans une variable statique séparée, c'est un peu malodorant. Utilisez simplement à la widget.getContext()place.
Sergej Koščejev
Oui, le contexte peut être supprimé. Merci de l'avoir signalé Sergej
Arun
6
Cela fonctionne à merveille mais vous devez appeler setMovementMethod après setText, sinon il écrasera avec le LinkMovementMethod par défaut
Ray Britton
Fonctionne, mais il n'est pas nécessaire d'itérer sur des périodes à chaque clic. Le filtre par position semble également sujet aux erreurs. Une approche d'initialisation unique similaire est illustrée dans cette réponse .
Mister Smith
@Arun Veuillez mettre à jour votre réponse en fonction des commentaires.
Behrouz.M
45

Voici une solution plus générique basée sur la réponse @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);
}

Pour l' utiliser juste la mise en œuvre onLinkClickde la TextViewLinkHandlerclasse. Par exemple:

    textView.setMovementMethod(new TextViewLinkHandler() {
        @Override
        public void onLinkClick(String url) {
            Toast.makeText(textView.getContext(), url, Toast.LENGTH_SHORT).show();
        }
    });
ruX
la source
2
fonctionne très bien. note: n'oubliez pas d'ajouter android: autoLink = "web" à votre texte. sans attribut de liaison automatique, cela ne fonctionne pas.
okarakose
J'ai essayé toutes les solutions énumérées ici. Celui-ci est le meilleur pour moi. Il est clair, simple à utiliser et puissant. Astuce: vous devez travailler avec Html.fromHtml pour en tirer le meilleur parti.
Kai Wang
1
Meilleure réponse! Merci! N'oubliez pas d'ajouter android: autoLink = "web" en xml ou en code LinkifyCompat.addLinks (textView, Linkify.WEB_URLS);
Nikita Axyonov
1
Ce qui serait une exigence apparemment simple est une affaire assez complexe sous Android. Cette solution enlève beaucoup de cette douleur. Ont constaté que autoLink = "web" n'est pas requis pour que cette solution fonctionne sur Android N .. +1
angryITguy
1
Pourquoi dans le monde une classe comme celle-ci n'existe pas nativement - après toutes ces années - me dépasse. Merci Android pour LinkMovementMethod, mais j'aimerais généralement contrôler ma propre interface utilisateur. Vous pouvez me signaler les événements de l'interface utilisateur afin que MON contrôleur puisse les gérer.
methodsignature
10

c'est très simple, ajoutez cette ligne à votre code:

tv.setMovementMethod(LinkMovementMethod.getInstance());
Jonathan
la source
1
Merci pour votre réponse, Jonathan. Oui, je connaissais MovementMethod; ce dont je n'étais pas sûr, c'était de savoir comment spécifier que ma propre application devrait gérer le clic sur le lien, plutôt que d'ouvrir simplement un navigateur, comme le ferait la méthode de mouvement par défaut (voir la réponse acceptée). Merci quand même.
David Hedlund
5

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

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

Liens

J'espère que cela t'aides! Vous pouvez trouver le code ici .

Victor Apoyan
la source
3

Juste pour partager une solution alternative en utilisant une bibliothèque que j'ai créée. Avec Textoo , cela peut être réalisé comme:

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

Ou avec une source HTML dynamique:

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
la source
Cela devrait être la meilleure réponse. Merci monsieur pour la contribution
Marian Pavel
3

pour qui cherche plus d'options en voici une

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

RESSOURCE: CodePath

Dasser Basyouni
la source
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
la source
1

J'ai changé la couleur de TextView en bleu en utilisant par exemple:

android:textColor="#3399FF"

dans le fichier xml. Comment le faire souligner est expliqué ici .

Ensuite, utilisez sa propriété onClick pour spécifier une méthode (je suppose que vous pourriez l'appeler setOnClickListener(this)autrement), par exemple:

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

Dans cette méthode, je peux faire ce que je veux comme d'habitude, comme lancer une intention. Notez que vous devez toujours faire la myTextView.setMovementMethod(LinkMovementMethod.getInstance());chose normale , comme dans la méthode onCreate () de votre activité.

Tyler Collier
la source
1

Cette réponse prolonge l'excellente solution de Jonathan S:

Vous pouvez utiliser la méthode suivante pour extraire des liens du texte:

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

Cela peut être utilisé pour supprimer l'un des paramètres de la clickify()méthode:

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

Quelques modifications apportées au 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);
        }
    }

Vous pouvez maintenant simplement définir le texte sur TextView, puis y ajouter un écouteur:

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

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

});
WKS
la source
0

La meilleure façon dont j'ai utilisé et cela a toujours fonctionné pour moi

android:autoLink="web"
Rohit Mandiwal
la source
8
Cela ne répond pas du tout à la question.
Tom
0

Exemple: Supposons que vous ayez défini du texte dans textview et que vous souhaitiez fournir un lien sur une expression de texte particulière: "Cliquez sur #facebook vous amènera à facebook.com"

Dans la mise en page xml:

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

En activité:

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

Créez également un fournisseur de balises comme:

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

}

Dans le fichier manifeste, faites comme entrée pour le fournisseur et l'activité de test comme:

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

Maintenant, lorsque vous cliquez sur #facebook, il invoquera testactivtiy. Et dans l'activité de test, vous pouvez obtenir les données comme:

Uri uri = getIntent().getData();
Ankit Adlakha
la source