talk is cheep, show you the code. 原始碼參考
背景
儘管 Android 裝置的效能日益增強,但是通過 webview 來展示內容和原生的體驗還是有一定的差距的,在某些情況下,我們只是需要簡單的圖文並排就夠了,比如一些帖子,這個時候用 webview 就顯的有點重,考慮到這一點,我們決定在客戶端原生支援特定的網頁標籤。
為了兼顧到各個平臺,我們約定輸出是標準的 html 內容,對於已有的內容,可以進行內容的重新排版,把多餘的標籤去掉並換成約定好的標籤。
設計思路
筆者有著多年對於 markdown 編輯器的使用經驗,對於markdown語法的簡潔有深度的喜愛,對於很多時候的編輯工作都是夠用了,我更傾向於輕便夠用而非周全複雜的東西,在設計編輯器的時候,不經意就想到了 markdown 。
經過協商我們初期先支援下面的幾種簡單的樣式:
- 標題[一級] 對應
<h1>
標籤。 - 文字段落[一級] 對應
<p>
標籤。 - 文字加粗、換行[二級,嵌在
<p>
裡面] 對應<b>
<br>
標籤。 - 圖片[一級],對應
<img>
標籤。 - 超連結[二級] 對應
<a>
標籤。
約定結果:
正確:
<h1>這是一級標題</h1>
<p>段落1</p>
<p>段落2<b>我是加粗部分</b>hello<br></p>
<img src="http://github.com/pic.png" width="100", height="100"/>
<p>段落3<b>加粗加粗加粗<br>加粗加粗加粗</b></p>
複製程式碼
錯誤:
<p>段落1<p>內部段落</p></p>
<p>段落3<img src="http://github.com/pic.png" width="100", height="100"/></p>
複製程式碼
定義好支援的標籤之後,接著就是由設計師設計好各個標籤對應的文字樣式,間距和圖片的展示方式了。
編輯器實現
通過研究幾個開源專案,發現原生實現富文字編輯器主要有兩個思路,一個是基於單個 EditText 通過組合不同的 Spannable 來實現,另外一個是組合 EditText 和 ImageView 等不同的控制元件,個人認為第二種方式更加靈活,但是加粗,連結等處理也是需要 Spannable 的,因此組合了兩種方式。
根據上面約定支援的標籤,我定義了三種型別的控制元件:
EditImageView
是插入編輯框的圖片控制元件,它也負責了上傳的相關工作。RichEditText
這個控制元件負責段落的編輯,段落內可以支援一些文字樣式,比如加粗和超連結。這個控制元件是 cwac-richedit 的一個實現,它封裝了很多的 spannable 實現,這裡只是用到了加粗和連結,原始碼雖然有改動,為了尊重作者的勞動成果,決定不改動它的名字。HeadingEditText
這個控制元件用來處理標題的輸入,其實就是字型大一些和加粗的 EditText。
RichTextEditor
是比較核心的實現,它繼承了ScrollView
,它的職責是協調控制元件和游標、返回鍵之間的互動,主要實現了下面的介面:
輸出:生成 html 的過程其實就是遍歷各個控制元件了 RichEditText 裡面的 Spannable 的過程。
Note: 值得一提的是筆者在看的時候,發現 cwac-richedit 這個專案是執行不起來的,一般情況下到這裡就放棄對這個庫的研究了,但是翻了下程式碼,發現作者的單元測試很充分,而且文件描述也算是比較清晰,仔細研究了一下,發現程式碼設計的有很多亮點,思路也非常清晰,於是後面就選擇了這個庫作為基礎的文欄位落樣式實現,如果想基於它實現下劃線,斜體,字型顏色等功能,應該是非常方便的一件事情。
網頁內容顯示實現
我們的富文字會作未帖子的詳情和評論列表中,由於是出現在列表中,我們需要考慮到控制元件的複用問題,所以一開始定義一個完整實現的富文字控制元件的思路就放棄了,而是通過按豎直方向拆分不同的 Item,利用 RecylerView 或者 ListView 的複用特性來實現,儘管這樣做起來會麻煩不少,但是完美地避免了列表不斷滑動過程中物件不斷建立和銷燬帶來的記憶體抖動問題。
從下面流程圖可以看出這個處理流程,首先解析 html 相關節點,並把其中相關的值和屬性封裝到不同的物件中,然後通過列表資料去驅動整個檢視的顯示,解析 Html 是通過 Jsoup 來實現的,介面非常友好,和用 Jquery 差不多。
由於要處理的標籤很少,在 Jsoup 的幫助下,整個解析程式碼不超過 30 行:
Document doc = Jsoup.parseBodyFragment(htmlContent);
List<Node> childNodeList = doc.body().childNodes();
if (childNodeList == null || childNodeList.isEmpty()) {
return null;
}
final int size = childNodeList.size();
List<IHtmlElement> elList = new ArrayList<>();
for (int pos = 0; pos != size; pos++) {
Node childNode = childNodeList.get(pos);
String tagName = childNode.nodeName();
if (tagName.equalsIgnoreCase("h")) {
elList.add(new PElement(Html.fromHtml(((Element) childNode).html())));
} else if(tagName.equalsIgnoreCase("h1")){
elList.add(new HElement(((Element) childNode).html()));
}else if (tagName.equalsIgnoreCase("img")) {
String src = childNode.attr("src");
String width = childNode.attr("width");
String height = childNode.attr("height");
elList.add(new ImgElement(src, NumberUtils.parseInt(width, 0), NumberUtils.parseInt(height, 0)));
} else {
if (childNode instanceof Element) {
elList.add(new PElement(Html.fromHtml(((Element) childNode).html())));
} else {
elList.add(new PElement(htmlContent));
}
}
}
複製程式碼
節點物件和對應的 Item 檢視,可以看到結構還是非常清晰的,對於以後想新增一些其他的樣式也是很好擴充套件的。
總結
雖然不是一個很複雜的東西,從整個實現思路來看,還是比較好的兼顧了效能和可擴充套件度,也很好體現了分而治之和資料驅動檢視的開發模式。不過從功能上來說還是存在一些缺陷的,比如游標不能跨段進行選擇, 只能支援至上而下的排版。
參考
- XRichText: https://github.com/sendtion/XRichText
- cwac-richedit: https://github.com/commonsguy/cwac-richedit
- Html解析庫Jsoup:https://jsoup.org/
技術交流群:70948803,大部分時間群裡都是安靜的,只交流技術相關,很少發言,不歡迎廣告噴子。