Redirect touch event on TextView with ClickableSpan to parent view
Asked Answered
L

2

8

I have a RecyclerView with TextViews that could contain custom hashtags that should be clickable. So I have created subclass of TextView in which using Pattern I create ClickableSpan. In order for ClickableSpan to be active, I have added

setMovementMethod(LinkMovementMethod.getInstance())

This method changes properties of TextView:

setFocusable(true);
setClickable(true);
setLongClickable(true);

Click on links works, but it prevents ripple drawable to be shown on list item, and click on TextView outside of hashtags are ignored.

So I'm interested how can I redirect touches on TextView (all except on hashtag) to it's parent?

Leslie answered 26/5, 2015 at 7:50 Comment(0)
L
4

So I haven't found solution on the internet so I ended up implementing it by myself.

So instead of using simple TextView with MovementMethod, I created a new class extending text view with onTouchEvent.

public class MyTextView extends TextView {
@Override
  public boolean onTouchEvent(@NonNull MotionEvent event) {
    if (linksActive) {
      Layout layout = this.getLayout();
      if (layout != null) {
        int line = layout.getLineForVertical((int) event.getY());
        int offset = layout.getOffsetForHorizontal(line, event.getX());

        if (getText() != null && getText() instanceof Spanned) {
          Spanned spanned = (Spanned) getText();

          ClickableSpan[] links = spanned.getSpans(offset, offset, ClickableSpan.class);

          if (links.length > 0) {

            if (event.getAction() == MotionEvent.ACTION_DOWN) {
              return true;
            } else if (event.getAction() == MotionEvent.ACTION_UP) {
              links[0].onClick(this);
            } else {
              return super.onTouchEvent(event);
            }
          }
        }
      }
    }

    return super.onTouchEvent(event);
  }
}

Also I created a class that extends ClickableSpan which handles clicks:

public class URLSpan extends ClickableSpan {
  private String url;
  private int spanType;

  public URLSpan(String url, int spanType) {
    this.url = url;
    this.spanType = spanType;
  }

  public int getType() {
    return spanType;
  }

  @Override
  public void onClick(@NonNull View widget) {
    if (widget instanceof OnSpanClickListener) {
      ((OnSpanClickListener) widget).onSpanClicked(url, type);
    } else {
      // ignored
    }
  }

}

And I this case I have to manage spans by myself (in setText() method)

