Подтвердить что ты не робот

Android ClickableSpan перехватывает событие click

У меня есть TextView в макете. Это так просто. Я помещаю OnClickListener в макет, и некоторая часть TextView имеет значение ClickableSpan. Я хочу, чтобы ClickableSpan выполнял что-то в функции onClick при нажатии и когда щелкнута другая часть TextView, она должна что-то сделать в функциях onClick для OnClickListener макета. Вот мой код.

    RelativeLayout l = (RelativeLayout)findViewById(R.id.contentLayout);
    l.setOnClickListener(new OnClickListener(){
        @Override
        public void onClick(View v) {
            Toast.makeText(MainActivity.this, "whole layout", Toast.LENGTH_SHORT).show();
        }
    });

    TextView textView = (TextView)findViewById(R.id.t1);
    textView.setMovementMethod(LinkMovementMethod.getInstance());

    SpannableString spannableString = new SpannableString(textView.getText().toString());
    ClickableSpan span = new ClickableSpan() {
        @Override
        public void onClick(View widget) {
            Toast.makeText(MainActivity.this, "just word", Toast.LENGTH_SHORT).show();
        }
    };
    spannableString.setSpan(span, 0, 5, Spannable.SPAN_INCLUSIVE_INCLUSIVE);        
    textView.setText(spannableString);
4b9b3361

Ответ 1

Первый ответ на ваш вопрос заключается в том, что вы не устанавливаете прослушиватель кликов в своем TextView, который использует события click, как указывает user2558882. После того, как вы установите прослушиватель кликов в TextView, вы увидите, что области вне зоны касания ClickableSpans будут работать, как ожидалось. Тем не менее, вы обнаружите, что когда вы нажмете на один из ваших ClickableSpans, будет вызван обратный вызов TextView onClick. Это приводит нас к сложной проблеме, если для вас есть проблема с огнем. user2558882 Ответ не может гарантировать, что ваш обратный вызов ClickableSpan onClick будет запущен перед вашим TextView. Вот несколько решений из аналогичного потока, которые лучше реализованы и объяснение от источника. Принятый ответ, что поток должен работать на большинстве устройств, но комментарии для этого ответа указывают на некоторые устройства, имеющие проблемы. Похоже, что некоторые устройства с пользовательскими интерфейсами оператора/производителя виноваты, но это спекуляция.

Итак, почему вы не можете гарантировать onClick callback order? Если вы посмотрите на источник для TextView (Android 4.3), вы заметите, что в методе onTouchEvent boolean superResult = super.onTouchEvent(event); (super is View) вызывается до handled |= mMovement.onTouchEvent(this, (Spannable) mText, event);, который является вызовом вашего метода перемещения, который затем вызывает ваш ClickableSpan onClick. Взглянув на супер (View) onTouchEvent (..), вы заметите:

    // Use a Runnable and post this rather than 
    // performClick directly. This lets other visual 
    // of the view update before click actions start.
    if (mPerformClick == null) {
        mPerformClick = new PerformClick();
    }
    if (!post(mPerformClick)) { // <---- In the case that this won't post, 
        performClick();         //    it'll fallback to calling it directly
    }

performClick() вызывает набор прослушивателей кликов, который в этом случае является нашим прослушивателем TextView. Это означает, что вы не будете знать, в каком порядке будут срабатывать ваши обратные вызовы onClick. То, что вы знаете, заключается в том, что ваши вызовы ClickableSpan и TextView будут вызваны. Решение по потоку, о котором я упоминал ранее, помогает обеспечить порядок, чтобы вы могли использовать флаги.

Если обеспечение совместимости с большим количеством устройств является приоритетом, вам лучше всего воспользоваться вторым взглядом на ваш макет, чтобы убедиться, что вы можете избежать застревания в этой ситуации. Как правило, есть много вариантов компоновки, чтобы обойти такие случаи.

Изменить ответ на комментарий:

Когда ваш TextView выполняет onTouchEvent, он вызывает ваш LinkMovementMethod onTouchEvent, чтобы он мог обрабатывать вызовы для ваших различных методов ClickClick onClick. Ваш LinkMovementMethod делает в своем onTouchEvent следующее:

    @Override
    public boolean onTouchEvent(TextView widget, Spannable buffer,
                            MotionEvent event) {
        int action = event.getAction();

        if (action == MotionEvent.ACTION_UP ||
            action == MotionEvent.ACTION_DOWN) {
            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) {
                if (action == MotionEvent.ACTION_UP) {
                    link[0].onClick(widget);
                } else if (action == MotionEvent.ACTION_DOWN) {
                    Selection.setSelection(buffer,
                                           buffer.getSpanStart(link[0]),
                                           buffer.getSpanEnd(link[0]));
                }

                return true;
            } else {
                Selection.removeSelection(buffer);

            }
         }

        return super.onTouchEvent(widget, buffer, event);
    }

