preface
Android’s rich text is still the same set of HTML tags that you parse the HTML, get different tags, and render them in different styles. However, it is very different from that on the Web. For example, Android supports very few tags by default, and its functions are relatively simple. Most cases need to be defined by themselves. Moreover, the client does not need to open a page like the Web when opening a link. For pictures and videos, it may need to open a local page. At this time, it also needs to define the label and corresponding span. For those who are not familiar with SPAN, see the previous article:
Android TextView Android of rich text. Text. Style.css. XxxSpan Android TextView rich text of ImageSpan Android TextView ClickableSpan rich text
Html parsing
Android.text.html is a tool for parsing Html. It is easy to use, just call the static method public static Spanned fromHtml(String source). Here’s an overview of the parsing process and some areas of concern.
1, HtmlToSpannedConverter
public static Spanned fromHtml(String source, int flags, ImageGetter imageGetter, TagHandler tagHandler) {
Parser parser = new Parser();
// ...
parser.setProperty(Parser.schemaProperty, HtmlParser.schema);
// ...
HtmlToSpannedConverter converter = new HtmlToSpannedConverter(source, imageGetter, tagHandler, parser, flags);
return converter.convert();
}
Copy the code
FromHtml first created a Parser named htmlParser. schema to parse HTML, and then HtmlToSpannedConverter to convert HTML tags to SPAN. HtmlToSpannedConverter is the focus of our attention. Here’s a quick look at how it’s constructed:
public HtmlToSpannedConverter( String source, Html.ImageGetter imageGetter, Html.TagHandler tagHandler, Parser parser, int flags) {
// Enter the text
mSource = source;
// Span text to output
mSpannableStringBuilder = new SpannableStringBuilder();
// A custom tool for getting images
mImageGetter = imageGetter;
// Custom tag handlers, the focus of this article
mTagHandler = tagHandler;
// Parser is an XMLReader
mReader = parser;
mFlags = flags;
}
Copy the code
2, HtmlToSpannedConverter. Convert
public Spanned convert(a) {
mReader.setContentHandler(this);
// ...
mReader.parse(new InputSource(new StringReader(mSource)));
// ...
return mSpannableStringBuilder;
}
Copy the code
We call the mReader parse to parse the mSource we just passed in. During the parse process, we call the ContentHandler to handle the start and end of various tags. The result is returned.
3, ContentHandler
Here’s the ContentHandler definition:
public interface ContentHandler {
// ...
// Process the start of the tag
public void startElement (String uri, String localName, String qName, Attributes atts) throws SAXException; \// Handle the end of the tag
public void endElement (String uri, String localName, String qName) throws SAXException;
// Ch is the character we want to process, and start and length are the positions of the characters we want to process
public void characters (char ch[], int start, int length) throws SAXException;
// ...
}
Copy the code
We only care about these three methods, so let’s see how HtmlToSpannedConverter implements these three methods. First, characters will send us the string to be processed in a quick way. Here we only do a simple processing, and the annotation is very clear, that is, filter out successive whitespace, and process the newline according to whitespace. Because HTML has the corresponding newline tag
, this will filter out the newline, but in the case of ordinary text with part of the HTML tag, the newline filter will display the problem, we can replace the newline with < BR > before parsing.
public void characters(char ch[], int start, int length) throws SAXException {
StringBuilder sb = new StringBuilder();
/* * Ignore whitespace that immediately follows other whitespace; * newlines count as spaces. */
for (int i = 0; i < length; i++) {
char c = ch[i + start];
if (c == ' ' || c == '\n') {
char pred;
int len = sb.length();
if (len == 0) {
len = mSpannableStringBuilder.length();
if (len == 0) {
pred = '\n';
} else {
pred = mSpannableStringBuilder.charAt(len - 1); }}else {
pred = sb.charAt(len - 1);
}
if(pred ! =' '&& pred ! ='\n') {
sb.append(' '); }}else {
sb.append(c);
}
}
mSpannableStringBuilder.append(sb);
}
Copy the code
StartElement calls handleStartTag and handles the beginning of the tag, where we can also see tags supported by the Html tool by default.
private void handleStartTag(String tag, Attributes attributes) {
// ...
else if (tag.equalsIgnoreCase("a")) {
startA(mSpannableStringBuilder, attributes);
}
// ...
else if (tag.equalsIgnoreCase("img")) {
startImg(mSpannableStringBuilder, attributes, mImageGetter);
}
// ...
else if(mTagHandler ! =null) {
mTagHandler.handleTag(true, tag, mSpannableStringBuilder, mReader); }}Copy the code
EndElement also corresponds to a call to handleEndTag to handle the end of the tag. One thing to note here is that our custom mTagHandler is placed at the end of the if-else list, which means that we can’t handle tags that are already supported by the Html tools, and it’s not practical to redefine a set of tag rules with the back end to ensure compatibility. In this case, we need to first replace these tags with Html tools do not support, we can define our own, and then parse the replaced tags.
4. Label processing
Let’s take a quick example of what we’re doing with the A tag.
private static void startA(Editable text, Attributes attributes) {
String href = attributes.getValue(""."href");
start(text, new Href(href));
}
private static void start(Editable text, Object mark) {
int len = text.length();
text.setSpan(mark, len, len, Spannable.SPAN_INCLUSIVE_EXCLUSIVE);
}
private static void endA(Editable text) {
Href h = getLast(text, Href.class);
if(h ! =null) {
if(h.mHref ! =null) {
setSpanFromMark(text, h, newURLSpan((h.mHref))); }}}Copy the code
The a tag takes the href attribute at the beginning and stores it in the href. The href is set as a span to the text and is saved. At this time, we can only know the starting position of the tag. And then at the end of the page we can figure out where the tag ends, take the Href out, reset the URLSpan, and that span will open a link when you click on it, by default, in the browser. The
tag is simply a text followed by ‘\n’.
private static void handleBr(Editable text) {
text.append('\n');
}
Copy the code
Other tag processing is similar and will not be described here.
The custom Html. TagHandler
In daily work, tag A may be the most used. We may need to handle clicking and jumping on pictures, videos and pages by ourselves. Here we take tag A as an example to illustrate. As mentioned earlier, the Html tool supports the A tag, and our custom mTagHandler has a low priority, so we need to sit down and simple preprocess, replace the A tag with the one that the Html tool does not know.
private static final String A_PATTERN = "<\s*[aA] (.*?) > (. *?) <\s*/[aA]\s*>";
private static final String SELF_DEF_A_TAG = "zdl_a";
private static final String A_REPLACE = "<" + SELF_DEF_A_TAG +"$1 > $2 < /" + SELF_DEF_A_TAG + ">";
public String preProcess(String text) {
return text.replaceAll(A_PATTERN, A_REPLACE);
}
Copy the code
After the preprocessing is complete, we can happily parse the tag. We can follow the way of Html tools, read the attribute at the beginning, save it as a span, take it out at the end, and finally generate the corresponding style of span according to the attribute. Public void handleTag(Boolean opening, String tag, Editable Output, XMLReader XMLReader) How to Get an attribute from an XMLReader We define a span for storing attributes:
class AttributeSpan {
public HashMap<String, String> attrs = new HashMap<>();
public int start;
public int end;
}
Copy the code
Read and save all attributes at the beginning of the tag, and read and use them at the end.
public void handleTag(boolean opening, String tag, Editable output, XMLReader xmlReader) {
if(! SELF_DEF_A_TAG.equalsIgnoreCase(tag)) {return; }
if (opening) {
AttributeSpan span = new AttributeSpan();
span.start = output.length();
// Save the attributes in the HashMap
TagHandler.getAttribute(span.attrs, xmlReader);
output.setSpan(span, span.start, span.start, Spanned.SPAN_INCLUSIVE_EXCLUSIVE);
} else {
AttributeSpan span = TagHandler.getLast(output, AttributeSpan.class);
if(span ! =null) {
span.end = output.length();
// Since images, videos, and pages are all in the form of a tags, we also need to use the class attribute to distinguish them
// Then add different spans according to different types. For example, we can define a ToastSpan to show all properties by popping toast when clicked
// By default, we use URLSpan. For special processing of images, videos, and pages, we only need to add the class attribute and the corresponding span
String clazz = span.attrs.get("class");
if (clazz == null) { clazz = ""; }
Object realSpan;
switch (clazz) {
case ToastSpan.CLASS:
realSpan = new ToastSpan(span);
break;
default:
realSpan = new URLSpan(span.attrs.get("href"));
break; } output.setSpan(realSpan, span.start, span.end, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE); }}}public static class ToastSpan extends ClickableSpan {
public static final String CLASS = "toast";
private final AttributeSpan attributeSpan;
public ToastSpan(AttributeSpan attributeSpan) {
this.attributeSpan = attributeSpan;
}
@Override
public void onClick(@NonNull View widget) {
StringBuilder builder = new StringBuilder();
for (String key: attributeSpan.attrs.keySet()) {
if (builder.length() > 0) {
builder.append(",");
}
builder.append(key);
builder.append(":"); builder.append(attributeSpan.attrs.get(key)); } Toast.makeText(widget.getContext(), builder, Toast.LENGTH_SHORT).show(); }}Copy the code
conclusion
The rich text of Android TextView is relatively simple, and the corresponding HTML parsing is not very complicated. In fact, it is a conversion process from label to SPAN. Here, the demo address RichTextDemo is attached, and you can refer to it if you are interested.