Skip to content

Instantly share code, notes, and snippets.

@vincent1086
Last active May 6, 2022 13:35
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 1 You must be signed in to fork a gist
  • Save vincent1086/1ca4df897efcf9bc0a4bda771af5636a to your computer and use it in GitHub Desktop.
Save vincent1086/1ca4df897efcf9bc0a4bda771af5636a to your computer and use it in GitHub Desktop.
Android - TextView Support (ul/ol/li)
import java.util.Vector;
import org.xml.sax.XMLReader;
import android.text.Editable;
import android.text.Html;
import android.text.Spannable;
import android.text.style.BulletSpan;
import android.text.style.LeadingMarginSpan;
import android.text.style.TypefaceSpan;
import android.util.Log;
/*
* HTML.formHtml() Source Code :
* http://www.netmite.com/android/mydroid/frameworks/base/core/java/android/text/Html.java
* http://stackoverflow.com/questions/3150400/html-list-tag-not-working-in-android-textview-what-can-i-do/16169511#16169511
* http://mohammedlakkadshaw.com/blog/Handling_Custom_Tags_Using_Html.tagHandler().html#.V9FpmRB94Y3
*/
public class HtmlTagHandler implements Html.TagHandler {
private int mListItemCount = 0;
private Vector<String> mListParents = new Vector<String>();
@Override
public void handleTag(final boolean opening, final String tag, Editable output, final XMLReader xmlReader) {
if (tag.equals("ul") || tag.equals("ol") || tag.equals("dd")) {
if (opening) {
mListParents.add(tag);
} else mListParents.remove(tag);
mListItemCount = 0;
} else if (tag.equals("li") && !opening) {
handleListTag(output);
}
else if(tag.equalsIgnoreCase("code")) {
if(opening) {
output.setSpan(new TypefaceSpan("monospace"), output.length(), output.length(), Spannable.SPAN_MARK_MARK);
} else {
Log.d("COde Tag","Code tag encountered");
Object obj = getLast(output, TypefaceSpan.class);
int where = output.getSpanStart(obj);
output.setSpan(new TypefaceSpan("monospace"), where, output.length(), 0);
}
}
}
private Object getLast(Editable text, Class kind) {
Object[] objs = text.getSpans(0, text.length(), kind);
if(objs.length == 0) {
return null;
} else {
for (int i=objs.length; i > 0; i--) {
if(text.getSpanFlags(objs[i-1]) == Spannable.SPAN_MARK_MARK) {
return objs[i-1];
}
}
return null;
}
}
private void handleListTag(Editable output) {
if (mListParents.lastElement().equals("ul")) {
output.append("\n");
String[] split = output.toString().split("\n");
int lastIndex = split.length - 1;
int start = output.length() - split[lastIndex].length() - 1;
output.setSpan(new BulletSpan(15 * mListParents.size()), start, output.length(), 0);
} else if (mListParents.lastElement().equals("ol")) {
mListItemCount++;
output.append("\n");
String[] split = output.toString().split("\n");
int lastIndex = split.length - 1;
int start = output.length() - split[lastIndex].length() - 1;
output.insert(start, mListItemCount + ". ");
output.setSpan(new LeadingMarginSpan.Standard(15 * mListParents.size()), start, output.length(), 0);
}
}
}
//////////////////////////////////////////////////////////////////////////////////////////////////////////////////
//////////////////////////////////////////////////////////////////////////////////////////////////////////////////
/*
* https://bitbucket.org/Kuitsi/android-textview-html-list/src/69949bd63422aeeefb79c67398dc14aab8806416/app/src/main/java/fi/iki/kuitsi/listtest/MyTagHandler.java?at=master&fileviewer=file-view-default#cl-86
*/
package fi.iki.kuitsi.listtest;
import java.util.Stack;
import org.xml.sax.XMLReader;
import android.text.Editable;
import android.text.Html;
import android.text.Spanned;
import android.text.style.BulletSpan;
import android.text.style.LeadingMarginSpan;
import android.util.Log;
/**
* Implements support for ordered and unordered lists in to Android TextView.
*
* Some code taken from inner class android.text.Html.HtmlToSpannedConverter. If you find this code useful,
* please vote my answer at <a href="http://stackoverflow.com/a/17365740/262462">StackOverflow</a> up.
*/
public class MyTagHandler implements Html.TagHandler {
/**
* Keeps track of lists (ol, ul). On bottom of Stack is the outermost list
* and on top of Stack is the most nested list
*/
Stack<String> lists = new Stack<String>();
/**
* Tracks indexes of ordered lists so that after a nested list ends
* we can continue with correct index of outer list
*/
Stack<Integer> olNextIndex = new Stack<Integer>();
/**
* List indentation in pixels. Nested lists use multiple of this.
*/
private static final int indent = 10;
private static final int listItemIndent = indent * 2;
private static final BulletSpan bullet = new BulletSpan(indent);
@Override
public void handleTag(boolean opening, String tag, Editable output, XMLReader xmlReader) {
if (tag.equalsIgnoreCase("ul")) {
if (opening) {
lists.push(tag);
} else {
lists.pop();
}
} else if (tag.equalsIgnoreCase("ol")) {
if (opening) {
lists.push(tag);
olNextIndex.push(Integer.valueOf(1)).toString();//TODO: add support for lists starting other index than 1
} else {
lists.pop();
olNextIndex.pop().toString();
}
} else if (tag.equalsIgnoreCase("li")) {
if (opening) {
if (output.length() > 0 && output.charAt(output.length() - 1) != '\n') {
output.append("\n");
}
String parentList = lists.peek();
if (parentList.equalsIgnoreCase("ol")) {
start(output, new Ol());
output.append(olNextIndex.peek().toString() + ". ");
olNextIndex.push(Integer.valueOf(olNextIndex.pop().intValue() + 1));
} else if (parentList.equalsIgnoreCase("ul")) {
start(output, new Ul());
}
} else {
if (lists.peek().equalsIgnoreCase("ul")) {
if ( output.charAt(output.length() - 1) != '\n' ) {
output.append("\n");
}
// Nested BulletSpans increases distance between bullet and text, so we must prevent it.
int bulletMargin = indent;
if (lists.size() > 1) {
bulletMargin = indent-bullet.getLeadingMargin(true);
if (lists.size() > 2) {
// This get's more complicated when we add a LeadingMarginSpan into the same line:
// we have also counter it's effect to BulletSpan
bulletMargin -= (lists.size() - 2) * listItemIndent;
}
}
BulletSpan newBullet = new BulletSpan(bulletMargin);
end(output,
Ul.class,
new LeadingMarginSpan.Standard(listItemIndent * (lists.size() - 1)),
newBullet);
} else if (lists.peek().equalsIgnoreCase("ol")) {
if ( output.charAt(output.length() - 1) != '\n' ) {
output.append("\n");
}
int numberMargin = listItemIndent * (lists.size() - 1);
if (lists.size() > 2) {
// Same as in ordered lists: counter the effect of nested Spans
numberMargin -= (lists.size() - 2) * listItemIndent;
}
end(output,
Ol.class,
new LeadingMarginSpan.Standard(numberMargin));
}
}
} else {
if (opening) Log.d("TagHandler", "Found an unsupported tag " + tag);
}
}
/** @see android.text.Html */
private static void start(Editable text, Object mark) {
int len = text.length();
text.setSpan(mark, len, len, Spanned.SPAN_MARK_MARK);
}
/** Modified from {@link android.text.Html} */
private static void end(Editable text, Class<?> kind, Object... replaces) {
int len = text.length();
Object obj = getLast(text, kind);
int where = text.getSpanStart(obj);
text.removeSpan(obj);
if (where != len) {
for (Object replace: replaces) {
text.setSpan(replace, where, len, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
}
}
return;
}
/** @see android.text.Html */
private static Object getLast(Spanned text, Class<?> kind) {
/*
* This knows that the last returned object from getSpans()
* will be the most recently added.
*/
Object[] objs = text.getSpans(0, text.length(), kind);
if (objs.length == 0) {
return null;
}
return objs[objs.length - 1];
}
private static class Ul { }
private static class Ol { }
}
@vishalhalani
Copy link

not working for me. i have to show orderlist with number

@AKILLIMUSTAFA
Copy link

Tag parameter always be "html" or "body" for handleTag method. Not be "li" or "lu".

@patjackson52
Copy link

Android got support for ul and li tags in Android N. They are supported tags, so they are no longer sent to the TagHandler.

@azrinsani
Copy link

Hi How do you indent/add margin to a list?

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment