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

Solutions Collecting From Web of "Android ClickableSpan перехватывает событие click"

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

Итак, почему вы не можете гарантировать onClick callback order? Если вы посмотрите на источник для TextView (Android 4.3), вы заметите, что в методе onTouchEvent используется boolean superResult = super.onTouchEvent(event); (Super is View) вызывается перед handled |= mMovement.onTouchEvent(this, (Spannable) mText, event); Который является вызовом вашего метода перемещения, который затем вызывает ваш ClickClick 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 } 

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

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

Изменить ответ для ответа:

Когда ваш TextView выполняет onTouchEvent, он вызывает ваш onTouchEvent LinkMovementMethod, чтобы он мог обрабатывать вызовы для ваших различных методов onClick ClickableSpan. Ваш 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's onTouchEvent or it's //onClick listener depending on your needs } } 

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

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

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

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

Вместо этого создайте пользовательский 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 , событие touch больше не будет потребляться методом перемещения. Тем не менее, TextView.setMovementMethod() который вызывает TextView.fixFocusableAndClickableSettings() , установит кликабельное, долгое нажатие и фокусируемое значение true, которое заставит View.onTouchEvent() потреблять событие touch. Чтобы исправить это, просто сбросьте три атрибута.

Таким образом, окончательный метод утилиты для сопровождения 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 Click on ClickableSpan s уволено, а клик вне их передается в родительский приемник макета.

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

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

Вы можете добиться этого, используя работу с функциями 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 } } }); 

Объявить глобальную 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); } }; 

Редактировать:

Сохраняя ответ пользователя 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 не требуется.