原标题:Android 图文混排富文本编辑器实现详解

本文作者

作者:ljzdyh

链接:

https://blog.csdn.net/ljzdyh/article/details/82497625

本文由作者授权发布。

1

概述

需求:

android 实现富文本编辑器,并且实现html解析和生成。

功能点:

字体加粗,斜体,下划线,删除线

字体设置大小 默认大(18px),中(16px),小(14px)

字体设置颜色

换行插入图片

编辑内容生成html

解析html并且显示

主要实现方式

EditText + Span 的实现方式

WebView + Java 的实现方式

webview方式存在兼容性问题,所以还是得走原生路线。EditText + Span。

2

知识储备

span是设置 EditText 内容效果的 对象,是内容表达的载体;span派生类有StyleSpan(加粗斜体),UnderlineSpan(下划线),StrikethroughSpan(删除线)等等。

Android中各种Span的用法

https://blog.csdn.net/qq_16430735/article/details/50427978

Spanable中的常用常量:

Spanned.SPAN_EXCLUSIVE_EXCLUSIVE --- 不包含start和end所在的端点 (a,b)

Spanned.SPAN_EXCLUSIVE_INCLUSIVE --- 不包含端start,但包含end所在的端点 (a,b]

Spanned.SPAN_INCLUSIVE_EXCLUSIVE --- 包含start,但不包含end所在的端点 [a,b)

Spanned.SPAN_INCLUSIVE_INCLUSIVE--- 包含start和end所在的端点 [a,b]

了解了大概之后,就开始写代码;

1.定义FontStyle 字体样式基类,定义初始化Span方法

private void setSpan(FontStyle fontStyle,boolean isSet,Class tClass){

Log.d( "setSpan", "");

int start = getSelectionStart();

int end= getSelectionEnd();

int mode = EXCLUD_INCLUD_MODE;

T[] spans = getEditableText().getSpans(start, end,tClass);

//获取

List spanStyles = getOldFontSytles(spans,fontStyle);

for(SpanPart spanStyle : spanStyles){

if(spanStyle.start

if(start== end){mode=EXCLUD_MODE;}

getEditableText().setSpan(getInitSpan(spanStyle), spanStyle.start,start,mode);

}

if(spanStyle. end> end){

getEditableText().setSpan(getInitSpan(spanStyle), end, spanStyle. end,mode);

}

}

if(isSet){

if(start== end){

mode=INCLUD_INCLUD_MODE;

}

getEditableText().setSpan(getInitSpan(fontStyle),start, end,mode);

}

}

用到的辅助方法:

privateCharacterStyle getInitSpan(FontStyle fontStyle){

if(fontStyle.isBold){

returnnewStyleSpan(Typeface.BOLD);

} elseif(fontStyle.isItalic){

returnnewStyleSpan(Typeface.ITALIC);

} elseif(fontStyle.isUnderline){

returnnewUnderlineSpan();

} elseif(fontStyle.isStreak){

returnnewStrikethroughSpan();

} elseif(fontStyle.fontSize> 0){

returnnewAbsoluteSizeSpan(fontStyle.fontSize, true);

} elseif(fontStyle.color!= 0){

returnnewForegroundColorSpan(fontStyle.color);

}

returnnull;

}

private List getOldFontSytles(T[] spans, FontStyle fontStyle){

List spanStyles = newArrayList<>();

for(T span:spans){

booleanisRemove= false;

if(span instanceofStyleSpan){ //特殊处理 styleSpan

intstyle_type = ((StyleSpan) span).getStyle();

if((fontStyle.isBold&& style_type== Typeface.BOLD)

|| (fontStyle.isItalic&&style_type== Typeface.ITALIC)){

isRemove= true;

}

} else{

isRemove= true;

}

if(isRemove) {

SpanPart spanStyle = newSpanPart(fontStyle);

spanStyle.start = getEditableText().getSpanStart(span);

spanStyle.end = getEditableText().getSpanEnd(span);

if(span instanceofAbsoluteSizeSpan){

spanStyle.fontSize = ((AbsoluteSizeSpan) span).getSize();

} elseif(span instanceofForegroundColorSpan){

spanStyle.color = ((ForegroundColorSpan) span).getForegroundColor();

}

spanStyles.add(spanStyle);

getEditableText().removeSpan(span);

}

}

returnspanStyles;

}

