Android:如何使用Html.TagHandler?

我正在试图build立一个留言板的Android应用程序。 为了显示发布内容的格式化的html,我select了TextView和Html.fromHtml()方法。 不幸的是,这只包含了一些html标签。 未知标签由实现TagHandler的类处理,并且必须由我自己生成。

现在,我search了很多东西,找不到这个类应该如何工作的例子。 让我们考虑我有一个u标签强调一些文字(我知道这是废弃,但无论)。 我的TagHandler是怎样的?

它被称为以下方式:

public void handleTag(boolean opening, String tag, Editable output, XMLReader xmlReader) { 

前两个参数很好。 我想我必须使用output.append()修改输出。 但是我如何在这里加上下划线?

所以,我终于明白了自己。

 public class MyHtmlTagHandler implements TagHandler { public void handleTag(boolean opening, String tag, Editable output, XMLReader xmlReader) { if(tag.equalsIgnoreCase("strike") || tag.equals("s")) { processStrike(opening, output); } } private void processStrike(boolean opening, Editable output) { int len = output.length(); if(opening) { output.setSpan(new StrikethroughSpan(), len, len, Spannable.SPAN_MARK_MARK); } else { Object obj = getLast(output, StrikethroughSpan.class); int where = output.getSpanStart(obj); output.removeSpan(obj); if (where != len) { output.setSpan(new StrikethroughSpan(), where, len, Spannable.SPAN_EXCLUSIVE_EXCLUSIVE); } } } 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; } } } 

如果有人需要它。

干杯

这个解决scheme可以在Android SDK中find

android.text.html 。 596 – 626行。复制/粘贴

 private static <T> Object getLast(Spanned text, Class<T> 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; } else { return objs[objs.length - 1]; } } private static void start(SpannableStringBuilder text, Object mark) { int len = text.length(); text.setSpan(mark, len, len, Spannable.SPAN_MARK_MARK); } private static <T> void end(SpannableStringBuilder text, Class<T> kind, Object repl) { int len = text.length(); Object obj = getLast(text, kind); int where = text.getSpanStart(obj); text.removeSpan(obj); if (where != len) { text.setSpan(repl, where, len, Spannable.SPAN_EXCLUSIVE_EXCLUSIVE); } } 

