8

I'm working on a rich text editor in Android. Basically it has bold, italics and link buttons that are tied to an EditText to change the style of the content. I have it working great if you select the text you want to style first, and then select the button using this method: http://developer.android.com/guide/appendix/faq/commontasks.html#selectingtext.

What I'm trying to do is have it work like a rich text editor, where you can use the buttons as a toggle to style the text for as long as you'd like, then click the toggle again to stop using the style. So if I wanted to type 'Pay attention to this!' in bold, I would click the 'B' button, then start typing the text and everything I type would be bold until I click the 'B' button again.

Any ideas on how to pull this off? I hope I've been clear enough :)

roundhill
  • 700
  • 5
  • 17
  • Just open/close the selected style's HTML tag before/after cursor. – Pentium10 Jun 22 '10 at 19:13
  • I'm not entirely sure you've provided a clear enough indication of what the actual *problem* is. You've stated your goal, but haven't really specified what your current obstacle is. Can you please elaborate? – Brian Lacy Jun 23 '10 at 14:34
  • I'm actually really interested, btw, because I'm interested in eventually adding simple WYSIWYG / rich text editing capabilities to my app. – Brian Lacy Jun 23 '10 at 14:35

5 Answers5

12

For those interested, I got this to work by saving the cursor location ('styleStart' variable below) when the ToggleButton was pressed, and then as the user types more characters, I actually remove the matching StyleSpan with removeSpan(), then re-add it back with setSpan() by using the saved original cursor location + the length of characters currently typed.

You also need to track if the user changes cursor position so that you don't style text that you don't want to. Here's the TextWatcher code:

final EditText contentEdit = (EditText) findViewById(R.id.content);
        contentEdit.addTextChangedListener(new TextWatcher() { 
            public void afterTextChanged(Editable s) { 
                //add style as the user types if a toggle button is enabled
                ToggleButton boldButton = (ToggleButton) findViewById(R.id.bold);
                ToggleButton emButton = (ToggleButton) findViewById(R.id.em);
                ToggleButton bquoteButton = (ToggleButton) findViewById(R.id.bquote);
                ToggleButton underlineButton = (ToggleButton) findViewById(R.id.underline);
                ToggleButton strikeButton = (ToggleButton) findViewById(R.id.strike);
                int position = Selection.getSelectionStart(contentEdit.getText());
                if (position < 0){
                    position = 0;
                }

                if (position > 0){

                    if (styleStart > position || position > (cursorLoc + 1)){
                        //user changed cursor location, reset
                        styleStart = position - 1;
                    }

                    cursorLoc = position;

                    if (boldButton.isChecked()){  
                        StyleSpan[] ss = s.getSpans(styleStart, position, StyleSpan.class);

                        for (int i = 0; i < ss.length; i++) {
                            if (ss[i].getStyle() == android.graphics.Typeface.BOLD){
                                s.removeSpan(ss[i]);
                            }
                        }
                        s.setSpan(new StyleSpan(android.graphics.Typeface.BOLD), styleStart, position, Spannable.SPAN_EXCLUSIVE_EXCLUSIVE);
                    }
                    if (emButton.isChecked()){
                        StyleSpan[] ss = s.getSpans(styleStart, position, StyleSpan.class);

                        boolean exists = false;
                        for (int i = 0; i < ss.length; i++) {
                            if (ss[i].getStyle() == android.graphics.Typeface.ITALIC){
                                s.removeSpan(ss[i]);
                            }
                        }
                        s.setSpan(new StyleSpan(android.graphics.Typeface.ITALIC), styleStart, position, Spannable.SPAN_EXCLUSIVE_EXCLUSIVE);
                    }
                    if (bquoteButton.isChecked()){

                        QuoteSpan[] ss = s.getSpans(styleStart, position, QuoteSpan.class);

                        for (int i = 0; i < ss.length; i++) {
                                s.removeSpan(ss[i]);
                        }
                        s.setSpan(new QuoteSpan(), styleStart, position, Spannable.SPAN_EXCLUSIVE_EXCLUSIVE);
                    }
                    if (underlineButton.isChecked()){
                        UnderlineSpan[] ss = s.getSpans(styleStart, position, UnderlineSpan.class);

                        for (int i = 0; i < ss.length; i++) {
                                s.removeSpan(ss[i]);
                        }
                        s.setSpan(new UnderlineSpan(), styleStart, position, Spannable.SPAN_EXCLUSIVE_EXCLUSIVE);
                    }
                    if (strikeButton.isChecked()){
                        StrikethroughSpan[] ss = s.getSpans(styleStart, position, StrikethroughSpan.class);

                        for (int i = 0; i < ss.length; i++) {
                                s.removeSpan(ss[i]);
                        }
                        s.setSpan(new StrikethroughSpan(), styleStart, position, Spannable.SPAN_EXCLUSIVE_EXCLUSIVE);
                    }
                }
            } 
            public void beforeTextChanged(CharSequence s, int start, int count, int after) { 
                    //unused
            } 
            public void onTextChanged(CharSequence s, int start, int before, int count) { 
                        //unused
                } 
});