Вы заметите, что он принимает MotionEvent, получает действие (ACTION_UP: поднимающий палец, ACTION_DOWN: нажатие пальца), координаты x и y, где возник сенсор, и затем находит, какой номер строки и смещение (позиция в текст) касание. Наконец, если есть ClickableSpans, которые охватывают эту точку, они извлекаются и вызывается их методы onClick. Поскольку мы хотим передать любые штрихи в родительский макет, вы можете либо вызвать свои макеты onTouchEvent, если хотите, чтобы он делал все, что он делает, когда был затронут, или вы могли бы назвать его клик-слушателем, если это реализует ваши необходимые функции. Здесь, где делают:

         if (link.length != 0) {
                if (action == MotionEvent.ACTION_UP) {
                    link[0].onClick(widget);
                } else if (action == MotionEvent.ACTION_DOWN) {
                    Selection.setSelection(buffer,
                                           buffer.getSpanStart(link[0]),
                                           buffer.getSpanEnd(link[0]));
                }

                return true;
            } else {
                Selection.removeSelection(buffer);

                // Your call to your layout onTouchEvent or it 
                //onClick listener depending on your needs

            }
         }

Итак, чтобы просмотреть, вы создадите новый класс, который расширяет LinkMovementMethod, переопределяет его метод onTouchEvent, копирует и вставляет этот источник с вашими вызовами в правильную позицию, где я комментировал, убедитесь, что вы настраиваете метод движения TextView на этот новый подкласс, и вы должны быть установлены.

Отредактировано снова для предотвращения побочных эффектов Взгляните на источник ScrollingMovementMethod (родитель LinkMovementMethod), и вы увидите, что это метод делегата, который вызывает статический метод return Touch.onTouchEvent(widget, buffer, event); Это означает, что вы можете просто добавить это как свою последнюю строку в методе и избежать вызова super (LinkMovementMethod's) реализация onTouchEvent, которая будет дублировать то, что вы вставляете, и другие события могут провалиться, как ожидалось.

Ответ 2

Я также столкнулся с этой проблемой, и, благодаря исходному коду @KMDev, я придумал гораздо более чистый подход.

Во-первых, поскольку у меня есть только TextView, который должен быть частично кликабельным, на самом деле мне не нужны большинство функций LinkMovementMethod (и его суперкласс ScrollingMovementMethod), который добавляет способность нажатие клавиши, прокрутка и т.д.

Вместо этого создайте пользовательский MovementMethod, который использует код OnTouch() из LinkMovementMethod:

ClickableMovementMethod.java

package com.example.yourapplication;

import android.text.Layout;
import android.text.Selection;
import android.text.Spannable;
import android.text.method.BaseMovementMethod;
import android.text.method.LinkMovementMethod;
import android.text.style.ClickableSpan;
import android.view.MotionEvent;
import android.widget.TextView;

/**
 * A movement method that traverses links in the text buffer and fires clicks. Unlike
 * {@link LinkMovementMethod}, this will not consume touch events outside {@link ClickableSpan}s.
 */
public class ClickableMovementMethod extends BaseMovementMethod {

    private static ClickableMovementMethod sInstance;

    public static ClickableMovementMethod getInstance() {
        if (sInstance == null) {
            sInstance = new ClickableMovementMethod();
        }
        return sInstance;
    }

    @Override
    public boolean canSelectArbitrarily() {
        return false;
    }

    @Override
    public boolean onTouchEvent(TextView widget, Spannable buffer, MotionEvent event) {

        int action = event.getActionMasked();
        if (action == MotionEvent.ACTION_UP || action == MotionEvent.ACTION_DOWN) {

            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) {
                if (action == MotionEvent.ACTION_UP) {
                    link[0].onClick(widget);
                } else {
                    Selection.setSelection(buffer, buffer.getSpanStart(link[0]),
                            buffer.getSpanEnd(link[0]));
                }
                return true;
            } else {
                Selection.removeSelection(buffer);
            }
        }

        return false;
    }

    @Override
    public void initialize(TextView widget, Spannable text) {
        Selection.removeSelection(text);
    }
}

Затем, используя этот ClickableMovementMethod, событие касания больше не будет потребляться методом перемещения. Тем не менее, TextView.setMovementMethod(), который вызывает TextView.fixFocusableAndClickableSettings(), установит кликабельное, долгое нажатие и фокусируемое значение true, которое сделает View.onTouchEvent() потреблением события касания. Чтобы исправить это, просто reset три атрибута.