要使用,像这样覆盖TagHandler:

 public void handleTag(boolean opening, String tag, Editable output, XMLReader xmlReader) { if(tag.equalsIgnoreCase("strike") || tag.equals("s")) { if(opening){ start((SpannableStringBuilder) output, new Strike(); } else { end((SpannableStringBuilder) output, Strike.class, new StrikethroughSpan()); } } } /* * Notice this class. It doesn't really do anything when it spans over the text. * The reason is we just need to distinguish what needs to be spanned, then on our closing * tag, we will apply the spannable. For each of your different spannables you implement, just * create a class here. */ private static class Strike{} 

我拿janoliver的答案,并提出了我的版本,试图支持更多的select

  String text = ""; // HTML text to convert // Preprocessing phase to set up for HTML.fromHtml(...) text = text.replaceAll("<span style=\"(?:color: (#[a-fA-F\\d]{6})?; )?(?:font-family: (.*?); )?(?:font-size: (.*?);)? ?\">(.*?)</span>", "<font color=\"$1\" face=\"$2\" size=\"$3\">$4</font>"); text = text.replaceAll("(?<=<font color=\"#[a-fA-F0-9]{6}\" )face=\"'(.*?)', .*?\"", "face=\"$1\""); text = text.replaceAll("(?<=<font color=\"#[a-fA-F0-9]{6}\" )(face=\".*?\" )size=\"xx-small\"", "$1size=\"1\""); text = text.replaceAll("(?<=<font color=\"#[a-fA-F0-9]{6}\" )(face=\".*?\" )size=\"x-small\"", "$1size=\"2\""); text = text.replaceAll("(?<=<font color=\"#[a-fA-F0-9]{6}\" )(face=\".*?\" )size=\"small\"", "$1size=\"3\""); text = text.replaceAll("(?<=<font color=\"#[a-fA-F0-9]{6}\" )(face=\".*?\" )size=\"medium\"", "$1size=\"4\""); text = text.replaceAll("(?<=<font color=\"#[a-fA-F0-9]{6}\" )(face=\".*?\" )size=\"large\"", "$1size=\"5\""); text = text.replaceAll("(?<=<font color=\"#[a-fA-F0-9]{6}\" )(face=\".*?\" )size=\"x-large\"", "$1size=\"6\""); text = text.replaceAll("(?<=<font color=\"#[a-fA-F0-9]{6}\" )(face=\".*?\" )size=\"xx-large\"", "$1size=\"7\""); text = text.replaceAll("<strong>(.*?)</strong>", "<_em>$1</_em>"); // we use strong for bold-face text = text.replaceAll("<em>(.*?)</em>", "<strong>$1</strong>"); // and em for italics text = text.replaceAll("<_em>(.*?)</_em>", "<em>$1</em>"); // but Android uses em for bold-face text = text.replaceAll("<span style=\"background-color: #([a-fA-F0-9]{6}).*?>(.*?)</span>", "<_$1>$2</_$1>"); text_view.setText(Html.fromHtml(text, null, new Html.TagHandler() { private List<Object> _format_stack = new LinkedList<Object>(); @Override public void handleTag(boolean open_tag, String tag, Editable output, XMLReader _) { if (tag.startsWith("ul")) processBullet(open_tag, output); else if (tag.matches(".[a-fA-F0-9]{6}")) processBackgroundColor(open_tag, output, tag.substring(1)); } private void processBullet(boolean open_tag, Editable output) { final int length = output.length(); if (open_tag) { final Object format = new BulletSpan(BulletSpan.STANDARD_GAP_WIDTH); _format_stack.add(format); output.setSpan(format, length, length, Spanned.SPAN_MARK_MARK); } else { applySpan(output, length, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE); } } private void processBackgroundColor(boolean open_tag, Editable output, String color) { final int length = output.length(); if (open_tag) { final Object format = new BackgroundColorSpan(Color.parseColor('#' + color)); _format_stack.add(format); output.setSpan(format, length, length, Spanned.SPAN_MARK_MARK); } else { applySpan(output, length, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE); } } private Object getLast(Editable text, Class kind) { @SuppressWarnings("unchecked") final Object[] spans = text.getSpans(0, text.length(), kind); if (spans.length != 0) for (int i = spans.length; i > 0; i--) if (text.getSpanFlags(spans[i-1]) == Spannable.SPAN_MARK_MARK) return spans[i-1]; return null; } private void applySpan(Editable output, int length, int flags) { if (_format_stack.isEmpty()) return; final Object format = _format_stack.remove(0); final Object span = getLast(output, format.getClass()); final int where = output.getSpanStart(span); output.removeSpan(span); if (where != length) output.setSpan(format, where, length, flags); } })); 

这似乎得到子弹,前景色和背景色。 它可能适用于字体,但你可能需要提供的字体,因为它似乎并不支持除Droid / Roboto之外的字体。

这更多的是一个概念certificate,你可能想要将正则expression式转换为String处理,因为正则expression式不支持以任何方式组合预处理,这意味着这需要大量的String传递。 这也似乎没有得到的字体大小改变,我试图定义它像“16sp”,“中”,或“4”没有看到变化。 如果任何人都有大小工作,分享思想?

我目前希望能够添加编号/有序的列表支持,即

  1. 项目
  2. 项目
  3. 项目

注意:对于任何这些开始的人来说,给予handleTag(...)的“标签”似乎就是标签的名称(如“span”),并且不包含任何属性(如果你有“),你可以看到我的背景颜色的漏洞。

虽然我可以在Html.java API中看到它的样式和文本alignment应该可以与标签<p><div>等一起使用。我无法使它与<p align="center">或还有很多其他的变种。 不能做这个中心alignment的文本,和其他样式,如字体大小,从我的ttf文件多个字体的面孔,背景颜色,我做了我自己的基于TextView的htmlTextView,但我自己的tagHandler类。 给一两个小小的刺激,大多数标签都很好,但是我的自定义alignment标签, 只在特殊条件下工作(我不明白),否则。 他们不工作或崩溃的应用程序! 这是我的alignment标记句柄。 它与所有其他自定义标签处理程序具有相同的结构,但真的performance怪异! 我的标签处理程序的基本forms是相同的, 不是由我设想的 ! 在网上search了很多小时之后,我发现了taghandler模​​板。 我很感激那些发布它的人,但是我的记忆和组织能力使我无法真正记住谁或在哪里,所以如果你认识到这个代码是你的,请告诉我。 唯一的链接(这里是)我有我的代码: stackoverflow :Android的:如何使用Html.TagHandler?

  private void ProcessAlignment(Layout.Alignment align, boolean opening, Editable output) { int len = output.length(); if (opening) { output.setSpan(new AlignmentSpan.Standard(align), len, len, Spannable.SPAN_MARK_MARK); } else { Object obj = getLast(output, AlignmentSpan.Standard.class); int where = output.getSpanStart(obj); output.removeSpan(obj); if (where != len) { output.setSpan(new AlignmentSpan.Standard(align), where, len, Spannable.SPAN_EXCLUSIVE_EXCLUSIVE); } } } 

我认为问题是,结束标签没有连接正确的开始标签。

 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 - 1; i >= 0; --i) { if (text.getSpanFlags(objs[i]) == Spannable.SPAN_MARK_MARK) { return objs[i]; } } return null; } } 

