您当前的位置:首页 > 互联网教程

如何实现一个 Android 端的富文本编辑器

发布时间:2025-05-13 00:19:07    发布人:远客网络

如何实现一个 Android 端的富文本编辑器

一、如何实现一个 Android 端的富文本编辑器

在 Android上实现富文本编辑器的思路大致分为三种:

使用多种 Layout布局,每一种布局对应一种 HTML格式,比如图片,比如顺序列表等。具体的实现例子可以参考这个链接。 Medium和

Evernote的富文本编辑就是采用这种方式实现的。总体来说比较复杂。

WebView+ JavaScript实现。现在 Web端有很多成熟的 JavaScript富文本编辑库,比如 Squire,你只需要做好

WebView和 JavaScript的交互就可以了(多写回调函数)。理论上虽然是这么说,但是在实现过程你需要解决 WebView的兼容性问题(

Android 4.4及其以上版本和 4.4以下版本的 WebView内核不一样),以及其他一些不可预见的问题(比如就遇到无法粘贴文字的问题)。

EditText+ Span。 Android的 TextView原生支持诸如粗体、删除线、引用等 Span

,要实现简单的富文本编辑需求,可操作性还是比较大的。综合再三,选择了这种方式来实现自己的需求。

既然决定使用 EditText+ Span的方式来实现,必然要对相关的 API有所了解。

首先来了解一下 Span。Span是一个强大的概念,有兴趣深入的同学推荐直接阅读这篇译文。

在这里主要使用两种类型的 Span:

继承自 CharacterStyle的 Span,比如 StyleSpan,可以在字符级别上添加粗体,下划线等。

继承自 ParagraphStyle的 Span,比如 QuoteSpan,可以为段落级别的文本添加引用。

接着需要一个可以将 Span的效果设置进去的文本结构(即实现了 Spannable接口), SpannableStringBuilder

是个不错的选择,同时 EditText提供的 getEditableText()方法也可以获得。通常只需要 getEditableText()

就可以了,但是在面对一些细节部分,可以使用 SpannableStringBuilder预先设置相应的 Span,再替换到原来的文本中。

设置 Span的方式也很简单,需要调用 Spannable.setSpan(Object what, int start, int end, int

flags)这个方法即可,方法中 4个参数的解释如下:

Object what,传入你使用的 Span对象。

int start,设置 Span的开始位置。

int end,设置 Span的结束位置。

int flags,代表设置 Span的作用域。

在这里重点介绍一下 int flags这个参数,它接受 4种类型的参数,分别是:

Spanned.SPAN_INCLUSIVE_EXCLUSIVE,表示你在设置 Span的区域之前输入文字,输入的文字也会受到 Span

Spanned.SPAN_INCLUSIVE_INCLUSIVE,表示你在设置 Span的区域前后输入文字,输入的文字都后受到 Span

Spanned.SPAN_EXCLUSIVE_EXCLUSIVE,表示你在设置 Span的区域中出输入文字,输入的文字才会受到 Span

Spanned.SPAN_EXCLUSIVE_INCLUSIVE,表示你在设置 Span的区域之后输入文字,输入的文字也会受到 Span

「受到影响」的意思就是,仍然会保持你设置的 Span的样式,比如选择Spanned.SPAN_EXCLUSIVE_INCLUSIVE

设置了一段文字的粗体,那么在这段文字后新输入的文字,也会是粗体。在这里推荐使用Spanned.SPAN_EXCLUSIVE_EXCLUSIVE

参数,毕竟其他几种参数相对不是很好控制,而且会给使用的人带来的疑惑。认为一个操作代表的行为应当是准确没有歧义的。

好,到这里已经知道大致怎么作出一个富文本编辑器组件的样子了,无非是指定开始位置和结束位置,再设置相应的 Span

即可。至于设置的时候采取什么样的规则,你可以自己定制。但仅仅解决了编辑的问题,仍然存在导入的问题和导出的问题。

导入的问题十分简单, Android SDK中提供了 Html.fromHtml()这个方法,可以很轻松地将 HTML字符串转换为所需的

Spanned对象。但是需要注意的是, Html.fromHtml()并不支持所有的 HTML标签,比如无序列表就不支持,因此你需要自己实现