And here's one of the ToggleButton click actions:

final ToggleButton boldButton = (ToggleButton) findViewById(R.id.bold);   

            boldButton.setOnClickListener(new Button.OnClickListener() {
                public void onClick(View v) {

                    EditText contentText = (EditText) findViewById(R.id.content);

                    int selectionStart = contentText.getSelectionStart();

                    styleStart = selectionStart;

                    int selectionEnd = contentText.getSelectionEnd();

                    if (selectionStart > selectionEnd){
                        int temp = selectionEnd;
                        selectionEnd = selectionStart;
                        selectionStart = temp;
                    }


                    if (selectionEnd > selectionStart)
                    {
                        Spannable str = contentText.getText();
                        StyleSpan[] ss = str.getSpans(selectionStart, selectionEnd, StyleSpan.class);

                        boolean exists = false;
                        for (int i = 0; i < ss.length; i++) {
                            if (ss[i].getStyle() == android.graphics.Typeface.BOLD){
                                str.removeSpan(ss[i]);
                                exists = true;
                            }
                        }

                        if (!exists){
                            str.setSpan(new StyleSpan(android.graphics.Typeface.BOLD), selectionStart, selectionEnd, Spannable.SPAN_EXCLUSIVE_EXCLUSIVE);
                        }

                        boldButton.setChecked(false);
                    }
                }
        });

There may be a better solution, happy to hear it if you have one!

roundhill
  • 700
  • 5
  • 17
5

You could just update the style after every character that they type using a TextWatcher and the addTextChangedListener() method.

Ok, this is just the bare bones example code.

int mStart = -1;

// Bold onClickListener
public void onClick(View view)
{
    if(mStart == -1) mStart = mEditText.getText().length();
    else mStart = -1;
}

// TextWatcher
onTextChanged(CharSequence s, int start, int before, int count)
{
    if(mStart > 0)
    {
        int end = mEditText.getText().length();
        mEditText.getText().setSpan(new StyleSpan(android.graphics.Typeface.BOLD), mStart, end - mStart, Spannable.SPAN_EXCLUSIVE_EXCLUSIVE);
    }
}
CaseyB
  • 24,780
  • 14
  • 77
  • 112
  • That sort of worked, but I need to get the HTML from the EditText using the Html.toHtml method, and that causes an html tag to be added around every character typed. – roundhill Jun 22 '10 at 22:15
  • I'm not saying to format each character, I'm saying to update the formatting each character. I'll update my answer with an example. – CaseyB Jun 22 '10 at 22:18
  • Thanks for the code, but it appears to do the same thing: adds a stylespan to every character typed. I can fix it by calling removeSpan as the user types, but there must be a better way than that. – roundhill Jun 22 '10 at 23:07
  • Thanks CaseyB, this helped get me to a solution. See below. – roundhill Jun 23 '10 at 16:44
2

I know this is an old thread, but my coworker spotted DroidWriter on the web. I've played with it a bit and it's pretty self explanatory and very easy to use and modify. Hope this helps anyone who's looking to create a rich text editor.

theshadowchild
  • 401
  • 4
  • 6
1

There is an open source EditText rich text editor called android-richtexteditor but the code was inexplicably deleted in r5. The code is still there in older revisions; just use svn to check out r4 or earlier to access it. The code appears to support italics, underline, color picker, text size, and more.

Also answered in these questions: 1, 2

Community
  • 1
  • 1
Jeff Axelrod
  • 27,676
  • 31
  • 147
  • 246
0

Just one possibility, not sure it's the best solution but its what comes to mind, and who knows but that it will inspire you to a more elegant solution:

Maybe you could consider internally tracking a hierarchical style structure of the "document", with all its style tags, and reconstructing / replacing the final output with each character typed? Of course you'll have to track cursor position too.

Brian Lacy
  • 18,785
  • 10
  • 55
  • 73
  • Thanks for this, it's got me off on the right track. However, this has some weaknesses. Namely, if you go back into the middle a previously styled text you don't split the span so you can insert new text with whatever the current style selected is. Also, I don't think you support multiple styles applied to the same text here or at least not in a way that allows multiple levels of selection. For instance I might start with bold AND italic and then just switch to italic and then go back to bold and italic. Then later I might change my mind and want to go back and change something in the middle. – jfelectron Jan 26 '11 at 21:17