setSpan 是公共设置样式方法,通过fontStyle传参,设置对应的样式,例如设置加粗和斜体:

privatevoidsetStyleSpan(booleanisSet,inttype){

FontStyle fontStyle = newFontStyle();

if(type== Typeface.BOLD){

fontStyle.isBold= true;

} elseif(type== Typeface.ITALIC){

fontStyle.isItalic= true;

}

setSpan(fontStyle,isSet,StyleSpan.class);

}

setSpan处理思路:

获取当前选中位置position,在该位置是否已经设置了 需要处理样式,如 加粗;

如果有,在getOldFontSytles 方法中,会进行判断移除;(因为假如选中位置有加粗,再设置一次就是取消)

span设置样式和 html 类似,是通过始末设tag来控制区间样式的,所以,你选中区间样式CD,可能与原有样式区间AB是包含,交集关系。因此,当你移除旧样式的时候,需要补始末的tag,这样才能保持未选中的区间样式不变。代码getOldFontSytles后for 循环执行补tag 逻辑。

当非选中状态下,即光标移至某处,设置字体样式,随后输入的文字都是当前设置样式,需要判断start =end ,然后变更span设置mode 方式。需要使用SPAN_INCLUSIVE_INCLUSIVE。

加粗斜体效果

2.插入图片

设置图片,需要用到ImageSpan ImageSpan(Context context, Bitmap b) 通过重定义RichImageSpan 继承 ImageSpan 同时重写getSource方法,赋值uri 这样利用Glide管理bitmap,防止内存溢出。(nimgn 是为了让图片占位,可以自行设置别的,没有要求)

publicclassRichImageSpanextendsImageSpan{

privateUri mUri;

publicRichImageSpan(Context context, Bitmap b, Uri uri){

super(context, b);

mUri = uri;

}

@Override

publicString getSource(){

returnmUri.toString();

}

}

/**

* 图片加载

* @parampath

*/

publicvoidimage(String path){

finalUri uri = Uri.parse(path);

finalintmaxWidth = view.getMeasuredWidth() -view. getPaddingLeft() - view.getPaddingRight();

RequestOptions options = newRequestOptions()

.centerCrop()

.placeholder(R.mipmap.ic_launcher)

.error(R.mipmap.ic_launcher);

glideRequests.asBitmap()

.load( newFile(path))

.apply(options)

.into( newSimpleTarget() {

@Override

publicvoidonResourceReady(Bitmap resource, Transition superBitmap> transition){

Bitmap bitmap = zoomBitmapToFixWidth(resource, maxWidth);

image(uri, bitmap);

}

});

}

publicvoidimage(Uri uri, Bitmap pic){

String img_str= "img";

intstart = view.getSelectionStart();

SpannableString ss = newSpannableString( "nimgn");

RichImageSpan myImgSpan = newRichImageSpan(mContext, pic, uri);

ss.setSpan(myImgSpan, 1, img_str.length()+ 1, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);

view.getEditableText().insert(start, ss); // 设置ss要添加的位置

view.requestLayout();

view.requestFocus();

}

插入图片效果

3.span生成html

目前原生 hmtl 能够支持进行html 解析,但是想做定制化的解析,需要对其进行修改。拷贝一份Html.java 为CustomHtml.java;

查看源码得知,html 将span 转化 html 是通过 withinParagraph方法,遍历当前控件样式CharacterStyle 数组,然后根据对应样式,加入对应css 标签(现在主流是style 方式, 目前我只是简单使用了常规html标签做样式控制,可以改)。

部分核心代码如下:

privatestaticvoidwithinParagraph(StringBuilder out, Spanned text, intstart, intend){

intnext;

for( inti = start; i < end; i = next) {

next = text.nextSpanTransition(i, end, CharacterStyle.class);

CharacterStyle[] style = text.getSpans(i, next, CharacterStyle.class);

AbsoluteSizeSpan tmp_rel_span = null;

ForegroundColorSpan tmp_fColor_span = null;

for( intj = 0; j < style.length; j++) {

if(style[j] instanceof StyleSpan) {

ints = ((StyleSpan) style[j]).getStyle();

if((s & Typeface.BOLD) != 0) {

out.append( "");

}

if((s & Typeface.ITALIC) != 0) {

out.append( "");

}

}

if(style[j] instanceof TypefaceSpan) {

String s = ((TypefaceSpan) style[j]).getFamily();

if( "monospace". equals(s)) {

out.append( "");

}

}

if(style[j] instanceof SuperSpan) {

out.append( "");

}

if(style[j] instanceof SubSpan) {

out.append( "");

}

if(style[j] instanceof UnderlineSpan) {

out.append( "");

}

if(style[j] instanceof StrikethroughSpan) {

out.append( "");

}

if(style[j] instanceof URLSpan) {

out.append( "

out.append(((URLSpan) style[j]).getURL());

out.append( "">");

}

if(style[j] instanceof ImageSpan) {

out.append( "

out.append(((ImageSpan) style[j]).getSource());

out.append( "">");

// Don't output the dummy character underlying the image.

i = next;

}

if(style[j] instanceof AbsoluteSizeSpan) {

tmp_rel_span= ((AbsoluteSizeSpan) style[j]);

}

if(style[j] instanceof RelativeSizeSpan) {

floatsizeEm = ((RelativeSizeSpan) style[j]).getSizeChange();

out.append(String.format( "", sizeEm));

}

if(style[j] instanceof ForegroundColorSpan) {

tmp_fColor_span = ((ForegroundColorSpan) style[j]);

}

if(style[j] instanceof BackgroundColorSpan) {

intcolor = ((BackgroundColorSpan) style[j]).getBackgroundColor();

out.append(String.format( "",

0xFFFFFF& color));

}

}

//处理字体 颜色

StringBuilder style_font = newStringBuilder();

if(tmp_fColor_span!= null||tmp_rel_span!= null){

style_font.append( "

}

//颜色

if(tmp_fColor_span!= null){

style_font.append(String.format( "color='#%06X' ", 0xFFFFFF& tmp_fColor_span.getForegroundColor()));

}

//字体

if(tmp_rel_span!= null){

String value= "16px";

if(tmp_rel_span.getSize()== FontStyle.BIG){

value= "18px";

} elseif(tmp_rel_span.getSize()==FontStyle.SMALL){

value= "14px";

}

style_font.append( "style='font-size:"+ value+ ";'");

}

if(style_font.length()> 0){

out.append(style_font+ ">");

}

withinStyle( out, text, i, next);

if(style_font.length()> 0){

out.append( "

");

}

for( intj = style.length - 1; j >= 0; j--) {

if(style[j] instanceof BackgroundColorSpan) {

out.append( "");

}

if(style[j] instanceof RelativeSizeSpan) {

out.append( "");

}

if(style[j] instanceof URLSpan) {

out.append( "

");

}

if(style[j] instanceof StrikethroughSpan) {

out.append( "");

}

if(style[j] instanceof UnderlineSpan) {

out.append( "");

}

if(style[j] instanceof SubSpan) {

out.append( "");

}

if(style[j] instanceof SuperSpan) {

out.append( "");

}

if(style[j] instanceof TypefaceSpan) {

String s = ((TypefaceSpan) style[j]).getFamily();

if(s. equals( "monospace")) {

out.append( "");

}

}

if(style[j] instanceof StyleSpan) {

ints = ((StyleSpan) style[j]).getStyle();

if((s & Typeface.BOLD) != 0) {

out.append( "");

}

if((s & Typeface.ITALIC) != 0) {

out.append( "");

}

}

}

}

}

接下来我们就刚刚gif 输入内容生成html看看效果:

copy出来在W3School上看显示效果:

p.s.图片显示不出,因为路径是手机本地,若需要,应当在转html时,先上传获得图片url,在赋值转html。

4. html 转 span

转换核心在于 CustomHtmlToSpannedConverter类,它通过识别html的标签 然后对应处理 生成span;我主要处理了handleStartTag ,handleEndTag 方法,增加了图片处理通过继承 ImageGetter (网上一般处理方法)重写getDrawable。

privatevoidhandleStartTag(String tag, Attributes attributes){

if(tag.equalsIgnoreCase( "br")) {

// We don't need to handle this. TagSoup will ensure that there's a for each

// so we can safely emit the linebreaks when we handle the close tag.

} elseif(tag.equalsIgnoreCase( "p")) {

startBlockElement(mSpannableStringBuilder, attributes, getMarginParagraph());

startCssStyle(mSpannableStringBuilder, attributes);

} elseif(tag.equalsIgnoreCase( "ul")) {

startBlockElement(mSpannableStringBuilder, attributes, getMarginList());

} elseif(tag.equalsIgnoreCase( "li")) {

startLi(mSpannableStringBuilder, attributes);

} elseif(tag.equalsIgnoreCase( "div")) {

startBlockElement(mSpannableStringBuilder, attributes, getMarginDiv());

} elseif(tag.equalsIgnoreCase( "span")) {

startCssStyle(mSpannableStringBuilder, attributes);

} elseif(tag.equalsIgnoreCase( "strong")) {

start(mSpannableStringBuilder, newBold());

} elseif(tag.equalsIgnoreCase( "b")) {

start(mSpannableStringBuilder, newBold());

} elseif(tag.equalsIgnoreCase( "em")) {

start(mSpannableStringBuilder, newItalic());

} elseif(tag.equalsIgnoreCase( "cite")) {

start(mSpannableStringBuilder, newItalic());

} elseif(tag.equalsIgnoreCase( "dfn")) {

start(mSpannableStringBuilder, newItalic());

} elseif(tag.equalsIgnoreCase( "i")) {

start(mSpannableStringBuilder, newItalic());

} elseif(tag.equalsIgnoreCase( "big")) {

start(mSpannableStringBuilder, newBig());

} elseif(tag.equalsIgnoreCase( "small")) {

start(mSpannableStringBuilder, newSmall());

} elseif(tag.equalsIgnoreCase( "font")) {

startFont(mSpannableStringBuilder, attributes);

} elseif(tag.equalsIgnoreCase( "blockquote")) {

startBlockquote(mSpannableStringBuilder, attributes);

} elseif(tag.equalsIgnoreCase( "tt")) {

start(mSpannableStringBuilder, newMonospace());

} elseif(tag.equalsIgnoreCase( "a")) {

startA(mSpannableStringBuilder, attributes);

} elseif(tag.equalsIgnoreCase( "u")) {

start(mSpannableStringBuilder, newUnderline());

} elseif(tag.equalsIgnoreCase( "del")) {

start(mSpannableStringBuilder, newStrikethrough());

} elseif(tag.equalsIgnoreCase( "s")) {

start(mSpannableStringBuilder, newStrikethrough());

} elseif(tag.equalsIgnoreCase( "strike")) {

start(mSpannableStringBuilder, newStrikethrough());

} elseif(tag.equalsIgnoreCase( "sup")) {

start(mSpannableStringBuilder, newSuper());

} elseif(tag.equalsIgnoreCase( "sub")) {

start(mSpannableStringBuilder, newSub());

} elseif(tag.length() == 2&&

Character.toLowerCase(tag.charAt( 0)) == 'h'&&

tag.charAt( 1) >= '1'&& tag.charAt( 1) <= '6') {

startHeading(mSpannableStringBuilder, attributes, tag.charAt( 1) - '1');

} elseif(tag.equalsIgnoreCase( "img")) {

startImg(mSpannableStringBuilder, attributes, mImageGetter);

} elseif(mTagHandler != null) {

mTagHandler.handleTag( true, tag, mSpannableStringBuilder, mReader);

}

}

如上代码所示,可以根据自己定义的协议,修改对应tag标签处理。

总体效果图:

已上传github,喜欢的朋友,可以收藏给个心;

https://github.com/awarmisland/RichEditText

项目非常有学习意义,尤其是对 Span 的实战。

不过,对于一些简单的图文混排可以考虑使用自定义的方式,但是如果考虑三端统一,尤其是支持 PC 上编辑文章,移动端显示的,最好的方式还是去使用webview,主要是 PC 上的编辑器会插入非常多复杂的 html 标签,非常难解析。当然了 webview 自带很多兼容性问题,选择开源项目,一定要提前查看issue,避免最后踩坑,我之前就遇到过类似囧境(低版本无法删除img标签,最终通过调用 js 删除img)。返回搜狐,查看更多

责任编辑:

setspan字体加粗_Android 图文混排富文本编辑器实现详解相关推荐

  1. android富文本图片自适应,Android 图片混排富文本编辑器控件

    一.一个Android 图片混排富文本编辑器控件(仿兴趣部落) 1.1 图片混排富文本控件 是一种图片和文字混合在一起的控件,文本之间可以插入图片,类似于网页的排版样式. 1.2 该控件主要是仿兴趣部 ...

  2. Android 图片混排富文本编辑器控件

    概述 一个Android 图片混排富文本编辑器控件(仿兴趣部落) 详细 代码下载:http://www.demodashi.com/demo/12032.html 一.一个Android 图片混排富文 ...

  3. RichEditeText——android图文混排富文本文章编辑器实现详解

    需求:android 实现富文本编辑器,并且实现html解析和生成. 功能点: 字体加粗,斜体,下划线,删除线 字体设置大小   默认大(18px),中(16px),小(14px) 字体设置颜色 换行 ...

  4. android 实现表格横向混动_Android图文混排实现方式详解

    在使用TextView的时候,我们经常需要在TextView中进行图文混排,比如在QQ中聊天的消息中的表情,底部tab图标等. 一.场景 二.实现方式 Android官方对TextView的图文混排提 ...

  5. android span字体修改,Android TextView使用SpannableString设置复合文本的方法详解

    本文实例讲述了Android TextView使用SpannableString设置复合文本的方法.分享给大家供大家参考,具体如下: TextView通常用来显示普通文本,但是有时候需要对其中某些文本 ...

  6. 本地Word图文直接复制到富文本编辑器中

    当前功能基于PHP,其它语言流程大致相同 1.新增上传word json配置 在ueditor\php\config.json中新增如下配置: /* 上传word配置 */ "wordAct ...

  7. html 把文字显示控制,控制字体加粗显示的html标签是哪个

    控制字体加粗显示的html标签是哪个 发布时间:2021-06-09 09:27:30 来源:亿速云 阅读:88 作者:小新 这篇文章主要介绍了控制字体加粗显示的html标签是哪个,具有一定借鉴价值, ...

  8. 强大的图文长微博工具 支持图文混排 支持美工字体

    功能:一键将写好的博客文章,同时发布到博客和微博,在微博上,自动生成图文长微博,一键发布.免费下载地址:http://www.juziseo.com/ .效果如下,支持长微博图文混排和美工字体,以及字 ...

  9. textView 的设置文本中某一文字的字体颜色以及图文混排

    ios7 新增的TextKit 技术 从书上看到的-- 首先需要实例化3个对象 NSTextStorage //存储字符的相关属性 包括颜色等 NSLayoutManager //将字符变化反应到Ns ...

  10. NGUI-制作位图字体以及图文混排

    制作字体过程 首先得下载一个位图制作工具Bitmap font generator,可以点击这里下载 1.新建txt文件,输入字体里面包含的文字 2.保存为utf-8格式:点击文件另存为,选择编码格式 ...

最新文章

  1. 震惊 Guava 竟然有坑
  2. 奖励名单表格模板_员工出勤工薪记算表(行政人事模板)
  3. C++读写EXCEL文件方式比较 .
  4. 客户端代码压缩成zip和服务器开启gzip
  5. 生活中回归分析实际例子_回归分析中R方和调整R方的区别
  6. mysql自主增长键_mysql中关于自增长主键的获取
  7. BZOJ 4520: [Cqoi2016]K远点对
  8. javaCRC8计算的坑
  9. 【Quant】80+面试,5个offer,Quant大神总结分享各家quant面试题
  10. SpringBoot整合EasyExcel实现Excel表格的导出功能
  11. 关于大疆mini一代如何手动拍摄合成全景图
  12. 最新2022中国大学排名发布!
  13. aix系统日志转存日志服务器,AIX查看系统日志
  14. echarts+高德地图设置卫星图层
  15. Golang程序调试 -- 内存泄漏pprof工具
  16. 十大高人气“断货王”蓝牙耳机盘点,双11哪款蓝牙耳机值得入手?
  17. 大数据常见面试题 Hadoop篇(2)
  18. Apache实现盗链与防盗链与隐藏版本信息
  19. android 防止屏幕误碰,小米11带来硬件防误触解决方案,彻底解决曲面屏误触问题...
  20. 7-4 有理数加法 (15 分)

热门文章

  1. 1月3日 接触ROS
  2. Java 学习/面试指南
  3. Windows 10 超过Windows 7成为最受欢迎的操作系统
  4. redis 系列16 持久化 RDB
  5. SQL Server 和 HSQLDB 中使用 merge into 完成 saveOrUpdate
  6. Linux系统Web网站目录和文件安全权限设置
  7. Yii框架官方指南系列53——专题:使用命令行生成代码(已废弃)
  8. 安装ffmpeg及nginx模块
  9. CentOS+Subversion 配置Linux 下 SVN服务器
  10. 梅花雨的日历控件在ASP.NET2.0下不可用的解决方法