Html.TagHandler接口来处理自己所需的标签,可以参考这个链接,实现了删除线和简单无序列表的支持。

面对粗体、斜体这样字符级别的样式, Html.fromHtml()

会自然而然的解析,该添加换行的地方就添加换行,并没有什么问题;但是面对引用、无序列表这样段落级别的样式,该方法会追加一个换行,也就是两个换行操作,相当于多出一个空行。通常来说认为一个

,但是如果你有特别需求的话,也可以通过前面说的那样,自己来解析,而不是用系统默认的方式。

之前介绍了如何导入,想必你也十分清楚,必然有一个对应的Html.toHtml()方法!没错,但是遗憾的是,这个方法也不全支持所有 Span

,比如列表就不支持。不过没有关系, Html.toHtml()这个方法本身的源码简洁易懂,可以参考着实现。

在这里重点说明 Spannanle的一个接口方法 nextSpanTransition(int start, int limit, Class

type),这个方法会在你指定的文本范围内,返回下一个你指定的 Span类型的开始位置,依照这个方法,就可以逐层扫描指定的 Span

,而不用同时考虑其他类型的 Span的影响,十分有用。

最后尽管说了这么多,导入导出还是有一个比较关键的问题,即导入的内容和导出的内容要保持一致,在这点上目前我还比较难以实现,只能说尽量控制吧,必要的时候还需要使用一下正则来处理导入导出的文本。

二、有哪些android开发技巧

意思是控件的绘制区域是否在padding里面。默认为true。如果你设置了此属性值为false,就能实现一个在布局上事半功陪的效果。先看一个效果图。

上图中的ListView顶部默认有一个间距,向上滑动后,间距消失,如下图所示。

如果使用margin或padding,都不能实现这个效果。加一个headerView又显得大材小用,而且过于麻烦。此处的clipToPadding配合paddingTop效果就刚刚好。

同样,还有另外一个属性也很神奇:android:clipChildren,具体请参考:【Android】神奇的android:clipChildren属性

按理说这两个属性一目了然,一个是填充布局空间适应父控件,一个是适应自身内容大小。但如果在列表如ListView中,用错了问题就大了。ListView中的getView方法需要计算列表条目,那就必然需要确定ListView的高度,onMesure才能做测量。如果指定了wrap_content,就等于告诉系统,如果我有一万个条目,你都帮我计算显示出来,然后系统按照你的要求就new了一万个对象出来。那你不悲剧了?先看一个图。

假设现在ListView有8条数据,match_parent需要new出7个对象,而wrap_content则需要8个。这里涉及到View的重用,就不多探讨了。所以这两个属性的设置将决定getView的调用次数。

由此再延伸出另外一个问题:getView被多次调用。

什么叫多次调用?比如position=0它可能调用了几次。看似很诡异吧。GridView和ListView都有可能出现,说不定这个祸首就是wrap_content。说到底是View的布局出现了问题。如果嵌套的View过于复杂,解决方案可以是通过代码测量列表所需要的高度,或者在getView中使用一个小技巧:parent.getChildCount== position

