androidandroid-edittextformattingspannablestringspannablestringbuilder

Efficiently applying text formatting on-the-fly by setting Spans onto EditText


I'm using an EditText control that I allow text formatting of (bold, italics etc.).

To apply formatting, within my TextWatcher's AfterTextChanged event handler I detect whether a formatting style, such as bold, has been toggled on via the UI. If it is, I've tried two different approaches, neither of which are satisfactory for different reasons:

Approach 1

textView.EditableText.SetSpan(new StyleSpan(TypefaceStyle.Bold), start, end, SpanTypes.ExclusiveExclusive);

For the start value, I've tried using _textView.SelectionStart - 1 or the starting position when the StyleSpan was first applied. And for the end value _textView.SelectionStart.

Although the text appears formatted fine using this method, it creates unnecessary StyleSpans when only the single would suffice. This is clear when I try to save the text to my local db through a Html conversion:

string html = Html.ToHtml(new SpannableString(Fragment_Textarea.Instance().Textarea().EditableText));

For example, instead of <b>this is bold text</b>, I'm getting <b><b><b><b><b><b><b><b><b><b><b><b><b><b><b><b><b>this is bold text</b></b></b></b></b></b></b></b></b></b></b></b></b></b></b></b></b>. So, clearly, I'm doing something wrong/being inefficient in this approach. What obviously this leads to is eventual slowdowns when both inputting text as well as retrieving at launch.

Something I've considered is to check whether there's a Span on the preceding character (_textView.SelectionStart - 1), and, if yes, to remove the span, and then add a span that starts at that point up until _textView.SelectionStart i.e. ensures there's only a single Span by constantly checking/removing/adding the necessary Span. But this seems like another inefficient method to handle this.

Approach 2

textView.EditableText.SetSpan(new StyleSpan(TypefaceStyle.Bold), start, end, SpanTypes.ExclusiveInclusive);

So, this doesn't lead to the same inefficiencies as above, but because of the SpanTypes.ExclusiveInclusive flag, I'm unable to stop the style formatting to end when I toggle it off via the UI. In other words, when I toggle the Bold style on, all text that follows will be formatted in bold styling, even when I've turned its toggle off.

Of the two, this seems to me like the one that's the correct general approach, and so I'm wondering whether I can do anything to stop the style being applied as soon as I turn its toggle off. Or is there another way that I've missed altogether as best practice for handling this sort of requirement.


Solution

  • So, I ended up taking quite a different approach by moving the responsibility of setting the span to when the button on the Toolbar to active a style is toggled (as opposed to in any of the text changed listeners).

    For example, when the bold style is toggled on, its event handler runs hitting the following code:

    int start = _textarea.SelectionStart - 1;
    var spanType = SpanTypes.ExclusiveInclusive;
    _textarea.EditableText.SetSpan(new StyleSpan(TypefaceStyle.Bold), start, _textarea.SelectionStart, spanType);
    

    The span type needs to be ExclusiveInclusive as suggested above. The trick is to change this as soon as the style has been toggled off. This is relatively straightforward if you're typing in bold and then turn the style off (just a matter of finding the span, removing it and then adding a new span with same start/end points but that's ExcExc). But I needed the code to be more flexible and account for a situation whereby you may later decide to type within the span text of another style. For example, let's say I start with:

    This is bold text

    But then I edit and want to change it to:

    This is bold yes text

    In such a scenario, I need to make sure I create an ExclusiveExclusive bold span on either side of the "yes". :

    int start = -1;
    int end = -1;
    List<Tuple<int, int>> respans = new List<Tuple<int, int>>(); 
    
    // go through all relevant spans that start from -1 indices ago
    var spans = _textarea.EditableText.GetSpans(_textarea.SelectionStart - 1, _textarea.SelectionStart, Class.FromType(typeof(StyleSpan)));
    if (spans.Length > 0)
    {
        for (int u = 0; u < spans.Length; u++)
        {
            // found a matching span!
            if (((StyleSpan)spans[u]).Style == TypefaceStyle.Bold)
            {
                // get the starting and ending indices for the iterated span
                start = _textarea.EditableText.GetSpanStart(spans[u]);
                end = _textarea.EditableText.GetSpanEnd(spans[u]);
    
                // remove the span
                _textarea.EditableText.RemoveSpan(spans[u]);
    
                // if the current index is less than when this iterated span ended
                // and greater than when it started
                // then that means non-bold text is being inserted in the middle of a bold span
                // that needs to be split into 2 (before current index + after current index)
                if (_textarea.SelectionStart > start && _textarea.SelectionStart < end)
                {
                    respans.Add(new Tuple<int, int>(start, _textarea.SelectionStart - 1));
                    for(int c = _textarea.SelectionStart + 1; c < _textarea.Length(); c++ )
                    {
                        if(_textarea.Text[c] != ' ' )
                        {
                            respans.Add(new Tuple<int, int>(c, end));
                            break;
                        }
                    }
                }
                // otherwise, the recreated span needs to start and end when the iterated did
                // with one important change in relation to its span type
                else
                {
                    respans.Add(new Tuple<int, int>(start, end));
                }
            }
        }
    
        // if there are 1 or more spans that need to be restored,
        // go through them and add them back according to start/end points set on their creation
        // as an ExclusiveExclusive span type
        if( respans.Count > 0 )
        {
            foreach( Tuple<int,int> tp in respans )
            {
                _textarea.EditableText.SetSpan(new StyleSpan(TypefaceStyle.Bold), tp.Item1, tp.Item2, SpanTypes.ExclusiveExclusive);
            }
        }
    }
    

    This seems to be doing the job: spans are created/managed when the UI is interacted with (and not text changed) 👍