轉(zhuǎn):
轉(zhuǎn)至:滬江技術(shù)學(xué)院
背景
OCS(Open Courseware System)是滬江統(tǒng)一的交互式課件制作軟件與教學(xué)工具废亭,我們?cè)谥谱髡n件的時(shí)候往往會(huì)對(duì)文字使用各種各樣的樣式淆九。例如:顏色,加粗腺怯,上標(biāo),下標(biāo)羡棵,下劃線她紫,字體沉御,行距屿讽,動(dòng)畫效果等,經(jīng)過多方的考慮最終決定使用HTML的通用標(biāo)準(zhǔn)來顯示富文本吠裆。
Web端可以直接使用伐谈,成本低廉穩(wěn)定。
iOS试疙,Android端對(duì)基本的標(biāo)簽系統(tǒng)也進(jìn)行了支持诵棵,方便使用。
以下文章重點(diǎn)講述在Android上的實(shí)現(xiàn)思路祝旷,iOS側(cè)可采用類似方案實(shí)現(xiàn)履澳。
通過對(duì)Android源碼及文檔的解讀,發(fā)現(xiàn)原生Android 使用Parse庫(該庫并不在Android源碼中是屬于第三方org.ccil.cowan.tagsoup)對(duì)HTML進(jìn)行解析怀跛。但只實(shí)現(xiàn)了部分標(biāo)簽距贷,對(duì)于CSS Style的支持基本上是0。為解決這些問題吻谋,我們對(duì)市面上的一些開源庫進(jìn)行了調(diào)研储耐。
以下列出代表性的幾個(gè)作為參考:
開源庫優(yōu)勢(shì)不足
AndroidCoreText支持圖文混排;HTML標(biāo)簽支持來自于Android原生不支持CSS樣式解析
XRichText直接調(diào)用text方法顯示html滨溉;支持自定義超鏈接,圖片长赞;能夠自定義圖片下載器不支持CSS樣式解析
HtmlSpanner支持CSS樣式晦攒;將HTML轉(zhuǎn)換成Android能識(shí)別的SpannableString支持了部分的css樣式,適配等需要做二次開發(fā)
綜上得哆,我們最終選用HtmlSpanner來做富文本的解析脯颜。
富文本解析總共分為以下三塊:
HTML標(biāo)簽支持
CSS樣式支持
多字體支持
HTML標(biāo)簽支持
HtmlSpanner是在原生庫的基礎(chǔ)上進(jìn)行擴(kuò)展封裝的。因此Android能支持的標(biāo)簽贩据,這個(gè)庫一并都支持了栋操。但是在特殊字符的處理上還需要手動(dòng)的進(jìn)行替換,特殊字符處理統(tǒng)一封裝在TextUtil類中饱亮。
Android不支持的一些HTML標(biāo)簽矾芙,而項(xiàng)目中又用得到的,可在HtmlSpanner的registerBuiltInHandlers方法中進(jìn)行注冊(cè)近上。
CSS樣式支持
HtmlSpanner對(duì)CSS的樣式是支持的剔宪,引用了Cssparser庫來解析CSS。
源碼地址:https://github.com/corgrath/osbcp-css-parser
HtmlSpanner 對(duì)Cssparser 庫的使用主要集在 CSSCompiler.java,將文本轉(zhuǎn)換成Rule的對(duì)象葱绒。
代碼如下:
privatevoidparseCSSFromText(String text, SpanStack spanStack){try{for(Rule rule : CSSParser.parse(text)) {? ? ? ? ? ? spanStack.registerCompiledRule(CSSCompiler.compile(rule, getSpanner()));? ? ? ? }? ? }catch(Exception e) {? ? ? ? Log.e("StyleNodeHandler","Unparseable CSS definition", e);? ? }}
若在樣式表中增加自定義屬性也能在該類中進(jìn)行擴(kuò)展感帅。
publicstaticStyleUpdatergetStyleUpdater(finalString key,finalString value){if("font-family".equals(key)) {//解析文本字體returnnewStyleUpdater() {@OverridepublicStyleupdateStyle(Style style, HtmlSpanner spanner){? ? ? ? ? ? ? ? Log.d("CSSCompiler","Applying style "+ key +": "+ value );? ? ? ? ? ? ? ? FontFamily family = spanner.getFont( value );? ? ? ? ? ? ? ? Log.d("CSSCompiler","Got font "+ family );returnstyle.setFontFamily(family);? ? ? ? ? ? }? ? ? ? };? ? }? ? ? ... ...}
多字體支持
在課件的制作中往往會(huì)使用到各類字體,比如 中文地淀,日文失球,韓文,法語等帮毁,也會(huì)用到一些海報(bào)體等藝術(shù)字实苞,若在手機(jī)中沒有包含這些字體時(shí),系統(tǒng)一般會(huì)用默認(rèn)字體來顯示作箍,整體的效果也會(huì)大打折扣硬梁。
we.png
常規(guī)的做法是將相應(yīng)的文字生成圖片,但是這樣的做法會(huì)帶來諸多的問題:
圖片數(shù)量多胞得,占用空間大
適配不一樣屏幕則需要多套圖片
文字動(dòng)畫效果難以實(shí)現(xiàn)
Android也提供了加載外部字體的功能荧止,但該功能在TextView上一次只能加載一套字體,并且一個(gè)常規(guī)的字體庫一般都有20M左右阶剑,如果一節(jié)課中使用多個(gè)字體則會(huì)對(duì)APP造成極大的負(fù)擔(dān)跃巡。
為解決這些問題我們找到了一種較好的方案:
APP從服務(wù)器獲取到精簡版的字體(只包含需要顯示的字體文件),然后構(gòu)造HtmlSpanner對(duì)象 牧愁。
HtmlSpanner 在像SystemFontResolver對(duì)象獲取需要的FontFamily對(duì)象素邪,最終轉(zhuǎn)換成android TextView 能夠識(shí)別的SpannableString對(duì)象。
流程如下:
1111.png
第一步,制作精簡版字庫
精簡版字庫可以用google提供的一款開源庫來解決猪半。
下載地址:https://code.google.com/p/sfntly/
使用起來也十分簡單:
java -jar sfnttool.jar-s"需要提取的文字"原始字體文件.ttf新字體文件.ttf
執(zhí)行以上命令即可得到一份精簡版的字體文件兔朦。
通過對(duì)幾種字體的提取得出以下數(shù)據(jù):
字體5字100字500字1000字原始大小
微軟雅黑10k46k205k422k14.7 M
華康娃娃體8k29k135k284k3 M
思源黑體6k24k95k192K8 M
第二步,自定義字體
Android端默認(rèn)支持三種字體:monospace,sans磨确,serif沽甥。
并有四種表現(xiàn)形式:正常、斜體乏奥、粗體摆舟、粗斜體。
DroidSans是默認(rèn)英文
DroidSansFallback 字體是 Google 為手機(jī)"Android"內(nèi)建的系統(tǒng)字體邓了,支持繁體中文恨诱、簡體中文、韓文骗炉、日文照宝。
HtmlSpanner 庫中 SystemFontResolver 類是用于構(gòu)造字體對(duì)象的。該類中構(gòu)造了4個(gè)字體痕鳍。
代碼如圖:
publicSystemFontResolver(){this.defaultFont =newFontFamily("default", Typeface.DEFAULT);this.serifFont =newFontFamily("serif", Typeface.SERIF);this.sansSerifFont =newFontFamily("sans-serif", Typeface.SANS_SERIF);this.monoSpaceFont =newFontFamily("monospace", Typeface.MONOSPACE );}
自定義字體需要對(duì)該類進(jìn)行一定的擴(kuò)展硫豆,使之能夠支持動(dòng)態(tài)加載龙巨,釋放字體對(duì)象。
@OverridepublicvoidapplyFont(String fontName, String path){if(!TextUtils.isEmpty(fontName) && !TextUtils.isEmpty(path)) {if(fontName.startsWith("\"") && fontName.endsWith("\"")) {? ? ? ? ? ? fontName = fontName.substring(1, fontName.length() -1);? ? ? ? }if(fontName.startsWith("\'") && fontName.endsWith("\'")) {? ? ? ? ? ? fontName = fontName.substring(1, fontName.length() -1);? ? ? ? }? ? ? ? File file =newFile(path);if(file.exists()) {? ? ? ? ? ? fontName = fontName.toLowerCase();? ? ? ? ? ? customFontMap.put(fontName,newFontFamily(fontName, Typeface.createFromFile(file)));? ? ? ? }? ? }}
第三步,顯示字體
//動(dòng)態(tài)自定義字體HtmlSpannerHelper.getInstance().getSpanner().getFontResolver().applyFont(getAssets(),"藝術(shù)1","fonts/a1.ttf");TextView tx2 = (TextView) findViewById(R.id.txt2);String content1 ="0129fg";HtmlSpannerHelper.getInstance().setText(tx2, content1);
以上是OCS播放器富文本的實(shí)現(xiàn)思路熊响,其中我們主要借助HtmlSpanner 庫進(jìn)行二次開發(fā)旨别,下面來看看HtmlSpanner庫是如何運(yùn)作的:
HtmlSpanner 剖析
y7.png
HtmlSpanner通過注冊(cè)大量的Handler 來識(shí)別Html 的標(biāo)簽,將注冊(cè)過的標(biāo)簽通過HtmlCleaner庫進(jìn)行解析汗茄,最終轉(zhuǎn)換成TextView能夠識(shí)別的Spannable對(duì)象進(jìn)行顯示秸弛。
HtmlSpanner 的解析流程
y6.png
HtmlSpanner 有很完善的擴(kuò)展性,例如擴(kuò)展自定義標(biāo)簽:
registerHandler("span", wrap((newStyledTextHandler())));
參數(shù)一為自定義的標(biāo)簽名稱洪碳,參數(shù)二是TagNodeHandler的繼承類
publicclassStyledTextHandlerextendsTagNodeHandler{privateStyle style;publicStyledTextHandler(){this.style =newStyle();? ? }@OverridepublicvoidbeforeChildren(TagNode node, SpannableStringBuilder builder, SpanStack spanStack){? ? ......? ? ? }}
遇到的坑
1. 行高在不同版本SDK中顯示不一樣
低版本的Android系統(tǒng)
h1.png
Android M上的顯示效果
h2.png
解決:
自定義一個(gè)LineHeightSpan引用 android.text.style.LineHeightSpan
重寫 chooseHeight 方法
@OverridepublicvoidchooseHeight(CharSequence text,intstart,intend,intspanstartv,intv, Paint.FontMetricsInt fm){intextra =0;if(height >0) {? ? ? ? extra = Math.round((height - (fm.descent - fm.ascent)));? ? }elseif(spacingmult >0) {? ? ? ? extra = Math.round((fm.descent - fm.ascent) * (spacingmult -1));? ? }if(android.os.Build.VERSION.SDK_INT >=23) {if(start == ((Spanned) text).getSpanStart(this)) {? ? ? ? ? ? fm.descent += extra;? ? ? ? ? ? fm.bottom = fm.descent;? ? ? ? }? ? }else{? ? ? ? fm.descent += extra;? ? ? ? fm.bottom = fm.descent;? ? }}
2. 文字大小適配
文字大小的解析來源與FontHandler 與StyledTextHandler递览。
在切換橫豎屏的時(shí)候進(jìn)行重繪。
//If we have a bottom margin, we insert an extra newline. We'll manipulate the line height//of this newline to create the margin.if(useStyle.getMarginBottom() !=null) {? ? StyleValue styleValue = useStyle.getMarginBottom();if(styleValue.getUnit() == StyleValue.Unit.PX) {if(styleValue.getIntValue() >0) {? ? ? ? ? ? appendNewLine(builder);? ? ? ? ? ? stack.pushSpan(newVerticalMarginSpan(styleValue.getIntValue()),? ? ? ? ? ? ? ? ? ? builder.length() -1, builder.length());? ? ? ? }? ? }else{if(styleValue.getFloatValue() >0f) {? ? ? ? ? ? appendNewLine(builder);? ? ? ? ? ? stack.pushSpan(newVerticalMarginSpan(styleValue.getFloatValue()),? ? ? ? ? ? ? ? ? ? builder.length() -1, builder.length());? ? ? ? }? ? }}
3. 系統(tǒng)資源消耗大
顯示藝術(shù)字時(shí)字體文件都在內(nèi)存中瞳腌,因此在退出的時(shí)候要記得手動(dòng)release一下绞铃,把資源清理掉。
設(shè)置文字的時(shí)候由于解析富文本嫂侍、圖片下載顯示比較耗時(shí),所以使用的是handler機(jī)制儿捧。因此也要注意handler的泄漏問題,最好是static的 handler挑宠。
publicvoidsetText(finalTextView textView,finalString text){newThread(newRunnable() {@Overridepublicvoidrun(){if(textView !=null&& text !=null) {? ? ? ? ? ? ? ? Spannable spannable = mSpannable.fromHtml(text);? ? ? ? ? ? ? ? Message message = mHandler.obtainMessage();? ? ? ? ? ? ? ? SpannableMessage obj =newSpannableMessage(textView, spannable);? ? ? ? ? ? ? ? message.obj = obj;? ? ? ? ? ? ? ? mHandler.sendMessage(message);? ? ? ? ? ? }? ? ? ? }? ? }).start();}
后續(xù)改進(jìn)點(diǎn)
HtmlSpanner 采用HTMLCleaner解析庫對(duì)HTML標(biāo)簽進(jìn)行解析菲盾。該庫比較龐大,方法數(shù)較多各淀,可替換成SDK內(nèi)置的javax.xml.parsers庫來進(jìn)行解析懒鉴。
HtmlSpanner庫可結(jié)合Picasso庫對(duì)圖片顯示進(jìn)行優(yōu)化,使其更強(qiáng)大碎浇。
在一個(gè)標(biāo)簽中存在多個(gè)樣式屬性時(shí) HtmlSpanner 僅解析了第一個(gè)style屬性临谱,可進(jìn)行style合并優(yōu)化處理。