Android: ClickableSpan en TextView seleccionable

Tengo un textview que puede contener acoplamientos clickable. Cuando se hace clic en uno de estos enlaces, quiero iniciar una actividad. Esto funciona bien, pero también debería ser posible hacer clic en todo el texto y comenzar otra actividad.

Así que esa es mi solución actual:

TextView tv = (TextView)findViewById(R.id.textview01); Spannable span = Spannable.Factory.getInstance().newSpannable("test link span"); span.setSpan(new ClickableSpan() { @Override public void onClick(View v) { Log.d("main", "link clicked"); Toast.makeText(Main.this, "link clicked", Toast.LENGTH_SHORT).show(); } }, 5, 9, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE); tv.setText(span); tv.setOnClickListener(new OnClickListener() { @Override public void onClick(View v) { Log.d("main", "textview clicked"); Toast.makeText(Main.this, "textview clicked", Toast.LENGTH_SHORT).show(); } }); tv.setMovementMethod(LinkMovementMethod.getInstance()); 

El problema es que cuando establezco un OnClickListener, cada vez que hago clic en un enlace primero el oyente para toda la vista de texto y luego el de ClickableSpan se llama.

¿Hay alguna manera de evitar que android llame al oyente para ver todo el texto, cuando se hace clic en un enlace? O para decidir en el oyente para toda la vista, si se hizo clic en un enlace o no?

Encontrado una solución que es bastante sencillo. Defina ClickableSpan en todas las áreas de texto que no forman parte de los vínculos y maneje el clic en ellos como si se hubiera hecho clic en la vista de texto:

 TextView tv = (TextView)findViewById(R.id.textview01); Spannable span = Spannable.Factory.getInstance().newSpannable("test link span"); span.setSpan(new ClickableSpan() { @Override public void onClick(View v) { Log.d("main", "link clicked"); Toast.makeText(Main.this, "link clicked", Toast.LENGTH_SHORT).show(); } }, 5, 9, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE); // All the rest will have the same spannable. ClickableSpan cs = new ClickableSpan() { @Override public void onClick(View v) { Log.d("main", "textview clicked"); Toast.makeText(Main.this, "textview clicked", Toast.LENGTH_SHORT).show(); } }; // set the "test " spannable. span.setSpan(cs, 0, 5, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE); // set the " span" spannable span.setSpan(cs, 6, span.length(), Spanned.SPAN_EXCLUSIVE_EXCLUSIVE); tv.setText(span); tv.setMovementMethod(LinkMovementMethod.getInstance()); 

Espero que esto ayude (sé que este hilo es viejo, pero en caso de que alguien lo vea ahora …).

Matthew sugirió subclase TextView y con esa pista surgió una solución bastante fea. Pero funciona:

He creado un "ClickPreventableTextView" que utilizo cuando tengo clickablespans en un TextView que debería ser clickable como un todo.

En su método onTouchEvent esta clase llama al método onTouchEvent de MovementMethod antes de llamar a onTouchEvent en su clase de TextView base. Así que se garantiza, que el Listener del clickablespan será invocado primero. Y puedo evitar la invocación de OnClickListener para todo el TextView

 /** * TextView that allows to insert clickablespans while whole textview is still clickable<br> * If a click an a clickablespan occurs, click handler of whole textview will <b>not</b> be invoked * In your span onclick handler you first have to check whether {@link ignoreSpannableClick} returns true, if so just return from click handler * otherwise call {@link preventNextClick} and handle the click event * @author Lukas * */ public class ClickPreventableTextView extends TextView implements OnClickListener { private boolean preventClick; private OnClickListener clickListener; private boolean ignoreSpannableClick; public ClickPreventableTextView(Context context) { super(context); } public ClickPreventableTextView(Context context, AttributeSet attrs) { super(context, attrs); } public ClickPreventableTextView(Context context, AttributeSet attrs, int defStyle) { super(context, attrs, defStyle); } public boolean onTouchEvent(MotionEvent event) { if (getMovementMethod() != null) getMovementMethod().onTouchEvent(this, (Spannable)getText(), event); this.ignoreSpannableClick = true; boolean ret = super.onTouchEvent(event); this.ignoreSpannableClick = false; return ret; } /** * Returns true if click event for a clickable span should be ignored * @return true if click event should be ignored */ public boolean ignoreSpannableClick() { return ignoreSpannableClick; } /** * Call after handling click event for clickable span */ public void preventNextClick() { preventClick = true; } @Override public void setOnClickListener(OnClickListener listener) { this.clickListener = listener; super.setOnClickListener(this); } @Override public void onClick(View v) { if (preventClick) { preventClick = false; } else if (clickListener != null) clickListener.onClick(v); } } 