public View getView(int position, View convertView, ViewGroup parent){

if(parent.getChildCount()== position){

3、IllegalArgumentException: pointerIndex out of range

出现这个Bug的场景还是很无语的。一开始我用ViewPager+ PhotoView(一个开源控件)显示图片,在多点触控放大缩小时就出现了这个问题。一开始我怀疑是PhotoView的bug,找了半天无果。要命的是不知如何try,老是crash。后来才知道是android遗留下来的bug,源码里没对pointer index做检查。改源码重新编译不太可能吧。明知有exception,又不能从根本上解决,如果不让它crash,那就只能try-catch了。解决办法是:自定义一个ViewPager并继承ViewPager。请看以下代码:

*自定义封装android.support.v4.view.ViewPager,重写onInterceptTouchEvent事件,捕获系统级别异常

public class CustomViewPager extends ViewPager{

public CustomViewPager(Context context){

public CustomViewPager(Context context, AttributeSet attrs){

public boolean onInterceptTouchEvent(MotionEvent ev){

return super.onInterceptTouchEvent(ev);

} catch(IllegalArgumentException e){

} catch(ArrayIndexOutOfBoundsException e){

把用到ViewPager的布局文件,替换成CustomViewPager就OK了。

4、ListView中item点击事件无响应

listView的Item点击事件突然无响应,问题一般是在listView中加入了button、checkbox等控件后出现的。这个问题是聚焦冲突造成的。在android里面,点击屏幕之后,点击事件会根据你的布局来进行分配的,当你的listView里面增加了button之后,点击事件第一优先分配给你listView里面的button。所以你的点击Item就失效了,这个时候你就要根据你的需求,是给你的item的最外层layout设置点击事件,还是给你的某个布局元素添加点击事件了。

解决办法:在ListView的根控件中设置(若根控件是LinearLayout,则在LinearLayout中加入以下属性设置)descendantFocusability属性。

android:descendantFocusability="blocksDescendants"

5、getSupportFragmentManager()和getChildFragmentManager()

有一个需求,Fragment需要嵌套3个Fragment。基本上可以想到用ViewPager实现。开始代码是这样写的:

mViewPager.setAdapter(new CustomizeFragmentPagerAdapter(getActivity().getSupportFragmentManager(), subFragmentList));

导致的问题是嵌套的Fragment有时会莫名其妙不显示。开始根本不知道问题出现在哪,当你不知道问题的原因时,去解决这个问题显然比较麻烦。经过一次又一次的寻寻觅觅,终于在stackoverflow上看到了同样的提问。说是用getChildFragmentManager()就可以了。真是这么神奇!

mViewPager.setAdapter(new CustomizeFragmentPagerAdapter(getChildFragmentManager, subFragmentList));

让我们看一下这两个有什么区别。首先是getSupportFragmentManager(或者getFragmentManager)的说明:

Return the FragmentManager for interacting with fragments associated with this fragment's activity.

然后是getChildFragmentManager:

Return a private FragmentManager for placing and managing Fragments inside of this Fragment.

Basically, the difference is that Fragment's now have their own internal FragmentManager that can handle Fragments. The child FragmentManager is the one that handles Fragments contained within only the Fragment that it was added to. The other FragmentManager is contained within the entire Activity.

这样的设计是不是很奇怪?两个同样会滚动的View居然放到了一起,而且还是嵌套的关系。曾经有一个这样的需求:界面一共有4个区域部分,分别是公司基本信息(logo、名称、法人、地址)、公司简介、公司荣誉、公司口碑列表。每部分内容都需要根据内容自适应高度,不能写死。鄙人首先想到的也是外部用一个ScrollView包围起来。然后把这4部分分别用4个自定义控件封装起来。基本信息和公司简介比较简单,荣誉需要用到RecyclerView和TextView的组合,RecyclerView(当然,用GridView也可以,3列多行的显示)存放荣誉图片,TextView显示荣誉名称。最后一部分口碑列表当然是ListView了。这时候,问题就出来了。需要解决ListView放到ScrollView中的滑动问题和RecyclerView的显示问题(如果RecyclerView的高度没法计算,你是看不到内容的)。

当然,网上已经有类似的提问和解决方案了。

四种方案解决ScrollView嵌套ListView问题

ListView的情况还比较好解决,优雅的做法无非写一个类继承ListView,然后重写onMeasure方法。

protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec){

int expandSpec= MeasureSpec.makeMeasureSpec(Integer.MAX_VALUE>> 2, MeasureSpec.AT_MOST);

super.onMeasure(widthMeasureSpec, expandSpec);

ListView可以重写onMeasure解决,RecyclerView重写这个方法是行不通的。

说到底其实计算高度嘛。有两种方式,一种是动态计算RecycleView,然后设置setLayoutParams;另外一种跟ListView的解决方式类似,定义一个类继承LinearLayoutManager或GridLayoutManager(注意:可不是继承RecyclerView),重写onMeasure方法(此方法比较麻烦,此处不表,下次写一篇文章再作介绍)。

int heightPx= DensityUtil.dip2px(getActivity(),(imageHeight+ imageRowHeight)* lines);

MarginLayoutParams mParams= new MarginLayoutParams(LayoutParams.MATCH_PARENT, heightPx);

mParams.setMargins(0, 0, 0, 0);

LinearLayout.LayoutParams lParams= new LinearLayout.LayoutParams(mParams);

honorImageRecyclerView.setLayoutParams(lParams);

思路是这样的:服务端返回荣誉图片后,由于是3列显示的方式,只需要计算需要显示几行,然后给定行间距和图片的高度,再设置setLayoutParams就行了。

int lines=(int) Math.ceil(totalImages/ 3d);

至此,这个奇怪的需求得到了解决。

可是在滑动的时候,感觉出现卡顿的现象。聪明的你肯定想到是滑动冲突了。应该是ScrollView的滑动干扰到了ListView的滑动。怎么办呢?能不能禁掉ScrollView的滑动?

百度一下,你肯定能搜索到答案的。先上代码:

public class CustomScrollView extends ScrollView{

public CustomScrollView(Context context){

public CustomScrollView(Context context, AttributeSet attrs){

public CustomScrollView(Context context, AttributeSet attrs, int defStyleAttr){

super(context, attrs, defStyleAttr);

touchSlop= ViewConfiguration.get(context).getScaledTouchSlop();

public boolean onInterceptTouchEvent(MotionEvent e){

case MotionEvent.ACTION_DOWN:

case MotionEvent.ACTION_MOVE:

if(Math.abs(moveY- downY)> touchSlop){

return super.onInterceptTouchEvent(e);

只要理解了getScaledTouchSlop()这个方法就好办了。这个方法的注释是:Distance in pixels a touch can wander before we think the user is scrolling。说这是一个距离,表示滑动的时候,手的移动要大于这个距离才开始移动控件,如果小于此距离就不触发移动。

但是还有另外一个问题:我每次加载这个界面花的时间太长了,每次由其它界面启动这个界面时,都要卡上1~2秒,而且因手机性能时间不等。并不是由于网络请求,取数据由子线程做,跟UI线程毫无关系。这样的体验自己看了都很不爽。

几天过去了,还是那样。马上要给老板演示了。这样的体验要被骂十次呀。

好吧,那我重构代码。不用ScrollView了。直接用一个ListView,然后add一个headerView存放其它内容。因为控件封装得还算好,没改多少布局就OK了,一运行,流畅顺滑,一切迎刃而解!

本来就是这么简单的问题,为什么非得用ScrollView嵌套呢?

stackoverflow早就告诉你了,不要这样嵌套!不要这样嵌套!不要这样嵌套!重要的事情说三遍。

ListView inside ScrollView is not scrolling on Android

当然,从android 5.0 Lollipop开始提供了一种新的API支持嵌入滑动,此时,让像这样的需求也能很好实现。

此处给一个网址,大家有兴趣自行了解,此处不再讨论。

7、EmojiconTextView的setText(null)

这是开源表情库com.rockerhieu.emojicon中的TextView加强版。相信很多人用到过这个开源工具包。TextView用setText(null)完全没问题。但EmojiconTextView setText(null)后就悲剧了,直接crash,显示的是null pointer。开始我怀疑时这个view没初始化,但并不是。那就调试一下呗。

public void setText(CharSequence text, BufferType type){

SpannableStringBuilder builder= new SpannableStringBuilder(text);

EmojiconHandler.addEmojis(getContext(), builder, mEmojiconSize);

super.setText(builder, type);

EmojiconTextView中的setText看来没什么问题。点SpannableStringBuilder进去看看,源码原来是这样的:

* Create a new SpannableStringBuilder containing a copy of the

* specified text, including its spans if any.

public SpannableStringBuilder(CharSequence text){

this(text, 0, text.length());

好吧。问题已经找到了,text.length(),不空指针才怪。

SpannableStringBuilder builder= new SpannableStringBuilder(text);

三、如何设定android studio 版本

如何设定android studio版本

TextView的属性:

Android:autoLink设定是否当文字为URL连结/email/电话号码/map时,文字显示为可点选的连结。可选值(none/web/email/phone/map/all)

android:autoText如果设定,将自动执行输入值的拼写纠正。此处无效果,在显示输入法并输入的时候起作用。

android:bufferType指定getText()方式取得的文字类别。选项editable类似于StringBuilder可追加字元,也就是说getText后可呼叫append方法设定文字内容。spannable则可在给定的字元区域使用样式,参见这里1、这里2。

android:capitalize设定英文字母大写型别。此处无效果,需要弹出输入法才能看得到,参见EditView此属性说明。

android:cursorVisible设定游标为显示/隐藏,预设显示。

android:digits设定允许输入哪些字元。如“1234567890.+-*/%()”

android:drawableBottom在text的下方输出一个drawable,如图片。如果指定一个颜色的话会把text的背景设为该颜色,并且同时和background使用时覆盖后者。

android:drawableLeft在text的左边输出一个drawable,如图片。

android:drawablePadding设定text与drawable(图片)的间隔,与drawableLeft、 drawableRight、drawableTop、drawableBottom一起使用,可设定为负数,单独使用没有效果。

android:drawableRight在text的右边输出一个drawable。

android:drawableTop在text的正上方输出一个drawable。

android:editable设定是否可编辑。

android:editorExtras设定文字的额外的输入资料。

android:ellipsize设定当文字过长时,该控制元件该如何显示。有如下值设定:”start”—?省略号显示在开头;”end”——省略号显示在结尾;”middle”—-省略号显示在中间;”marquee”——以跑马灯的方式显示(动画横向移动)

android:freezesText设定储存文字的内容以及游标的位置。

android:gravity设定文字位置,如设定成“center”,文字将居中显示。

android:hintText为空时显示的文字提示资讯,可通过textColorHint设定提示资讯的颜色。此属性在 EditView中使用,但是这里也可以用。

android:imeOptions附加功能,设定右下角IME动作与编辑框相关的动作,如actionDone右下角将显示一个“完成”,而不设定预设是一个回车符号。这个在EditView中再详细说明,此处无用。

android:imeActionId设定IME动作ID。

android:imeActionLabel设定IME动作标签。

android:includeFontPadding设定文字是否包含顶部和底部额外空白,预设为true。

android:inputMethod为文字指定输入法,需要完全限定名(完整的包名)。例如:.google.android.inputmethod.pinyin,但是这里报错找不到。

android:inputType设定文字的型别,用于帮助输入法显示合适的键盘型别。在EditView中再详细说明,这里无效果。

android:linksClickable设定连结是否点选连线,即使设定了autoLink。

android:marqueeRepeatLimit在ellipsize指定marquee的情况下,设定重复滚动的次数,当设定为 marquee_forever时表示无限次。

android:ems设定TextView的宽度为N个字元的宽度。这里测试为一个汉字字元宽度

android:maxEms设定TextView的宽度为最长为N个字元的宽度。与ems同时使用时覆盖ems选项。

android:minEms设定TextView的宽度为最短为N个字元的宽度。与ems同时使用时覆盖ems选项。

android:maxLength限制显示的文字长度,超出部分不显示。

android:lines设定文字的行数,设定两行就显示两行,即使第二行没有资料。

android:maxLines设定文字的最大显示行数,与width或者layout_width结合使用,超出部分自动换行,超出行数将不显示。

android:minLines设定文字的最小行数,与lines类似。

android:lineSpacingExtra设定行间距。

android:lineSpacingMultiplier设定行间距的倍数。如”1.2”

android:numeric如果被设定,该TextView有一个数字输入法。此处无用,设定后唯一效果是TextView有点选效果,此属性在EdtiView将详细说明。

android:password以小点”.”显示文字

android:phoneNumber设定为电话号码的输入方式。

android:privateImeOptions设定输入法选项,此处无用,在EditText将进一步讨论。

android:scrollHorizontally设定文字超出TextView的宽度的情况下,是否出现横拉条。

android:selectAllOnFocus如果文字是可选择的,让他获取焦点而不是将游标移动为文字的开始位置或者末尾位置。 TextView中设定后无效果。

android:shadowColor指定文字阴影的颜色,需要与shadowRadius一起使用。

android:shadowDx设定阴影横向座标开始位置。

android:shadowDy设定阴影纵向座标开始位置。

android:shadowRadius设定阴影的半径。设定为0.1就变成字型的颜色了,一般设定为3.0的效果比较好。

android:singleLine设定单行显示。如果和layout_width一起使用,当文字不能全部显示时,后面用“…”来表示。如android:text="test_ singleLine"

android:singleLine="true" android:layout_width="20dp"将只显示“t…”。如果不设定singleLine或者设定为false,文字将自动换行

android:textAppearance设定文字外观。如“?android:attr/textAppearanceLargeInverse”这里引用的是系统自带的一个外观,?表示系统是否有这种外观,否则使用预设的外观。可设定的值如下:textAppearanceButton/textAppearanceInverse/textAppearanceLarge/textAppearanceLargeInverse/textAppearanceMedium/textAppearanceMediumInverse/textAppearanceSmall/textAppearanceSmallInverse

android:textColor设定文字颜色

android:textColorHighlight被选中文字的底色,预设为蓝色

android:textColorHint设定提示资讯文字的颜色,预设为灰色。与hint一起使用。

android:textColorLink文字连结的颜色.

android:textScaleX设定文字之间间隔,预设为1.0f。

android:textSize设定文字大小,推荐度量单位”sp”,如”15sp”

android:textStyle设定字形[bold(粗体) 0, italic(斜体) 1, bolditalic(又粗又斜) 2]可以设定一个或多个,用“|”隔开

android:typeface设定文字字型,必须是以下常量值之一:normal 0, sans 1, serif 2, monospace(等宽字型) 3]

android:height设定文字区域的高度,支援度量单位:px(画素)/dp/sp/in/mm(毫米)

android:maxHeight设定文字区域的最大高度

android:minHeight设定文字区域的最小高度

android:width设定文字区域的宽度,支援度量单位:px(画素)/dp/sp/in/mm(毫米),与layout_width的区别看这里。

android:maxWidth设定文字区域的最大宽度

android:minWidth设定文字区域的最小宽度

安装Android Studio的准备工作 1.下载好JDK去官网上找一个下载下来 2.安装JDK.并配置环境变数.安装过程可以一直下一步,无脑操作 3.下载Android Studio的安装包去官网上找一个,下载下来(jdk的环境变数一定要配置)准备工作完成之后,就可以开始我们的安装了 1.安装Android studio也是无脑操作,一直点下一步。直到安装结束 2.安装好之后,我们要新建我们的专案。重点从这里开始点选 New Project会出现.我们设定好名称,也可以无脑操作,一直点选next,直至结束,不过这个过程需要我们耐心的等待。(时间有点漫长)。然后我们就会进入我们的开发介面。点选图片中红圈的图示(SDK manager)会出现下图这个步骤是安装Android的sdk,推荐,Android1.6~Android4.4.2全部安装。(这个耗费时间挺长的,请在网速良好且大量闲暇时光下安装,安装过程中可以看部电影)安装完之后,就开始配置我们的avd(Android Virtual Device),也就是Android的虚拟环境。点选,图片中红圈的图示点选New(Test是我配置好的)随便设定AVD name,建议如图设定设定完成之后,就可以点选下图的三角号编译我们的程式了。

一、修改Android Studio(以下简称AS)的记忆体配置因为在汇入原始码时需要消耗大量记忆体,所以先修改IDEA_HOME/bin/studio.vmoptions中-Xms和-Xmx的值。文件中使用的是748m,可自行修改。二、配置AS的JDK、SDK在IDE中新增一个没有classpath的JDK,这样可以确保使用原始码里的库档案并将其作为要使用的SDK的Java SDK。如下图三、生成汇入AS所需配置...一、修改Android Studio(以下简称AS)的记忆体配置

因为在汇入原始码时需要消耗大量记忆体,所以先修改IDEA_HOME/bin/studio.vmoptions中-Xms和-Xmx的值。文件中使用的是748m,可自行修改。

在IDE中新增一个没有classpath的JDK,这样可以确保使用原始码里的库档案

并将其作为要使用的SDK的Java SDK。如下图

三、生成汇入AS所需配置档案(*.ipr)

①编译原始码(为了确保生成了.java档案,如R.java;如果编译过,则无需再次编译)

②检查out/host/linux-x86/framework/目录下是否有idegen.jar

mmm development/tools/idegen/

在5.0.1的原始码中会生成res.java的资料夹,导致idegen.jar执行时抛FileNotFoundException,这是idegen的程式码不够严谨造成的。

我的分享里有修改这个bug的patch,或者直接使用我分享的idegen.jar。

development/tools/idegen/idegen.sh

这时会在原始码的根目录下生成android.ipr和android.iml两个IntelliJ IDEA(AS是基于IntelliJ IDEA社群版开发的)的配置档案

④在AS中开启原始码根目录下新生成的android.ipr全部