这是总课,有些是不对的。 最大的组成部分是我的理解! 也许有人可以帮助我更好地理解…

 public class htmlTextView extends AppCompatTextView { static Typeface mLogo; static Typeface mGAMZ; static Typeface mChalk; static Typeface mSouvenir; int GS_PAINTFLAGS = FILTER_BITMAP_FLAG | ANTI_ALIAS_FLAG | SUBPIXEL_TEXT_FLAG | HINTING_ON; public htmlTextView(Context context) { super(context); initialise(); } public htmlTextView(Context context, @Nullable AttributeSet attrs) { super(context, attrs); initialise(); } public htmlTextView(Context context, @Nullable AttributeSet attrs, int defStyleAttr) { super(context, attrs, defStyleAttr); initialise(); } private void initialise() { mLogo = Typeface.createFromAsset(theAssetManager, "fonts/logo.ttf"); mGAMZ = Typeface.createFromAsset(theAssetManager, "fonts/GAMZ One.ttf"); mChalk = Typeface.createFromAsset(theAssetManager, "fonts/swapfix.ttf"); mSouvenir = Typeface.createFromAsset(theAssetManager, "fonts/Souvenir Regular.ttf"); setPaintFlags(GS_PAINTFLAGS); } public void setDefaultTypefaceSouvenir() { setTypeface(mSouvenir); } public void setDefaultTypefaceGAMZ() { setTypeface(mGAMZ); } public void setDefaultTypefaceChalk() { setTypeface(mChalk); } /*public myTextView(Context context, @Nullable AttributeSet attrs, int defStyleAttr, int defStyleRes) { super(context, attrs, defStyleAttr, defStyleRes); }*/ public void setHTML(String htmltext) { if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) { // Nougat API 24 setText(Html.fromHtml(htmltext, Html.FROM_HTML_MODE_LEGACY, null, new TypefaceTagHandler())); } else { setText(Html.fromHtml(htmltext, null, new TypefaceTagHandler())); } } @Override protected void dispatchDraw(Canvas canvas) { super.dispatchDraw(canvas); } @Override protected void onDraw(Canvas canvas) { super.onDraw(canvas); } @Override public Bitmap getDrawingCache(boolean autoScale) { return super.getDrawingCache(autoScale); } @Override public void draw(Canvas canvas) { super.draw(canvas); } // http://stackoverflow.com/questions/4044509/android-how-to-use-the-html-taghandler private static class TypefaceTagHandler implements Html.TagHandler { private void ProcessAlignment(Layout.Alignment align, boolean opening, Editable output) { int len = output.length(); if (opening) { output.setSpan(new AlignmentSpan.Standard(align), len, len, Spannable.SPAN_MARK_MARK); } else { Object obj = getLast(output, AlignmentSpan.Standard.class); int where = output.getSpanStart(obj); output.removeSpan(obj); if (where != len) { output.setSpan(new AlignmentSpan.Standard(align), where, len, Spannable.SPAN_EXCLUSIVE_EXCLUSIVE); } } } private void ProcessTypefaceTag(Typeface tf, boolean opening, Editable output) { int len = output.length(); if (opening) { output.setSpan(new CustomTypefaceSpan("", tf), len, len, Spannable.SPAN_MARK_MARK); } else { Object obj = getLast(output, CustomTypefaceSpan.class); int where = output.getSpanStart(obj); output.removeSpan(obj); if (where != len) { output.setSpan(new CustomTypefaceSpan("", tf), where, len, Spannable.SPAN_EXCLUSIVE_EXCLUSIVE); } } } private void ProcessScaleTag(float scalefactor, boolean opening, Editable output) { int len = output.length(); if (opening) { output.setSpan(new RelativeSizeSpan(scalefactor), len, len, Spannable.SPAN_MARK_MARK); } else { Object obj = getLast(output, RelativeSizeSpan.class); int where = output.getSpanStart(obj); output.removeSpan(obj); if (where != len) { output.setSpan(new RelativeSizeSpan(scalefactor), where, len, Spannable.SPAN_EXCLUSIVE_EXCLUSIVE); } } } private void ProcessBox(int colour, boolean opening, Editable output) { int len = output.length(); if (opening) { output.setSpan(new BackgroundColorSpan(colour), len, len, Spannable.SPAN_MARK_MARK); } else { Object obj = getLast(output, BackgroundColorSpan.class); int where = output.getSpanStart(obj); output.removeSpan(obj); if (where != len) { output.setSpan(new BackgroundColorSpan(colour), where, len, Spannable.SPAN_EXCLUSIVE_EXCLUSIVE); } } } private void ProcessTextColour(int colour, boolean opening, Editable output) { int len = output.length(); if (opening) { output.setSpan(new ForegroundColorSpan(colour), len, len, Spannable.SPAN_MARK_MARK); } else { Object obj = getLast(output, ForegroundColorSpan.class); int where = output.getSpanStart(obj); output.removeSpan(obj); if (where != len) { output.setSpan(new ForegroundColorSpan(colour), where, len, Spannable.SPAN_EXCLUSIVE_EXCLUSIVE); } } } final HashMap<String, String> attributes = new HashMap<>(); @Override public void handleTag(boolean opening, String tag, Editable output, XMLReader xmlReader) { String Attr = ""; //if (!opening) attributes.clear(); processAttributes(xmlReader); if ("txt".equalsIgnoreCase(tag)) { Attr = attributes.get("clr"); System.out.println("clr Attr: " + Attr + ", opening: " + opening); if (Attr == null || Attr.isEmpty() || "black".equalsIgnoreCase(Attr) || Attr.charAt(0) == 'k') { System.out.println("did black, opening: " + opening); ProcessTextColour(parseColor("#000000"), opening, output); } else { if (Attr.equalsIgnoreCase("g")) { ProcessTextColour(parseColor("#b2b3b3"), opening, output); } else { System.out.println("did colour, opening: " + opening); ProcessTextColour(parseColor(Attr), opening, output); } } return; } if ("box".equalsIgnoreCase(tag)) { ProcessBox(parseColor("#d7d6d5"), opening, output); return; } if ("scl".equalsIgnoreCase(tag)) { Attr = attributes.get("fac"); System.out.println("scl Attr: " + Attr); if (Attr != null && !Attr.isEmpty()) { ProcessScaleTag(parseFloat(Attr), opening, output); } return; } if ("left".equalsIgnoreCase(tag)) { ProcessAlignment(Layout.Alignment.ALIGN_NORMAL, opening, output); return; } if ("centre".equalsIgnoreCase(tag)) { ProcessAlignment(Layout.Alignment.ALIGN_CENTER, opening, output); return; } if ("right".equalsIgnoreCase(tag)) { ProcessAlignment(Layout.Alignment.ALIGN_OPPOSITE, opening, output); return; } if ("logo".equalsIgnoreCase(tag)) { ProcessTypefaceTag(mLogo, opening, output); return; } if ("gamz".equalsIgnoreCase(tag)) { ProcessTypefaceTag(mGAMZ, opening, output); return; } if ("chalk".equalsIgnoreCase(tag)) { System.out.println("chalk " + (opening ? "opening" : "closing")); ProcessTypefaceTag(mChalk, opening, output); return; } } 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 - 1; i >= 0; --i) { if (text.getSpanFlags(objs[i]) == Spannable.SPAN_MARK_MARK) { return objs[i]; } } return null; } } private void processAttributes(final XMLReader xmlReader) { try { Field elementField = xmlReader.getClass().getDeclaredField("theNewElement"); elementField.setAccessible(true); Object element = elementField.get(xmlReader); Field attsField = element.getClass().getDeclaredField("theAtts"); attsField.setAccessible(true); Object atts = attsField.get(element); Field dataField = atts.getClass().getDeclaredField("data"); dataField.setAccessible(true); String[] data = (String[])dataField.get(atts); Field lengthField = atts.getClass().getDeclaredField("length"); lengthField.setAccessible(true); int len = (Integer)lengthField.get(atts); /** * MSH: Look for supported attributes and add to hash map. * This is as tight as things can get :) * The data index is "just" where the keys and values are stored. */ for(int i = 0; i < len; i++) attributes.put(data[i * 5 + 1], data[i * 5 + 4]); } catch (Exception e) { Log.d(TAG, "Exception: " + e); } } } private static class CustomTypefaceSpan extends TypefaceSpan { private final Typeface newType; public CustomTypefaceSpan(String family, Typeface type) { super(family); newType = type; } @Override public void updateDrawState(TextPaint ds) { applyCustomTypeFace(ds, newType); } @Override public void updateMeasureState(TextPaint paint) { applyCustomTypeFace(paint, newType); } private void applyCustomTypeFace(Paint paint, Typeface tf) { int oldStyle; Typeface old = paint.getTypeface(); if (old == null) { oldStyle = 0; } else { oldStyle = old.getStyle(); } int fake = oldStyle & ~tf.getStyle(); if ((fake & Typeface.BOLD) != 0) { paint.setFakeBoldText(true); } if ((fake & Typeface.ITALIC) != 0) { paint.setTextSkewX(-0.25f); } paint.setTypeface(tf); } } 

}

htmlTextView是从以下活动创build的:

  protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_main); theAssetManager = getAssets(); htmlTextView tv = new htmlTextView(this); tv.setDefaultTypefaceSouvenir(); tv.setTextColor(BLACK); tv.setBackgroundColor(0xfff0f0f0); tv.setPadding(4, 4, 4, 4); tv.setTextSize(30); tv.setMovementMethod(new ScrollingMovementMethod()); tv.setHTML(getString(R.string.htmljumblies)); //tv.setHTML(getString(R.string.htmltest)); RelativeLayout rl = (RelativeLayout) findViewById(R.id.rl); rl.addView(tv); } 