El oyente para el alcance que se puede hacer clic ahora se ve así

  span.setSpan(new ClickableSpan() { @Override public void onClick(View v) { Log.d("main", "link clicked"); if (widget instanceof ClickPreventableTextView) { if (((ClickPreventableTextView)widget).ignoreSpannableClick()) return; ((ClickPreventableTextView)widget).preventNextClick(); } Toast.makeText(Main.this, "link clicked", Toast.LENGTH_SHORT).show(); } }, 5, 9, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE); 

Para mí la principal desventaja es que ahora getMovementMethod (). OnTouchEvent será llamado dos veces (TextView llama a ese método en su método onTouchEvent). No sé si esto tiene algún efecto secundario, atm funciona como se esperaba.

Esta es una solución bastante fácil .. Esto funcionó para mí

 textView.setOnClickListener(new View.OnClickListener() { @Override public void onClick(View v) { ClassroomLog.log(TAG, "Textview Click listener "); if (textView.getSelectionStart() == -1 && textView.getSelectionEnd() == -1) { // do your code here this will only call if its not a hyperlink } } }); 

Resuelto algo muy similar en una manera muy agradable. Quería tener texto que tiene un enlace que se puede hacer clic! Y quería ser capaz de presionar el texto donde no hay ningún enlace y tener un oyente de clic en él. Tomé el LinkMovementMethod de grepcode y lo cambié un poco de copia y pasado esta clase y copiar la parte inferior y funcionará:

 import android.text.Layout; import android.text.NoCopySpan; import android.text.Selection; import android.text.Spannable; import android.text.method.MovementMethod; import android.text.method.ScrollingMovementMethod; import android.text.style.ClickableSpan; import android.view.KeyEvent; import android.view.MotionEvent; import android.view.View; import android.widget.TextView; public class CustomLinkMovementMethod extends ScrollingMovementMethod { private static final int CLICK = 1; private static final int UP = 2; private static final int DOWN = 3; public abstract interface TextClickedListener { public abstract void onTextClicked(); } TextClickedListener listener = null; public void setOnTextClickListener(TextClickedListener listen){ listener = listen; } @Override public boolean onKeyDown(TextView widget, Spannable buffer, int keyCode, KeyEvent event) { switch (keyCode) { case KeyEvent.KEYCODE_DPAD_CENTER: case KeyEvent.KEYCODE_ENTER: if (event.getRepeatCount() == 0) { if (action(CLICK, widget, buffer)) { return true; } } } return super.onKeyDown(widget, buffer, keyCode, event); } @Override protected boolean up(TextView widget, Spannable buffer) { if (action(UP, widget, buffer)) { return true; } return super.up(widget, buffer); } @Override protected boolean down(TextView widget, Spannable buffer) { if (action(DOWN, widget, buffer)) { return true; } return super.down(widget, buffer); } @Override protected boolean left(TextView widget, Spannable buffer) { if (action(UP, widget, buffer)) { return true; } return super.left(widget, buffer); } @Override protected boolean right(TextView widget, Spannable buffer) { if (action(DOWN, widget, buffer)) { return true; } return super.right(widget, buffer); } private boolean action(int what, TextView widget, Spannable buffer) { boolean handled = false; Layout layout = widget.getLayout(); int padding = widget.getTotalPaddingTop() + widget.getTotalPaddingBottom(); int areatop = widget.getScrollY(); int areabot = areatop + widget.getHeight() - padding; int linetop = layout.getLineForVertical(areatop); int linebot = layout.getLineForVertical(areabot); int first = layout.getLineStart(linetop); int last = layout.getLineEnd(linebot); ClickableSpan[] candidates = buffer.getSpans(first, last, ClickableSpan.class); int a = Selection.getSelectionStart(buffer); int b = Selection.getSelectionEnd(buffer); int selStart = Math.min(a, b); int selEnd = Math.max(a, b); if (selStart < 0) { if (buffer.getSpanStart(FROM_BELOW) >= 0) { selStart = selEnd = buffer.length(); } } if (selStart > last) selStart = selEnd = Integer.MAX_VALUE; if (selEnd < first) selStart = selEnd = -1; switch (what) { case CLICK: if (selStart == selEnd) { return false; } ClickableSpan[] link = buffer.getSpans(selStart, selEnd, ClickableSpan.class); if (link.length != 1) return false; link[0].onClick(widget); break; case UP: int beststart, bestend; beststart = -1; bestend = -1; for (int i = 0; i < candidates.length; i++) { int end = buffer.getSpanEnd(candidates[i]); if (end < selEnd || selStart == selEnd) { if (end > bestend) { beststart = buffer.getSpanStart(candidates[i]); bestend = end; } } } if (beststart >= 0) { Selection.setSelection(buffer, bestend, beststart); return true; } break; case DOWN: beststart = Integer.MAX_VALUE; bestend = Integer.MAX_VALUE; for (int i = 0; i < candidates.length; i++) { int start = buffer.getSpanStart(candidates[i]); if (start > selStart || selStart == selEnd) { if (start < beststart) { beststart = start; bestend = buffer.getSpanEnd(candidates[i]); } } } if (bestend < Integer.MAX_VALUE) { Selection.setSelection(buffer, beststart, bestend); return true; } break; } return false; } public boolean onKeyUp(TextView widget, Spannable buffer, int keyCode, KeyEvent event) { return false; } @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); if (action == MotionEvent.ACTION_UP) { if(listener != null) listener.onTextClicked(); } } } return super.onTouchEvent(widget, buffer, event); } public void initialize(TextView widget, Spannable text) { Selection.removeSelection(text); text.removeSpan(FROM_BELOW); } public void onTakeFocus(TextView view, Spannable text, int dir) { Selection.removeSelection(text); if ((dir & View.FOCUS_BACKWARD) != 0) { text.setSpan(FROM_BELOW, 0, 0, Spannable.SPAN_POINT_POINT); } else { text.removeSpan(FROM_BELOW); } } public static MovementMethod getInstance() { if (sInstance == null) sInstance = new CustomLinkMovementMethod(); return sInstance; } private static CustomLinkMovementMethod sInstance; private static Object FROM_BELOW = new NoCopySpan.Concrete(); 

}