Итак, окончательный метод утилиты, сопровождающий ClickableMovementMethod, находится здесь:

public static void setTextViewLinkClickable(TextView textView) {
    textView.setMovementMethod(ClickableMovementMethod.getInstance());
    // Reset for TextView.fixFocusableAndClickableSettings(). We don't want View.onTouchEvent()
    // to consume touch events.
    textView.setClickable(false);
    textView.setLongClickable(false);
}

Это работает как прелесть для меня.

Нажмите события на ClickableSpan, и выйдите за их пределы, передайте их в родительский приемник макета.

Обратите внимание, что если вы делаете свой TextView доступным, я не тестировал его на этот случай, и, возможно, вам нужно самому копаться в исходном файле: P

Ответ 3

Вот простое решение, и это сработало для меня

Вы можете добиться этого, используя работу в функциях getSelectionStart() и getSelectionEnd() класса Textview,

tv.setOnClickListener(new View.OnClickListener() {
    @Override
    public void onClick(View v) {
        ClassroomLog.log(TAG, "Textview Click listener ");
        if (tv.getSelectionStart() == -1 && tv.getSelectionEnd() == -1) {
            //This condition will satisfy only when it is not an autolinked text
            //Fired only when you touch the part of the text that is not hyperlinked 
        }
    }
});

Ответ 4

Объявить глобальную переменную boolean:

boolean wordClicked = false;

Объявить и инициализировать l как final:

final RelativeLayout l = (RelativeLayout)findViewById(R.id.contentLayout);

Добавьте OnClickListener в textView:

textView.setOnClickListener(new OnClickListener() {

    @Override
    public void onClick(View v) {
        if (!wordClicked) {
            // Let the click be handled by `l's` OnClickListener
            l.performClick();   
        }
    }
});

Измените span на:

ClickableSpan span = new ClickableSpan() {

    @Override
    public void onClick(View widget) {
        wordClicked = true;
        Toast.makeText(Trial.this, "just word", Toast.LENGTH_SHORT).show();

        // A 100 millisecond delay to let the click event propagate to `textView's` 
        // OnClickListener and to let the check `if (!wordClicked)` fail
        new Handler().postDelayed(new Runnable() {

            @Override
            public void run() {
                wordClicked = false;                        
            }
        }, 100L);
    }
};

Edit:

Поддержание пользовательского ответа KMDev в поле зрения, следующий код будет соответствовать вашим требованиям. Мы создаем два пролета: один для указанной длины: spannableString.setSpan(.., 0, 5, ..);, а другой с остатком: spannableString.setSpan(.., 6, spannableString.legth(), ..);. Второй ClickableSpan (span2) выполняет щелчок по RelativeLayout. Более того, переопределяя updateDrawState(TextPaint), мы можем дать второму пролету неповторимый (нестандартный) вид. В то время как первый диапазон имеет цвет ссылки и подчеркивается.

final RelativeLayout l = (RelativeLayout)findViewById(R.id.contentLayout);
    l.setOnClickListener(new OnClickListener(){
        @Override
        public void onClick(View v) {
            Toast.makeText(Trial.this, "whole layout", Toast.LENGTH_SHORT).show();
        }
    });

TextView textView = (TextView)findViewById(R.id.t1);
textView.setMovementMethod(LinkMovementMethod.getInstance());
textView.setHighlightColor(Color.TRANSPARENT);

SpannableString spannableString = new SpannableString(textView.getText().toString());

ClickableSpan span = new ClickableSpan() {
    @Override
    public void onClick(View widget) {
        Toast.makeText(Trial.this, "just word", Toast.LENGTH_SHORT).show();
    }
};

spannableString.setSpan(span, 0, 5, Spannable.SPAN_INCLUSIVE_INCLUSIVE);     

ClickableSpan span2 = new ClickableSpan() {
    @Override
    public void onClick(View widget) {
        l.performClick();
    }

    @Override
    public void updateDrawState(TextPaint tp) {
        tp.bgColor = getResources().getColor(android.R.color.transparent);
        tp.setUnderlineText(false);
    }
};

spannableString.setSpan(span2, 6, spannableString.length(), 
                                      Spannable.SPAN_INCLUSIVE_INCLUSIVE);  

textView.setText(spannableString);

Особая благодарность пользователю KMDev за то, что вы заметили проблемы с моим оригинальным ответом. Нет необходимости выполнять (ошибочную) проверку с использованием логических переменных, а установка OnClickListener для TextView не требуется.