EditText with SpannableStringBuilder and ImageSpan doesn't works fine

12,082

This is the implementation to handle emoticons inside a EditText. This implementation uses the TextWatcher to monitor the EditText changes and detect if some emoticon was removed when some text is deleted.

Note that this implementation also verifies if a text selection was deleted (not only the delete key).

To avoid issues with text prediction when typing a text, it is recommended to surround the emoticon text with spaces (the text prediction can join the emoticon text with the adjacent text).

package com.takamori.testapp;

import java.util.ArrayList;

import android.app.Activity;
import android.graphics.drawable.Drawable;
import android.os.Bundle;
import android.text.Editable;
import android.text.Spanned;
import android.text.TextWatcher;
import android.text.style.ImageSpan;
import android.view.Menu;
import android.view.MenuItem;
import android.widget.EditText;

public class MainActivity extends Activity {

    private EmoticonHandler mEmoticonHandler;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);

        EditText editor = (EditText) findViewById(R.id.messageEditor);
        // Create the emoticon handler.
        mEmoticonHandler = new EmoticonHandler(editor);
    }

    @Override
    public boolean onCreateOptionsMenu(Menu menu) {
        getMenuInflater().inflate(R.menu.main, menu);
        return true;
    }

    @Override
    public boolean onOptionsItemSelected(MenuItem item) {
        switch (item.getItemId()) {
            case R.id.action_insert_emoticon:
                // WARNING: The emoticon text shall be surrounded by spaces
                // to avoid issues with text prediction.
                mEmoticonHandler.insert(" :-) ", R.drawable.smile);
                return true;

            default:
                return super.onOptionsItemSelected(item);
        }
    }

    private static class EmoticonHandler implements TextWatcher {

        private final EditText mEditor;
        private final ArrayList<ImageSpan> mEmoticonsToRemove = new ArrayList<ImageSpan>();

        public EmoticonHandler(EditText editor) {
            // Attach the handler to listen for text changes.
            mEditor = editor;
            mEditor.addTextChangedListener(this);
        }

        public void insert(String emoticon, int resource) {
            // Create the ImageSpan
            Drawable drawable = mEditor.getResources().getDrawable(resource);
            drawable.setBounds(0, 0, drawable.getIntrinsicWidth(), drawable.getIntrinsicHeight());
            ImageSpan span = new ImageSpan(drawable, ImageSpan.ALIGN_BASELINE);

            // Get the selected text.
            int start = mEditor.getSelectionStart();
            int end = mEditor.getSelectionEnd();
            Editable message = mEditor.getEditableText();

            // Insert the emoticon.
            message.replace(start, end, emoticon);
            message.setSpan(span, start, start + emoticon.length(), Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
        }

        @Override
        public void beforeTextChanged(CharSequence text, int start, int count, int after) {
            // Check if some text will be removed.
            if (count > 0) {
                int end = start + count;
                Editable message = mEditor.getEditableText();
                ImageSpan[] list = message.getSpans(start, end, ImageSpan.class);

                for (ImageSpan span : list) {
                    // Get only the emoticons that are inside of the changed
                    // region.
                    int spanStart = message.getSpanStart(span);
                    int spanEnd = message.getSpanEnd(span);
                    if ((spanStart < end) && (spanEnd > start)) {
                        // Add to remove list
                        mEmoticonsToRemove.add(span);
                    }
                }
            }
        }

        @Override
        public void afterTextChanged(Editable text) {
            Editable message = mEditor.getEditableText();

            // Commit the emoticons to be removed.
            for (ImageSpan span : mEmoticonsToRemove) {
                int start = message.getSpanStart(span);
                int end = message.getSpanEnd(span);

                // Remove the span
                message.removeSpan(span);

                // Remove the remaining emoticon text.
                if (start != end) {
                    message.delete(start, end);
                }
            }
            mEmoticonsToRemove.clear();
        }

        @Override
        public void onTextChanged(CharSequence text, int start, int before, int count) {
        }

    }
}
Share:
12,082

Related videos on Youtube

Alex
Author by

Alex

Updated on June 04, 2022

Comments

  • Alex
    Alex about 2 years

    I'm trying to put emoticons inside a EditText. I've managed to do it and it works fine but I have a problem when I try to delete these emoticons from the EditText using the soft keyboard. I can't do this action with a single delete button's click. When I insert a new ImageSpan I replace an imageId for it but when I try to delete de icon I have to delete all the imageId characters before delete the image.

    String fileName = "emoticon1.png";
    Drawable d = new BitmapDrawable(getResources(), fileName);
    String imageId = "[" + fileName + "]";
    int cursorPosition = content.getSelectionStart();
    int end = cursorPosition + imageId.length();
    content.getText().insert(cursorPosition, imageId);
    
    SpannableStringBuilder ss = new SpannableStringBuilder(content.getText());
    d.setBounds(0, 0, d.getIntrinsicWidth(), d.getIntrinsicHeight());
    ImageSpan span = new ImageSpan(d, ImageSpan.ALIGN_BASELINE);
    ss.setSpan(span, cursorPosition, end, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
    content.setText(ss, TextView.BufferType.SPANNABLE);
    content.setSelection(end);
    

    I need to remove the emoticons with a single delete button's click. Could you help me, please?

    Thanks!

    • longi
      longi over 10 years
      what about saving start and end position of your emoticon (f.e. 'HashMap')?! on every character delete you have to check, if the position is the end of an emoticon in you hashmap. if so, delete the whole. (but you have to take care of changes before emoticons)
  • Matt Logan
    Matt Logan over 10 years
    "To avoid issues with text prediction when typing a text, it is recommended to surround the emoticon text with spaces (the text prediction can join the emoticon text with the adjacent text)." Is there a better solution to this problem?
  • vepzfe
    vepzfe almost 7 years
    Thanks so much for this solution. Still works and useful after 4 years (considering how fast Android changes).
  • Ahmed Shahid
    Ahmed Shahid over 2 years
    I am getting ConcurrentModificationException on my mEmoticonsToRemove list. Seems as if the before and after are called from different threads and almost immediately that one method has not stopped using the list while the other one is called. I am testing on Android 12.