Android: ClickableSpan en TextView que se puede hacer clic

Tengo una vista de texto que puede contener enlaces clicables. Cuando se hace clic en uno de estos enlaces, quiero comenzar una actividad. Esto funciona bien, pero también debería ser posible hacer clic en la vista de texto completa y comenzar otra actividad.

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 configuro OnClickListener, cada vez que hago clic en un enlace, primero se llama al oyente de toda la vista de texto y luego al de ClickableSpan.

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

Encontré una solución que es bastante directa. Defina ClickableSpan en todas las áreas de texto que no forman parte de los enlaces y haga clic en ellas 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 …).

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

Matthew sugirió que se subclasificara TextView y con esa sugerencia surgió una solución bastante fea. Pero funciona:

Creé un “ClickPreventableTextView” que uso cuando tengo clickablespans en un TextView en el que se puede hacer clic como un todo.

En su método onTouchEvent, esta clase llama al método onTouchEvent de MovementMethod antes de llamar a TouchEvent en su clase base TextView. Por lo tanto, está garantizado que el Oyente de clickablespan será invocado primero. Y puedo evitar invocar OnClickListener para todo el TextView

 /** * TextView that allows to insert clickablespans while whole textview is still clickable
* If a click an a clickablespan occurs, click handler of whole textview will not 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 lapso cliqueable 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 se llamará dos veces (TextView llama a ese método en su métodoTouchEvent). No sé si esto tiene algún efecto secundario, ATM funciona como se esperaba.

El código es trabajo para mí y eso es del 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; } }); 

Resolvió algo muy similar de una manera muy agradable. ¡Quería tener un texto con un enlace al que se pueda hacer clic! y quería poder presionar el texto Donde no hay ningún enlace y tener un oyente de clic en él. Tomé LinkMovementMethod de grepcode y lo cambié un poco Copiar y pasar esta clase y copiar el final 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(); 

}

Luego, en su 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 subclasificar TextView y cambiar su comportamiento, desafortunadamente. ¿Has pensado en intentar poner un fondo detrás de TextView y adjuntar un onClickListener a él?