The document discusses creating a rich text component for a Facebook clone mobile app. It describes parsing simple HTML markup to display formatted text, links, bold, and italics. Code is provided for a RichTextView class that extends Container and uses an XML parser to tokenize HTML and add labeled or button components to the container. The parser maintains state like current font and color and fires link click events.
Creating a Facebook Clone - Part V - Transcript.pdf
1. Creating a Facebook Clone - Part V
We need to make a decision on the UI design: Should we use the iOS design, the Android design or both?
Android & iOS differ greatly in the login process for Facebook. I'm assuming the reason for this is Apples review process that places restrictions on what Facebook can
do.
3. public class RichTextView extends Container {
private String text;
private final float fontSize = 2.6f;
private EventDispatcher listeners = new EventDispatcher();
private Font currentFont;
private int currentColor = 0;
private String currentLink;
private Style lastCmp;
private Font defaultFont;
private Font boldFont;
private Font italicFont;
private int sizeOfSpace;
public RichTextView() {
init();
}
public RichTextView(String text) {
init();
setText(text);
}
RichTextView
A very common request is support for rich text in Codename One, this is hard to do in a generic/performant cross platform way but can be done easily for simple tasks
like this.
Notice that for a lot of use cases I would just use BrowserComponent. Here that won't be ideal because I want deep integration with the UI and deep control.
Since we already have an XML parser that's perfectly capable of parsing HTML I decided to use a very simple HTML based syntax. I also added support for bold & italic
while demonstrating that simple things like line breaks still work.
Lets go over the code step by step.
This is a Container, as we parse we add elements into it
4. public class RichTextView extends Container {
private String text;
private final float fontSize = 2.6f;
private EventDispatcher listeners = new EventDispatcher();
private Font currentFont;
private int currentColor = 0;
private String currentLink;
private Style lastCmp;
private Font defaultFont;
private Font boldFont;
private Font italicFont;
private int sizeOfSpace;
public RichTextView() {
init();
}
public RichTextView(String text) {
init();
setText(text);
}
RichTextView
This is the HTML text we set, it's useful for the getText() method
5. public class RichTextView extends Container {
private String text;
private final float fontSize = 2.6f;
private EventDispatcher listeners = new EventDispatcher();
private Font currentFont;
private int currentColor = 0;
private String currentLink;
private Style lastCmp;
private Font defaultFont;
private Font boldFont;
private Font italicFont;
private int sizeOfSpace;
public RichTextView() {
init();
}
public RichTextView(String text) {
init();
setText(text);
}
RichTextView
Font size is hardcoded in millimeters for simplicity
6. public class RichTextView extends Container {
private String text;
private final float fontSize = 2.6f;
private EventDispatcher listeners = new EventDispatcher();
private Font currentFont;
private int currentColor = 0;
private String currentLink;
private Style lastCmp;
private Font defaultFont;
private Font boldFont;
private Font italicFont;
private int sizeOfSpace;
public RichTextView() {
init();
}
public RichTextView(String text) {
init();
setText(text);
}
RichTextView
The EventDispatcher lets us broadcast link click events as ActionEvent
7. public class RichTextView extends Container {
private String text;
private final float fontSize = 2.6f;
private EventDispatcher listeners = new EventDispatcher();
private Font currentFont;
private int currentColor = 0;
private String currentLink;
private Style lastCmp;
private Font defaultFont;
private Font boldFont;
private Font italicFont;
private int sizeOfSpace;
public RichTextView() {
init();
}
public RichTextView(String text) {
init();
setText(text);
}
RichTextView
The following variables are used by the parser to store temporary parser state as it builds the UI
8. public class RichTextView extends Container {
private String text;
private final float fontSize = 2.6f;
private EventDispatcher listeners = new EventDispatcher();
private Font currentFont;
private int currentColor = 0;
private String currentLink;
private Style lastCmp;
private Font defaultFont;
private Font boldFont;
private Font italicFont;
private int sizeOfSpace;
public RichTextView() {
init();
}
public RichTextView(String text) {
init();
setText(text);
}
RichTextView
These are the fonts and constants we use when the parser asks us to create a component
9. private Font defaultFont;
private Font boldFont;
private Font italicFont;
private int sizeOfSpace;
public RichTextView() {
init();
}
public RichTextView(String text) {
init();
setText(text);
}
private void init() {
defaultFont = Font.createTrueTypeFont(NATIVE_MAIN_LIGHT, fontSize);
boldFont = Font.createTrueTypeFont(NATIVE_MAIN_BOLD, fontSize);
italicFont = Font.createTrueTypeFont(NATIVE_ITALIC_LIGHT, fontSize);
sizeOfSpace = defaultFont.charWidth(' ');
currentFont = defaultFont;
}
public void setAlignment(int align) {
((FlowLayout)getLayout()).setAlign(align);
}
RichTextView
The constructors delegate to the init method which in turn initializes the default fonts.
10. setText(text);
}
private void init() {
defaultFont = Font.createTrueTypeFont(NATIVE_MAIN_LIGHT, fontSize);
boldFont = Font.createTrueTypeFont(NATIVE_MAIN_BOLD, fontSize);
italicFont = Font.createTrueTypeFont(NATIVE_ITALIC_LIGHT, fontSize);
sizeOfSpace = defaultFont.charWidth(' ');
currentFont = defaultFont;
}
public void setAlignment(int align) {
((FlowLayout)getLayout()).setAlign(align);
}
private void createComponent(String t) {
if(t.indexOf(' ') > -1) {
for(String s : StringUtil.tokenize(t, ' ')) {
createComponent(s);
}
return;
}
Label l;
if(currentLink != null) {
Button b = new Button(t, "Label");
RichTextView
Notice that the default layout is FlowLayout we rely on that to allow aligning the text to the center/right
11. setText(text);
}
private void init() {
defaultFont = Font.createTrueTypeFont(NATIVE_MAIN_LIGHT, fontSize);
boldFont = Font.createTrueTypeFont(NATIVE_MAIN_BOLD, fontSize);
italicFont = Font.createTrueTypeFont(NATIVE_ITALIC_LIGHT, fontSize);
sizeOfSpace = defaultFont.charWidth(' ');
currentFont = defaultFont;
}
public void setAlignment(int align) {
((FlowLayout)getLayout()).setAlign(align);
}
private void createComponent(String t) {
if(t.indexOf(' ') > -1) {
for(String s : StringUtil.tokenize(t, ' ')) {
createComponent(s);
}
return;
}
Label l;
if(currentLink != null) {
Button b = new Button(t, "Label");
RichTextView
This method is invoked by the parser to add a component
12. setText(text);
}
private void init() {
defaultFont = Font.createTrueTypeFont(NATIVE_MAIN_LIGHT, fontSize);
boldFont = Font.createTrueTypeFont(NATIVE_MAIN_BOLD, fontSize);
italicFont = Font.createTrueTypeFont(NATIVE_ITALIC_LIGHT, fontSize);
sizeOfSpace = defaultFont.charWidth(' ');
currentFont = defaultFont;
}
public void setAlignment(int align) {
((FlowLayout)getLayout()).setAlign(align);
}
private void createComponent(String t) {
if(t.indexOf(' ') > -1) {
for(String s : StringUtil.tokenize(t, ' ')) {
createComponent(s);
}
return;
}
Label l;
if(currentLink != null) {
Button b = new Button(t, "Label");
RichTextView
If the string has spaces we split it to multiple separate strings so each individual component can break a line in the flow layout
13. ((FlowLayout)getLayout()).setAlign(align);
}
private void createComponent(String t) {
if(t.indexOf(' ') > -1) {
for(String s : StringUtil.tokenize(t, ' ')) {
createComponent(s);
}
return;
}
Label l;
if(currentLink != null) {
Button b = new Button(t, "Label");
final String currentLinkValue = currentLink;
b.addActionListener(e -> listeners.fireActionEvent(
new ActionEvent(currentLinkValue)));
l = b;
} else {
l = new Label(t);
}
Style s = l.getAllStyles();
s.setFont(currentFont);
s.setFgColor(currentColor);
s.setPaddingUnit(Style.UNIT_TYPE_PIXELS);
s.setPadding(0, 0, 0, sizeOfSpace);
RichTextView
We add either a Label or a Button that looks like a Label
14. ((FlowLayout)getLayout()).setAlign(align);
}
private void createComponent(String t) {
if(t.indexOf(' ') > -1) {
for(String s : StringUtil.tokenize(t, ' ')) {
createComponent(s);
}
return;
}
Label l;
if(currentLink != null) {
Button b = new Button(t, "Label");
final String currentLinkValue = currentLink;
b.addActionListener(e -> listeners.fireActionEvent(
new ActionEvent(currentLinkValue)));
l = b;
} else {
l = new Label(t);
}
Style s = l.getAllStyles();
s.setFont(currentFont);
s.setFgColor(currentColor);
s.setPaddingUnit(Style.UNIT_TYPE_PIXELS);
s.setPadding(0, 0, 0, sizeOfSpace);
RichTextView
The parser updates currentLink so if we have a link we are within an <a> tag and need to use a button styled to look like a link
15. ((FlowLayout)getLayout()).setAlign(align);
}
private void createComponent(String t) {
if(t.indexOf(' ') > -1) {
for(String s : StringUtil.tokenize(t, ' ')) {
createComponent(s);
}
return;
}
Label l;
if(currentLink != null) {
Button b = new Button(t, "Label");
final String currentLinkValue = currentLink;
b.addActionListener(e -> listeners.fireActionEvent(
new ActionEvent(currentLinkValue)));
l = b;
} else {
l = new Label(t);
}
Style s = l.getAllStyles();
s.setFont(currentFont);
s.setFgColor(currentColor);
s.setPaddingUnit(Style.UNIT_TYPE_PIXELS);
s.setPadding(0, 0, 0, sizeOfSpace);
RichTextView
currentLinkValue is the content of href we just fire the listener event dispatcher with the href content as the source
16. ((FlowLayout)getLayout()).setAlign(align);
}
private void createComponent(String t) {
if(t.indexOf(' ') > -1) {
for(String s : StringUtil.tokenize(t, ' ')) {
createComponent(s);
}
return;
}
Label l;
if(currentLink != null) {
Button b = new Button(t, "Label");
final String currentLinkValue = currentLink;
b.addActionListener(e -> listeners.fireActionEvent(
new ActionEvent(currentLinkValue)));
l = b;
} else {
l = new Label(t);
}
Style s = l.getAllStyles();
s.setFont(currentFont);
s.setFgColor(currentColor);
s.setPaddingUnit(Style.UNIT_TYPE_PIXELS);
s.setPadding(0, 0, 0, sizeOfSpace);
RichTextView
If this isn't a link it's label
17. if(currentLink != null) {
Button b = new Button(t, "Label");
final String currentLinkValue = currentLink;
b.addActionListener(e -> listeners.fireActionEvent(
new ActionEvent(currentLinkValue)));
l = b;
} else {
l = new Label(t);
}
Style s = l.getAllStyles();
s.setFont(currentFont);
s.setFgColor(currentColor);
s.setPaddingUnit(Style.UNIT_TYPE_PIXELS);
s.setPadding(0, 0, 0, sizeOfSpace);
s.setMargin(0, 0, 0, 0);
lastCmp = s;
add(l);
}
public final void setText(String text) {
this.text = text;
removeAll();
try {
char[] chrs = ("<body>" + text + "</body>").toCharArray();
new Parser().eventParser(new CharArrayReader(chrs));
RichTextView
We remove the spaces, padding & margin. We then use the width of the a space to re-add a space in the form of padding. This allows line breaks on word boundaries
18. if(currentLink != null) {
Button b = new Button(t, "Label");
final String currentLinkValue = currentLink;
b.addActionListener(e -> listeners.fireActionEvent(
new ActionEvent(currentLinkValue)));
l = b;
} else {
l = new Label(t);
}
Style s = l.getAllStyles();
s.setFont(currentFont);
s.setFgColor(currentColor);
s.setPaddingUnit(Style.UNIT_TYPE_PIXELS);
s.setPadding(0, 0, 0, sizeOfSpace);
s.setMargin(0, 0, 0, 0);
lastCmp = s;
add(l);
}
public final void setText(String text) {
this.text = text;
removeAll();
try {
char[] chrs = ("<body>" + text + "</body>").toCharArray();
new Parser().eventParser(new CharArrayReader(chrs));
RichTextView
The parser might need to remove a space for some special cases e.g. if we close an </a> tag and place a dot right next to it
19. if(currentLink != null) {
Button b = new Button(t, "Label");
final String currentLinkValue = currentLink;
b.addActionListener(e -> listeners.fireActionEvent(
new ActionEvent(currentLinkValue)));
l = b;
} else {
l = new Label(t);
}
Style s = l.getAllStyles();
s.setFont(currentFont);
s.setFgColor(currentColor);
s.setPaddingUnit(Style.UNIT_TYPE_PIXELS);
s.setPadding(0, 0, 0, sizeOfSpace);
s.setMargin(0, 0, 0, 0);
lastCmp = s;
add(l);
}
public final void setText(String text) {
this.text = text;
removeAll();
try {
char[] chrs = ("<body>" + text + "</body>").toCharArray();
new Parser().eventParser(new CharArrayReader(chrs));
RichTextView
The setText method is the API that sets the HTML to this component
20. l = new Label(t);
}
Style s = l.getAllStyles();
s.setFont(currentFont);
s.setFgColor(currentColor);
s.setPaddingUnit(Style.UNIT_TYPE_PIXELS);
s.setPadding(0, 0, 0, sizeOfSpace);
s.setMargin(0, 0, 0, 0);
lastCmp = s;
add(l);
}
public final void setText(String text) {
this.text = text;
removeAll();
try {
char[] chrs = ("<body>" + text + "</body>").toCharArray();
new Parser().eventParser(new CharArrayReader(chrs));
} catch(IOException err) {
log(err);
}
}
public String getText() {
return text;
}
RichTextView
We wrap the HTML so it's well formed and parse the text using the Parser inner class
21. this.text = text;
removeAll();
try {
char[] chrs = ("<body>" + text + "</body>").toCharArray();
new Parser().eventParser(new CharArrayReader(chrs));
} catch(IOException err) {
log(err);
}
}
public String getText() {
return text;
}
public void addLinkListener(ActionListener al) {
listeners.addListener(al);
}
public void removeLinkListener(ActionListener al) {
listeners.removeListener(al);
}
class Parser extends XMLParser {
@Override
protected void textElement(String text) {
RichTextView
These add listeners so when a user clicks on a link these listeners will receive the event from the event dispatcher
22. listeners.addListener(al);
}
public void removeLinkListener(ActionListener al) {
listeners.removeListener(al);
}
class Parser extends XMLParser {
@Override
protected void textElement(String text) {
if(text.length() > 0) {
if(lastCmp != null && text.startsWith(" ")) {
lastCmp.setPadding(0, 0, 0, sizeOfSpace);
}
createComponent(text);
if(!text.endsWith(" ")) {
lastCmp.setPadding(0, 0, 0, 0);
}
}
}
@Override
protected boolean startTag(String tag) {
switch(tag.toLowerCase()) {
case "a":
currentColor = 0x4267B2;
RichTextView
The next stage is the Parser inner class, it's a bit big . We derive XMLParser & override callback methods to tokenize the string
23. listeners.addListener(al);
}
public void removeLinkListener(ActionListener al) {
listeners.removeListener(al);
}
class Parser extends XMLParser {
@Override
protected void textElement(String text) {
if(text.length() > 0) {
if(lastCmp != null && text.startsWith(" ")) {
lastCmp.setPadding(0, 0, 0, sizeOfSpace);
}
createComponent(text);
if(!text.endsWith(" ")) {
lastCmp.setPadding(0, 0, 0, 0);
}
}
}
@Override
protected boolean startTag(String tag) {
switch(tag.toLowerCase()) {
case "a":
currentColor = 0x4267B2;
RichTextView
All the methods here are callbacks in a SAX style parser. We also support a DOM style but I chose a callback approach for performance
24. listeners.addListener(al);
}
public void removeLinkListener(ActionListener al) {
listeners.removeListener(al);
}
class Parser extends XMLParser {
@Override
protected void textElement(String text) {
if(text.length() > 0) {
if(lastCmp != null && text.startsWith(" ")) {
lastCmp.setPadding(0, 0, 0, sizeOfSpace);
}
createComponent(text);
if(!text.endsWith(" ")) {
lastCmp.setPadding(0, 0, 0, 0);
}
}
}
@Override
protected boolean startTag(String tag) {
switch(tag.toLowerCase()) {
case "a":
currentColor = 0x4267B2;
RichTextView
If the block element starts with a space we add a space to the last block element
25. listeners.addListener(al);
}
public void removeLinkListener(ActionListener al) {
listeners.removeListener(al);
}
class Parser extends XMLParser {
@Override
protected void textElement(String text) {
if(text.length() > 0) {
if(lastCmp != null && text.startsWith(" ")) {
lastCmp.setPadding(0, 0, 0, sizeOfSpace);
}
createComponent(text);
if(!text.endsWith(" ")) {
lastCmp.setPadding(0, 0, 0, 0);
}
}
}
@Override
protected boolean startTag(String tag) {
switch(tag.toLowerCase()) {
case "a":
currentColor = 0x4267B2;
RichTextView
We remove the space from the previous component if the block doesn't end with a space
26. }
createComponent(text);
if(!text.endsWith(" ")) {
lastCmp.setPadding(0, 0, 0, 0);
}
}
}
@Override
protected boolean startTag(String tag) {
switch(tag.toLowerCase()) {
case "a":
currentColor = 0x4267B2;
break;
case "b":
currentFont = boldFont;
break;
case "i":
currentFont = italicFont;
break;
}
return true;
}
@Override
protected void endTag(String tag) {
RichTextView
Here we handle the individual HTML tags we want to support by changing the current font or color
27. currentColor = 0x4267B2;
break;
case "b":
currentFont = boldFont;
break;
case "i":
currentFont = italicFont;
break;
}
return true;
}
@Override
protected void endTag(String tag) {
currentColor = 0;
currentLink = null;
currentFont = defaultFont;
}
@Override
protected void attribute(
String tag, String attributeName, String value) {
if(tag.toLowerCase().equals("a") &&
attributeName.toLowerCase().equals("href")) {
currentLink = value;
}
RichTextView
When a tag ends we clear the current font/color and restore the default, we don't handle complexity like tag nesting etc.
28. }
@Override
protected void endTag(String tag) {
currentColor = 0;
currentLink = null;
currentFont = defaultFont;
}
@Override
protected void attribute(
String tag, String attributeName, String value) {
if(tag.toLowerCase().equals("a") &&
attributeName.toLowerCase().equals("href")) {
currentLink = value;
}
}
@Override
protected void notifyError(int errorId, String tag,
String attribute, String value, String description) {
log("Error during parsing: " + tag);
}
}
}
RichTextView
When we're in an <a> tag we save the value of the href attribute for use in the event handler code. That's it, we now have a poor mans HTML renderer that we can use
for simple blocks of highlighted text. We'll make use of it soon...