htmljumblies在strings.xml中定义如下。 这个特定的版本会崩溃的应用程序,但如果第一个<centre></centre>标签被删除第7和第9行, 混乱将出现集中? 令人困惑和沮丧! 保持它们,并删除<centre></centre>标签包围Jumblies ,没有任何反应。 标题中的行不是集中alignment的!

  <string name="htmljumblies"> <![CDATA[&DoubleLongRightArrow;<logo><scl fac="1.1"><font color="#e5053a">GAMZ</font></scl></logo> <chalk><scl fac="1.8"> SWAP </scl></chalk> <scl fac="1.00">Set <b>1</b>, Game <b>1</b></scl> <br> <centre> <gamz><font color="#e5053a"><scl fac="1.50">a</scl></font><scl fac="0.90">(9)</scl></gamz>, <gamz><font color="#00a3dd"><scl fac="1.50">e</scl></font><scl fac="0.90">(8)</scl></gamz>, <gamz><font color="#fba311"><scl fac="1.50">i</scl></font><scl fac="0.90">(8)</scl></gamz>, <gamz><font color="#bc5e1e"><scl fac="1.50">o</scl></font><scl fac="0.90">(8)</scl></gamz>, <gamz><font color="#bf30b5"><scl fac="1.50">u</scl></font><scl fac="0.90">(9)</scl></gamz> </centre> <br> This is an example of my custom <b>htmlTextView</b> drawn from HTML format text with custom tags to use custom fonts, colouring typeface sizing and highlight boxes. The default font is <b><i>Souvenir</i></b>, but 3 other fonts are used:<br> The <font color="#e5053a"><b><logo><scl fac="1.1">GAMZ</scl></logo></b></font> <font color="#000080"><gamz><scl fac="0.8"><box>letter</box> <box>fonts</box><sc></gamz></font> and <chalk><scl fac="1.8">swapfix</scl></chalk>, essentially <chalk><scl fac="0.9">Staccato 555</scl></chalk>, as used in the words <chalk><scl fac="1.2">SWAP</scl></chalk> and <chalk><scl fac="1.2">FIX</scl></chalk> on the <font color="#e5053a"><b><logo><scl fac="1.1">GAMZ</scl></logo></b></font> boxes. <br> <centre> <scl fac="2"><box><b> <u>The Jumblies</u> </b></box></scl><br> <font color="#0000ff"> They went to sea in a Sieve, they did,<br> In a Sieve they went to sea:<br> In spite of all their friends could say,<br> On a winter\'s morn, on a stormy day,<br> In a Sieve they went to sea!<br> And when the Sieve turned round and round,<br> And every one cried, \'You\'ll all be drowned!\'<br> They called aloud, \'Our Sieve ain\'t big,<br> But we don\'t care a button! we don\'t care a fig!<br> In a Sieve we\'ll go to sea!\'<br> Far and few, far and few,<br> Are the lands where the Jumblies live;<br> Their heads are green, and their hands are blue,<br> And they went to sea in a Sieve.<br> <br> They sailed away in a Sieve, they did,<br> In a Sieve they sailed so fast,<br> With only a beautiful pea-green veil<br> Tied with a riband by way of a sail,<br> To a small tobacco-pipe mast;<br> And every one said, who saw them go,<br> \'O won\'t they be soon upset, you know!<br> For the sky is dark, and the voyage is long,<br> And happen what may, it\'s extremely wrong<br> In a Sieve to sail so fast!\'<br> Far and few, far and few,<br> Are the lands where the Jumblies live;<br> Their heads are green, and their hands are blue,<br> And they went to sea in a Sieve.<br> <br> The water it soon came in, it did,<br> The water it soon came in;<br> So to keep them dry, they wrapped their feet<br> In a pinky paper all folded neat,<br> And they fastened it down with a pin.<br> And they passed the night in a crockery-jar,<br> And each of them said, \'How wise we are!<br> Though the sky be dark, and the voyage be long,<br> Yet we never can think we were rash or wrong,<br> While round in our Sieve we spin!\'<br> Far and few, far and few,<br> Are the lands where the Jumblies live;<br> Their heads are green, and their hands are blue,<br> And they went to sea in a Sieve.<br> <br> And all night long they sailed away;<br> And when the sun went down,<br> They whistled and warbled a moony song<br> To the echoing sound of a coppery gong,<br> In the shade of the mountains brown.<br> \'O Timballo! How happy we are,<br> When we live in a Sieve and a crockery-jar,<br> And all night long in the moonlight pale,<br> We sail away with a pea-green sail,<br> In the shade of the mountains brown!\'<br> Far and few, far and few,<br> Are the lands where the Jumblies live;<br> Their heads are green, and their hands are blue,<br> And they went to sea in a Sieve.<br> <br> They sailed to the Western Sea, they did,<br> To a land all covered with trees,<br> And they bought an Owl, and a useful Cart,<br> And a pound of Rice, and a Cranberry Tart,<br> And a hive of silvery Bees.<br> And they bought a Pig, and some green Jack-daws,<br> And a lovely Monkey with lollipop paws,<br> And forty bottles of Ring-Bo-Ree,<br> And no end of Stilton Cheese.<br> Far and few, far and few,<br> Are the lands where the Jumblies live;<br> Their heads are green, and their hands are blue,<br> And they went to sea in a Sieve.<br> <br> And in twenty years they all came back,<br> In twenty years or more,<br> And every one said, \'How tall they\'ve grown!<br> For they\'ve been to the Lakes, and the Torrible Zone,<br> And the hills of the Chankly Bore!\'<br> And they drank their health, and gave them a feast<br> Of dumplings made of beautiful yeast;<br> And every one said, \'If we only live,<br> We too will go to sea in a Sieve,---<br> To the hills of the Chankly Bore!\'<br> Far and few, far and few,<br> Are the lands where the Jumblies live;<br> Their heads are green, and their hands are blue,<br> And they went to sea in a Sieve.</centre></font> ]]> </string> 

我们一直在内部开发这个库https://github.com/square1-io/rich-text-android一段时间了,我们在一些内容密集的新闻应用程序中使用它。;

该库可以parsing大多数常见的html标签,包括video和img以及图像的远程下载。 一个自定义的视图,RichTextView可以被用来代替TextView来显示parsing的内容。

我们刚刚公开发布,所以文档仍然不完整,但提供的示例应该很容易遵循,以确定它是否符合您的需求。