ClickableSpan de Android intercepta el evento de clic
events textview (4)
Tengo un TextView en un diseño. Es tan simple. Puse un OnClickListener en el diseño y parte de TextView está configurada para ser ClickableSpan. Quiero que ClickableSpan haga algo en la función onClick cuando se hace clic y cuando se hace clic en la otra parte de TextView, tiene que hacer algo en las funciones onClick de OnClickListener del diseño. Aquí está mi código.
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);
Aquí hay una solución fácil y funcionó para mí.
Puede lograr esto utilizando una solución alternativa en las funciones getSelectionStart () y getSelectionEnd () de la clase 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
}
}
});
Declara una variable boolean
global:
boolean wordClicked = false;
Declara e inicializa l
como final
:
final RelativeLayout l = (RelativeLayout)findViewById(R.id.contentLayout);
Agrega un OnClickListener
a textView
:
textView.setOnClickListener(new OnClickListener() {
@Override
public void onClick(View v) {
if (!wordClicked) {
// Let the click be handled by `l''s` OnClickListener
l.performClick();
}
}
});
Cambie el span
a:
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);
}
};
Editar:
Manteniendo a la vista la respuesta del usuario KMDev, el siguiente código cumplirá con sus especificaciones. Creamos dos intervalos: uno para la longitud especificada: spannableString.setSpan(.., 0, 5, ..);
y el otro con el resto: spannableString.setSpan(.., 6, spannableString.legth(), ..);
. El segundo ClickableSpan
(span2) realiza un clic en el RelativeLayout
. Además, al reemplazar updateDrawState(TextPaint)
, podemos dar al segundo tramo un aspecto no distintivo (sin estilo). Considerando que, primer tramo tiene un color de enlace y está subrayado.
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);
Gracias especiales al usuario KMDev por notar los problemas con mi respuesta original. No es necesario realizar una comprobación (defectuosa) utilizando variables booleanas, y no es necesario configurar OnclickListener
para TextView.
La primera respuesta a su pregunta es que no está configurando un detector de clics en su TextView, que está consumiendo eventos de clics como lo señala el usuario2558882. Después de configurar un detector de clics en su TextView, verá que las áreas fuera del área táctil de ClickableSpans funcionarán como se espera. Sin embargo, luego encontrará que al hacer clic en uno de sus ClickableSpans, también se activará la devolución de llamada onClick de TextView. Eso nos lleva a un problema difícil si tener ambos disparos es un problema para usted. La respuesta de user2558882 no puede garantizar que la devolución de llamada onClick de ClickableSpan se activará antes que su TextView. Aquí hay algunas soluciones de un hilo similar que están mejor implementadas y una explicación de la fuente. La respuesta aceptada de ese hilo debería funcionar en la mayoría de los dispositivos, pero los comentarios para esa respuesta mencionan que ciertos dispositivos tienen problemas. Parece que algunos dispositivos con IU de operador / fabricante personalizados son los culpables, pero eso es especulación.
Entonces, ¿por qué no puede garantizar onClick orden de devolución de llamada? Si echa un vistazo a la fuente de TextView (Android 4.3), notará que en el método onTouchEvent, boolean superResult = super.onTouchEvent(event);
(super es la vista) se llama antes de handled |= mMovement.onTouchEvent(this, (Spannable) mText, event);
que es la llamada a su método de movimiento que luego llama onClick de ClickableSpan. Echando un vistazo a super''s (Ver) onTouchEvent (..), notará:
// 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 () llama al conjunto de escuchas de clics, que en este caso es el oyente de clics de TextView. Lo que esto significa es que no sabrá en qué orden se activarán sus devoluciones de llamadas onClick. Lo que sí SABES, es que se llamarán a sus oyentes de clics ClickableSpan y TextView. La solución en el hilo que mencioné anteriormente, ayuda a garantizar el orden para que pueda usar indicadores.
Si garantizar la compatibilidad con una gran cantidad de dispositivos es una prioridad, lo mejor es que te sirva un segundo vistazo a tu diseño para ver si puedes evitar quedarte atrapado en esta situación. Por lo general, hay muchas opciones de diseño para bordear casos como este.
Editar para respuesta de comentario:
Cuando su TextView ejecuta onTouchEvent, llama a su LinkMovementMethod''s onTouchEvent para que pueda manejar las llamadas a los diversos métodos onClick de ClickableSpan. Su LinkMovementMethod hace lo siguiente en su 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);
}
Notará que toma el MotionEvent, obtiene la acción (ACTION_UP: levantando el dedo, ACTION_DOWN: presionando el dedo hacia abajo), las coordenadas x e y de donde se originó el toque y luego encuentra qué número de línea y desplazamiento (posición en el texto) el toque golpeó. Finalmente, si hay ClickableSpans que abarcan ese punto, se recuperan y se llaman sus métodos onClick. Ya que queremos transmitir cualquier toque a su diseño principal, puede llamar a sus diseños en Touch Tovent si desea que haga todo lo que hace cuando se toca, o puede llamar a su oyente de clics si implementa la funcionalidad necesaria. Aquí es donde hacer eso:
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
}
}
Entonces, para revisar, creará una nueva clase que extenderá LinkMovementMethod, anulará su método onTouchEvent, copiará y pegará esta fuente con sus llamadas en la posición correcta donde comenté, asegúrese de configurar el método de movimiento de TextView para esta nueva subclase y usted debe ser establecido.
Editado nuevamente para evitar posibles efectos secundarios. Eche un vistazo a la fuente de ScrollingMovementMethod (padre de LinkMovementMethod) y verá que es un método delegado que llama a un método estático de return Touch.onTouchEvent(widget, buffer, event);
Esto significa que puede agregar eso como su última línea en el método y evitar llamar a la implementación onTouchEvent de super''s (LinkMovementMethod) que duplicaría lo que está pegando y otros eventos pueden fallar como se espera.
También me he encontrado con este problema, y gracias al código fuente que mencionó @KMDev, se me ocurrió un enfoque mucho más limpio.
Primero, ya que solo tengo un TextView
que se puede hacer parcialmente accesible, de hecho no necesito la mayoría de las funcionalidades LinkMovementMethod
(y su súper clase ScrollingMovementMethod
) que agrega la capacidad de manejar la pulsación de teclas, el desplazamiento, etc.
En su lugar, cree un MovementMethod
personalizado que use el código OnTouch()
de 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);
}
}
Luego, al utilizar este método ClickableMovementMethod
, el evento táctil no se consumirá más por el método de movimiento. Sin embargo, TextView.setMovementMethod()
que llama a TextView.fixFocusableAndClickableSettings()
, establecerá TextView.setMovementMethod()
, TextView.setMovementMethod()
largos y enfocables a true, lo que hará que View.onTouchEvent()
consuma el evento táctil. Para arreglar esto, simplemente reinicie los tres atributos.
Por lo tanto, el método de utilidad final, para acompañar a ClickableMovementMethod
, está aquí:
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);
}
Esto funciona como un encanto para mí.
Los eventos de ClickableSpan
en ClickableSpan
se activan y los de clic se pasan a la escucha de diseño principal.
Tenga en cuenta que si está seleccionando su TextView
, no he probado ese caso, y tal vez necesite profundizar en la fuente usted mismo: P