3

Is there a way to prevent a user from deleting or modifying a spannable in an EditText? More specifically, I have an ImageSpan as the first character of the EditText. I want to ensure the user cannot delete that ImageSpan.

I realize I can use TextWatcher and replace the ImageSpan if the user deletes it. That's rather ugly and I'm hoping there's a way to prevent the deletion in the first place.

Here's a snip of code where I set the text value:

Bitmap bitmap = <bitmap from elsewhere>;
String text = <text to display after ImageSpan, from elsewhere>;

SpannableString ss = new SpannableString (" " + text);
ImageSpan image = new ImageSpan (getContext(), bitmap, ImageSpan.ALIGN_BOTTOM);
ss.setSpan (image, 0, 1, 0);

setText (ss);
Peri Hartman
  • 19,314
  • 18
  • 55
  • 101
  • There're multiple ways to do what you want like setting a custom `InputFilter` or subclassing a `TextView` and providing your own implementation of `InputConnection` with some methods overridden. But using a `TextWatcher` is the simplest solution I'm aware of. – Michael Apr 03 '16 at 17:00
  • So, are you saying there are no Spannable properties to set to handle this? – Peri Hartman Apr 03 '16 at 17:07
  • There's one method I didn't mentioned. I'll write it as an answer. – Michael Apr 03 '16 at 17:11

1 Answers1

6

OK, the solution is quite complicated but it's working. We need a custom InputFilter and a SpanWatcher. Let's start.

The first step is quite simple. We set a non-editable prefix with an image span and set a cursor after this prefix.

final String prefix = "?";
final ImageSpan image = new ImageSpan(this, R.drawable.image);

edit.setText(prefix);
edit.getText().setSpan(image, 0, prefix.length(), Spanned.SPAN_INCLUSIVE_EXCLUSIVE);
edit.setSelection(prefix.length());

Then we set an InputFilter that will prevent the prefix with the span from editing. It can be done by moving a range being edited so it starts after the prefix.

edit.setFilters(new InputFilter[] {
    new InputFilter() {
      @Override
      public CharSequence filter(final CharSequence source, final int start,
          final int end, final Spanned dest, final int dstart, final int dend) {
        final int newStart = Math.max(prefix.length(), dstart);
        final int newEnd = Math.max(prefix.length(), dend);
        if (newStart != dstart || newEnd != dend) {
          final SpannableStringBuilder builder = new SpannableStringBuilder(dest);
          builder.replace(newStart, newEnd, source);
          if (source instanceof Spanned) {
            TextUtils.copySpansFrom(
                (Spanned) source, 0, source.length(), null, builder, newStart);
          }
          Selection.setSelection(builder, newStart + source.length());
          return builder;
        } else {
          return null;
        }
      }
    }
});

Then we create a SpanWatcher that will detect selection changes and move the selection out of the prefix range.

final SpanWatcher watcher = new SpanWatcher() {
  @Override
  public void onSpanAdded(final Spannable text, final Object what,
      final int start, final int end) {
    // Nothing here.
  }

  @Override
  public void onSpanRemoved(final Spannable text, final Object what, 
      final int start, final int end) {
    // Nothing here.
  }

  @Override
  public void onSpanChanged(final Spannable text, final Object what, 
      final int ostart, final int oend, final int nstart, final int nend) {
    if (what == Selection.SELECTION_START) {
      if (nstart < prefix.length()) {
        final int end = Math.max(prefix.length(), Selection.getSelectionEnd(text));
        Selection.setSelection(text, prefix.length(), end);
      }
    } else if (what == Selection.SELECTION_END) {
      final int start = Math.max(prefix.length(), Selection.getSelectionEnd(text));
      final int end = Math.max(start, nstart);
      if (end != nstart) {
        Selection.setSelection(text, start, end);
      }
    }
  }
};

And finally we just add the SpanWatcher to the text.

edit.getText().setSpan(watcher, 0, 0, Spanned.SPAN_INCLUSIVE_EXCLUSIVE);

And that's all. So comparing this solution to just adding a TextWatcher I would prefer the latter approach.

Michael
  • 53,859
  • 22
  • 133
  • 139
  • I haven't gone through your code yet, but I sure appreciate the time you put into it. One would think Google would make it easier to intercept characters being typed into an EditText ! – Peri Hartman Apr 03 '16 at 18:08
  • 1
    In my opinion `TextView` is one of the most complicated UI components. It can be really difficult to do some obvious things with it. – Michael Apr 03 '16 at 18:23