文章介绍了如何在 Android 实现一个简单的富文本编辑器以及富文本的展示方案,并附有源码 demo 实现,读者可以先把 demo 运行起来,对照代码和文章阅读应该不难弄懂。
talk is cheep, show you the code. 源码参考
背景
尽管Android设备的性能日益增强,但是通过webview来展示内容和原生的体验还是有一定的差距的,在某些情况下,我们只是需要简单的图文并排就够了,比如一些帖子,这个时候用webview就显的有点重,考虑到这一点,我们决定在客户端原生支持特定的网页标签。
为了兼顾到各个平台,我们约定输出是标准的html内容,对于已有的内容,可以进行内容的重新排版,把多余的标签去掉并换成约定好的标签。
设计思路
笔者有着多年对于markdown编辑器的使用经验,对于markdown语法的简洁有深度的喜爱,对于很多时候的编辑工作都是够用了,我更倾向于轻便够用而非周全复杂的东西,在设计编辑器的时候,不经意就想到了markdown。
经过协商我们初期先支持下面的几种简单的样式:
- 标题[一级] 对应
<h1>
标签。 - 文本段落[一级] 对应
<p>
标签。 - 文本加粗、换行[二级,嵌在
<p>
里面] 对应<b>
<br>
标签。 - 图片[一级],对应
<img>
标签。 - 超链接[二级] 对应
<a>
标签。
约定结果:
正确:
1 | <h1>这是一级标题</h1> |
2 | <p>段落1</p> |
3 | <p>段落2<b>我是加粗部分</b>hello<br></p> |
4 | <img src="http://github.com/pic.png" width="100", height="100"/> |
5 | <p>段落3<b>加粗加粗加粗<br>加粗加粗加粗</b></p> |
错误:
1 | <p>段落1<p>内部段落</p></p> |
2 | <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行:
1 | |
2 | Document doc = Jsoup.parseBodyFragment(htmlContent); |
3 | List<Node> childNodeList = doc.body().childNodes(); |
4 | if (childNodeList == null || childNodeList.isEmpty()) { |
5 | return null; |
6 | } |
7 | final int size = childNodeList.size(); |
8 | List<IHtmlElement> elList = new ArrayList<>(); |
9 | for (int pos = 0; pos != size; pos++) { |
10 | Node childNode = childNodeList.get(pos); |
11 | String tagName = childNode.nodeName(); |
12 | if (tagName.equalsIgnoreCase("h")) { |
13 | elList.add(new PElement(Html.fromHtml(((Element) childNode).html()))); |
14 | } else if(tagName.equalsIgnoreCase("h1")){ |
15 | elList.add(new HElement(((Element) childNode).html())); |
16 | }else if (tagName.equalsIgnoreCase("img")) { |
17 | String src = childNode.attr("src"); |
18 | String width = childNode.attr("width"); |
19 | String height = childNode.attr("height"); |
20 | elList.add(new ImgElement(src, NumberUtils.parseInt(width, 0), NumberUtils.parseInt(height, 0))); |
21 | } else { |
22 | if (childNode instanceof Element) { |
23 | elList.add(new PElement(Html.fromHtml(((Element) childNode).html()))); |
24 | } else { |
25 | elList.add(new PElement(htmlContent)); |
26 | } |
27 | } |
28 | } |
节点对象和对应的Item视图,可以看到结构还是非常清晰的,对于以后想添加一些其他的样式也是很好扩展的。
总结
虽然不是一个很复杂的东西,从整个实现思路来看,还是比较好的兼顾了性能和可扩展度,也很好体现了分而治之和数据驱动视图的开发模式。不过从功能上来说还是存在一些缺陷的,比如光标不能跨段进行选择, 只能支持至上而下的排版。
参考
- XRichText: https://github.com/sendtion/XRichText
- cwac-richedit: https://github.com/commonsguy/cwac-richedit
- Html解析库Jsoup:https://jsoup.org/
关于Agile Studio工作室
我们是一支由资深独立开发者和设计师组成的团队,成员均有扎实的技术实力和多年的产品设计开发经验,提供可信赖的软件定制服务。
未经声明,本站文章均为原创,转载请附上链接:
http://blog.agilestudio.cn/Android-RichEditor-And-NativeHtml/