A continuación, en el código donde se agrega la vista de texto:

  CustomLinkMovementMethod link = (CustomLinkMovementMethod)CustomLinkMovementMethod.getInstance(); link.setOnTextClickListener(new CustomLinkMovementMethod.TextClickedListener() { @Override public void onTextClicked() { Toast.makeText(UserProfileActivity.this, "text Pressed", Toast.LENGTH_LONG).show(); } }); YOUR_TEXTVIEW.setMovementMethod(link); 

Creo que esto implica subclase TextView y cambiar su comportamiento, por desgracia. ¿Has pensado en intentar poner un fondo detrás del TextView y adjuntar un onClickListener a él?

El código es trabajo para mí y que es de código fuente de LinkMovementMethod

 tv.setOnTouchListener(new View.OnTouchListener() { @Override public boolean onTouch(View v, MotionEvent event) { TextView tv = (TextView) v; if (action == MotionEvent.ACTION_UP) { int x = (int) event.getX(); int y = (int) event.getY(); Layout layout = tv.getLayout(); int line = layout.getLineForVertical(y); int off = layout.getOffsetForHorizontal(line, x); ClickableSpan[] link = h.diary.contentSpan.getSpans(off, off, ClickableSpan.class); if (link.length != 0) { link[0].onClick(tv); } else { //do other click } } return true; } }); 
  • Cómo configurar la parte de la vista de texto se puede hacer clic
  • Borde de desvanecimiento para TextView verticalmente desplazable
  • Actualización de Textview en una actividad con servicio local
  • Necesita hacer que TextView se desplace verticalmente automáticamente
  • Espaciado de línea individual para cada línea
  • Selector de texto no funciona
  • Justificar texto en TextView Android
  • Excepción de Android TextView y puntero nulo
  • Utilizar programaticamente setTextSize en? Attr / textAppearanceLarge
  • Cómo cambiar el color de fondo de TextView a valor inicial
  • El fondo de TextView se está estirando
  • FlipAndroid es un fan de Google para Android, Todo sobre Android Phones, Android Wear, Android Dev y Aplicaciones para Android Aplicaciones.