Android 富文本擴(kuò)展

轉(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)化處理。

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末奴璃,一起剝皮案震驚了整個(gè)濱河市吴裤,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌溺健,老刑警劉巖,帶你破解...
    沈念sama閱讀 218,451評(píng)論 6 506
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件钮蛛,死亡現(xiàn)場(chǎng)離奇詭異鞭缭,居然都是意外死亡,警方通過查閱死者的電腦和手機(jī)魏颓,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 93,172評(píng)論 3 394
  • 文/潘曉璐 我一進(jìn)店門岭辣,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人甸饱,你說我怎么就攤上這事沦童÷乇簦” “怎么了?”我有些...
    開封第一講書人閱讀 164,782評(píng)論 0 354
  • 文/不壞的土叔 我叫張陵偷遗,是天一觀的道長墩瞳。 經(jīng)常有香客問我,道長氏豌,這世上最難降的妖魔是什么喉酌? 我笑而不...
    開封第一講書人閱讀 58,709評(píng)論 1 294
  • 正文 為了忘掉前任,我火速辦了婚禮泵喘,結(jié)果婚禮上泪电,老公的妹妹穿的比我還像新娘。我一直安慰自己纪铺,他們只是感情好相速,可當(dāng)我...
    茶點(diǎn)故事閱讀 67,733評(píng)論 6 392
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著鲜锚,像睡著了一般突诬。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上烹棉,一...
    開封第一講書人閱讀 51,578評(píng)論 1 305
  • 那天攒霹,我揣著相機(jī)與錄音,去河邊找鬼浆洗。 笑死催束,一個(gè)胖子當(dāng)著我的面吹牛,可吹牛的內(nèi)容都是我干的伏社。 我是一名探鬼主播抠刺,決...
    沈念sama閱讀 40,320評(píng)論 3 418
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場(chǎng)噩夢(mèng)啊……” “哼摘昌!你這毒婦竟也來了速妖?” 一聲冷哼從身側(cè)響起,我...
    開封第一講書人閱讀 39,241評(píng)論 0 276
  • 序言:老撾萬榮一對(duì)情侶失蹤聪黎,失蹤者是張志新(化名)和其女友劉穎罕容,沒想到半個(gè)月后,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體稿饰,經(jīng)...
    沈念sama閱讀 45,686評(píng)論 1 314
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡锦秒,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 37,878評(píng)論 3 336
  • 正文 我和宋清朗相戀三年,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了喉镰。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片旅择。...
    茶點(diǎn)故事閱讀 39,992評(píng)論 1 348
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡,死狀恐怖侣姆,靈堂內(nèi)的尸體忽然破棺而出生真,到底是詐尸還是另有隱情沉噩,我是刑警寧澤,帶...
    沈念sama閱讀 35,715評(píng)論 5 346
  • 正文 年R本政府宣布柱蟀,位于F島的核電站川蒙,受9級(jí)特大地震影響,放射性物質(zhì)發(fā)生泄漏产弹。R本人自食惡果不足惜派歌,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 41,336評(píng)論 3 330
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望痰哨。 院中可真熱鬧胶果,春花似錦、人聲如沸斤斧。這莊子的主人今日做“春日...
    開封第一講書人閱讀 31,912評(píng)論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽撬讽。三九已至蕊连,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間游昼,已是汗流浹背甘苍。 一陣腳步聲響...
    開封第一講書人閱讀 33,040評(píng)論 1 270
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留烘豌,地道東北人载庭。 一個(gè)月前我還...
    沈念sama閱讀 48,173評(píng)論 3 370
  • 正文 我出身青樓,卻偏偏與公主長得像廊佩,于是被迫代替她去往敵國和親囚聚。 傳聞我的和親對(duì)象是個(gè)殘疾皇子,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 44,947評(píng)論 2 355

推薦閱讀更多精彩內(nèi)容

  • Android 自定義View的各種姿勢(shì)1 Activity的顯示之ViewRootImpl詳解 Activity...
    passiontim閱讀 172,144評(píng)論 25 707
  • “也許欣賞不是一種愉悅标锄,更是一種關(guān)注顽铸。我們通過思考而對(duì)某一件藝術(shù)作品欣賞,是一種無法解釋的關(guān)注料皇,排除了任何企圖谓松;這...
    陳三白閱讀 290評(píng)論 7 3
  • 北京貨車今天開始不能進(jìn)六環(huán)了歉眷,基本都是京牌了牺六,這兩個(gè)月不停的市場(chǎng)被關(guān)停,工廠停工汗捡,2018年1月1日前北京市所有無...
    孫宇新閱讀 620評(píng)論 0 2
  • 時(shí)間匆匆 我總是計(jì)劃著 等我有錢的 我要去學(xué)習(xí)服裝設(shè)計(jì) 放下因?yàn)樯畹墓ぷ?漸漸的淑际,我想得到的更多了 等我有錢了 ...
    子恒予以閱讀 157評(píng)論 0 0
  • 情緒的毛線團(tuán)啊,一個(gè)點(diǎn)扇住,一條線春缕,一條越來越長的線,越來越長的線啊艘蹋,纏啊纏锄贼,繞啊繞,繞啊繞女阀,纏啊纏宅荤,纏著你,繞著你浸策,...
    心晴伽園閱讀 322評(píng)論 0 0