public static void formatPhoneNumbers(@NonNull Spannable spannable, @ColorInt int color) {
    Matcher matcher = MY_Patterns.PHONE_NO.matcher(spannable);
    while (matcher.find()) {
      if (!Linkify.sPhoneNumberMatchFilter.acceptMatch(spannable, matcher.start(), matcher.end())) {
        continue;
      }

      removeSpans(spannable, SpanType.PHONE_NO, matcher.start(), matcher.end());

      if (hasClickableSpans(spannable, matcher.start(), matcher.end())) {
        // ignore different clickable spans overlap
        continue;
      }

      final ForegroundColorSpan span = new ForegroundColorSpan(color);
      final ClickableSpan urlSpan = new URLSpan(matcher.group(), SpanType.PHONE_NO);

      spannable.setSpan(urlSpan, matcher.start(), matcher.end(), Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
      spannable.setSpan(span, matcher.start(), matcher.end(), Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
    }
  }

  private static void removeSpans(@NonNull Spannable spannable, int type, int start, int end) {
    URLSpan[] arr = spannable.getSpans(start, end, URLSpan.class);
    for (URLSpan span : arr) {
      if (span.getType() == type) {
        spannable.removeSpan(span);
      }
    }
  }

  private static boolean hasClickableSpans(@NonNull Spannable spannable, int start, int end) {
    ClickableSpan[] arr = spannable.getSpans(start, end, ClickableSpan.class);
    return arr.length > 0;
  }
Leslie answered 27/12, 2015 at 11:12 Comment(3)
I have the same problem, This solution already work? Could you deliver the onClick to the Parent (and the ripple effect) instead of the TextView ?Berkly
when do you set linksActive?Laaland
I added that property as an xml attribute and also added public setter for that classLeslie
D
6

Thanks to Viktor --

I use this to allow touch events not on a clickable span to pass through to the parent

override fun dispatchTouchEvent(event: MotionEvent?): Boolean {

        if (hasOnClickListeners() == false && linksClickable == false) {
            // Avoid all touches
            return false
        }

        // Handle avoiding touch events, if not on a clickable span
        if (event != null && hasOnClickListeners() == false) {
            if (layout != null) {
                val line = layout.getLineForVertical(event.y.toInt())
                val offset = layout.getOffsetForHorizontal(line, event.x)

                if (text != null && text is Spanned) {
                    val spanned = text as Spanned

                    val spansAtPoint = spanned.getSpans(offset, offset, ClickableSpan::class.java)
                    if (spansAtPoint.size == 0) {
                        // No clickable span at event -- abort touch
                        return false
                    }
                }
            }
        }
        return super.dispatchTouchEvent(event)
    }
Dactylography answered 4/6, 2019 at 15:16 Comment(0)
L
4

So I haven't found solution on the internet so I ended up implementing it by myself.

So instead of using simple TextView with MovementMethod, I created a new class extending text view with onTouchEvent.

public class MyTextView extends TextView {
@Override
  public boolean onTouchEvent(@NonNull MotionEvent event) {
    if (linksActive) {
      Layout layout = this.getLayout();
      if (layout != null) {
        int line = layout.getLineForVertical((int) event.getY());
        int offset = layout.getOffsetForHorizontal(line, event.getX());

        if (getText() != null && getText() instanceof Spanned) {
          Spanned spanned = (Spanned) getText();

          ClickableSpan[] links = spanned.getSpans(offset, offset, ClickableSpan.class);

          if (links.length > 0) {

            if (event.getAction() == MotionEvent.ACTION_DOWN) {
              return true;
            } else if (event.getAction() == MotionEvent.ACTION_UP) {
              links[0].onClick(this);
            } else {
              return super.onTouchEvent(event);
            }
          }
        }
      }
    }

    return super.onTouchEvent(event);
  }
}

Also I created a class that extends ClickableSpan which handles clicks:

public class URLSpan extends ClickableSpan {
  private String url;
  private int spanType;

  public URLSpan(String url, int spanType) {
    this.url = url;
    this.spanType = spanType;
  }

  public int getType() {
    return spanType;
  }

  @Override
  public void onClick(@NonNull View widget) {
    if (widget instanceof OnSpanClickListener) {
      ((OnSpanClickListener) widget).onSpanClicked(url, type);
    } else {
      // ignored
    }
  }

}

And I this case I have to manage spans by myself (in setText() method)

public static void formatPhoneNumbers(@NonNull Spannable spannable, @ColorInt int color) {
    Matcher matcher = MY_Patterns.PHONE_NO.matcher(spannable);
    while (matcher.find()) {
      if (!Linkify.sPhoneNumberMatchFilter.acceptMatch(spannable, matcher.start(), matcher.end())) {
        continue;
      }

      removeSpans(spannable, SpanType.PHONE_NO, matcher.start(), matcher.end());

      if (hasClickableSpans(spannable, matcher.start(), matcher.end())) {
        // ignore different clickable spans overlap
        continue;
      }

      final ForegroundColorSpan span = new ForegroundColorSpan(color);
      final ClickableSpan urlSpan = new URLSpan(matcher.group(), SpanType.PHONE_NO);

      spannable.setSpan(urlSpan, matcher.start(), matcher.end(), Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
      spannable.setSpan(span, matcher.start(), matcher.end(), Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
    }
  }

  private static void removeSpans(@NonNull Spannable spannable, int type, int start, int end) {
    URLSpan[] arr = spannable.getSpans(start, end, URLSpan.class);
    for (URLSpan span : arr) {
      if (span.getType() == type) {
        spannable.removeSpan(span);
      }
    }
  }

  private static boolean hasClickableSpans(@NonNull Spannable spannable, int start, int end) {
    ClickableSpan[] arr = spannable.getSpans(start, end, ClickableSpan.class);
    return arr.length > 0;
  }
Leslie answered 27/12, 2015 at 11:12 Comment(3)
I have the same problem, This solution already work? Could you deliver the onClick to the Parent (and the ripple effect) instead of the TextView ?Berkly
when do you set linksActive?Laaland
I added that property as an xml attribute and also added public setter for that classLeslie

© 2022 - 2024 — McMap. All rights reserved.