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

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

功能点:

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

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

字体设置颜色

换行插入图片

编辑内容生成html

解析html并且显示

主要实现方式

EditText + Span 的实现方式

WebView + JavaScript 的实现方式

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

知识准备

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

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方法

/**

* 返回 初始化 span

* @param fontStyle

* @return

*/

private CharacterStyle getInitSpan(FontStyle fontStyle){

if(fontStyle.isBold){

return new StyleSpan(Typeface.BOLD);

}else if(fontStyle.isItalic){

return new StyleSpan(Typeface.ITALIC);

}else if(fontStyle.isUnderline){

return new UnderlineSpan();

}else if(fontStyle.isStreak){

return new StrikethroughSpan();

}else if(fontStyle.fontSize>0){

return new AbsoluteSizeSpan(fontStyle.fontSize,true);

}else if(fontStyle.color!=0){

return new ForegroundColorSpan(fontStyle.color);

}

return null;

}

/**

* 通用set Span

* @param fontStyle

* @param isSet

* @param tClass

* @param

*/

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);

}

}

/**

* 获取当前 选中 spans

* @param spans

* @param fontStyle

* @param

* @return

*/

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

List spanStyles = new ArrayList<>();

for(T span:spans){

boolean isRemove=false;

if(span instanceof StyleSpan){//特殊处理 styleSpan

int style_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 = new SpanPart(fontStyle);

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

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

if(span instanceof AbsoluteSizeSpan){

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

}else if(span instanceof ForegroundColorSpan){

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

}

spanStyles.add(spanStyle);

getEditableText().removeSpan(span);

}

}

return spanStyles;

}

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

/**

* bold italic

* @param isSet

* @param type

*/

private void setStyleSpan(boolean isSet,int type){

FontStyle fontStyle = new FontStyle();

if(type== Typeface.BOLD){

fontStyle.isBold=true;

}else if(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。

加粗斜体效果

8fd4b10d5f4b

jiacu.gif

2.插入图片

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

public class RichImageSpan extends ImageSpan {

private Uri mUri;

public RichImageSpan(Context context, Bitmap b, Uri uri) {

super(context, b);

mUri = uri;

}

@Override

public String getSource() {

return mUri.toString();

}

}

/**

* 图片加载

* @param path

*/

public void image(String path) {

final Uri uri = Uri.parse(path);

final int maxWidth = view.getMeasuredWidth() -view. getPaddingLeft() - view.getPaddingRight();

RequestOptions options = new RequestOptions()

.centerCrop()

.placeholder(R.mipmap.ic_launcher)

.error(R.mipmap.ic_launcher);

glideRequests.asBitmap()

.load(new File(path))

.apply(options)

.into(new SimpleTarget() {

@Override

public void onResourceReady(Bitmap resource, Transition super Bitmap> transition) {

Bitmap bitmap = zoomBitmapToFixWidth(resource, maxWidth);

image(uri, bitmap);

}

});

}

public void image(Uri uri, Bitmap pic) {

String img_str="img";

int start = view.getSelectionStart();

SpannableString ss = new SpannableString("\nimg\n");

RichImageSpan myImgSpan = new RichImageSpan(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();

// setClick(ss.getSpanStart(myImgSpan),ss.getSpanEnd(myImgSpan),img_str);

}

插入图片效果

8fd4b10d5f4b

tupian.gif

3.span生成html

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

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

部分核心代码如下

private static void withinParagraph(StringBuilder out, Spanned text, int start, int end) {

int next;

for (int i = 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 (int j = 0; j < style.length; j++) {

if (style[j] instanceof StyleSpan) {

int s = ((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 SuperscriptSpan) {

out.append("");

}

if (style[j] instanceof SubscriptSpan) {

out.append("");

}

if (style[j] instanceof UnderlineSpan) {

out.append("");

}

if (style[j] instanceof StrikethroughSpan) {

// out.append("");

out.append(" ");

}

if (style[j] instanceof URLSpan) {

out.append("

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

out.append("\">");

}

if (style[j] instanceof ImageSpan) {

out.append("p

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]);

// AbsoluteSizeSpan s = ((AbsoluteSizeSpan) style[j]);

// float sizeDip = s.getSize();

// if (!s.getDip()) {

// Application application = CustomApplication.currentApplication();

// sizeDip /= application.getResources().getDisplayMetrics().density;

// }

//

// // px in CSS is the equivalance of dip in Android

// out.append(String.format("", sizeDip));

}

if (style[j] instanceof RelativeSizeSpan) {

float sizeEm = ((RelativeSizeSpan) style[j]).getSizeChange();

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

}

if (style[j] instanceof ForegroundColorSpan) {

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

// int color = ((ForegroundColorSpan) style[j]).getForegroundColor();

// out.append(String.format("", 0xFFFFFF & color));

}

if (style[j] instanceof BackgroundColorSpan) {

int color = ((BackgroundColorSpan) style[j]).getBackgroundColor();

out.append(String.format("",

0xFFFFFF & color));

}

}

//处理字体 颜色

StringBuilder style_font = new StringBuilder();

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";

}else if(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 (int j = style.length - 1; j >= 0; j--) {

if (style[j] instanceof BackgroundColorSpan) {

out.append("");

}

if (style[j] instanceof ForegroundColorSpan) {

// out.append("");

}

if (style[j] instanceof RelativeSizeSpan) {

out.append("");

}

if (style[j] instanceof AbsoluteSizeSpan) {

// out.append("");

}

if (style[j] instanceof URLSpan) {

out.append("

");

}

if (style[j] instanceof StrikethroughSpan) {

// out.append("");

out.append("");

}

if (style[j] instanceof UnderlineSpan) {

out.append("");

}

if (style[j] instanceof SubscriptSpan) {

out.append("");

}

if (style[j] instanceof SuperscriptSpan) {

out.append("");

}

if (style[j] instanceof TypefaceSpan) {

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

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

out.append("");

}

}

if (style[j] instanceof StyleSpan) {

int s = ((StyleSpan) style[j]).getStyle();

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

out.append("");

}

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

out.append("");

}

}

}

}

}

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

8fd4b10d5f4b

html_con.png

copy出来在W3School上看显示效果

8fd4b10d5f4b

QQ截图20180912224109.png

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

html 转 span

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

private void handleStartTag(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.

} else if (tag.equalsIgnoreCase("p")) {

startBlockElement(mSpannableStringBuilder, attributes, getMarginParagraph());

startCssStyle(mSpannableStringBuilder, attributes);

} else if (tag.equalsIgnoreCase("ul")) {

startBlockElement(mSpannableStringBuilder, attributes, getMarginList());

} else if (tag.equalsIgnoreCase("li")) {

startLi(mSpannableStringBuilder, attributes);

} else if (tag.equalsIgnoreCase("div")) {

startBlockElement(mSpannableStringBuilder, attributes, getMarginDiv());

} else if (tag.equalsIgnoreCase("span")) {

startCssStyle(mSpannableStringBuilder, attributes);

} else if (tag.equalsIgnoreCase("strong")) {

start(mSpannableStringBuilder, new Bold());

} else if (tag.equalsIgnoreCase("b")) {

start(mSpannableStringBuilder, new Bold());

} else if (tag.equalsIgnoreCase("em")) {

start(mSpannableStringBuilder, new Italic());

} else if (tag.equalsIgnoreCase("cite")) {

start(mSpannableStringBuilder, new Italic());

} else if (tag.equalsIgnoreCase("dfn")) {

start(mSpannableStringBuilder, new Italic());

} else if (tag.equalsIgnoreCase("i")) {

start(mSpannableStringBuilder, new Italic());

} else if (tag.equalsIgnoreCase("big")) {

start(mSpannableStringBuilder, new Big());

} else if (tag.equalsIgnoreCase("small")) {

start(mSpannableStringBuilder, new Small());

} else if (tag.equalsIgnoreCase("font")) {

startFont(mSpannableStringBuilder, attributes);

} else if (tag.equalsIgnoreCase("blockquote")) {

startBlockquote(mSpannableStringBuilder, attributes);

} else if (tag.equalsIgnoreCase("tt")) {

start(mSpannableStringBuilder, new Monospace());

} else if (tag.equalsIgnoreCase("a")) {

startA(mSpannableStringBuilder, attributes);

} else if (tag.equalsIgnoreCase("u")) {

start(mSpannableStringBuilder, new Underline());

} else if (tag.equalsIgnoreCase("del")) {

start(mSpannableStringBuilder, new Strikethrough());

} else if (tag.equalsIgnoreCase("s")) {

start(mSpannableStringBuilder, new Strikethrough());

} else if (tag.equalsIgnoreCase("strike")) {

start(mSpannableStringBuilder, new Strikethrough());

} else if (tag.equalsIgnoreCase("sup")) {

start(mSpannableStringBuilder, new Super());

} else if (tag.equalsIgnoreCase("sub")) {

start(mSpannableStringBuilder, new Sub());

} else if (tag.length() == 2 &&

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

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

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

} else if (tag.equalsIgnoreCase("img")) {

startImg(mSpannableStringBuilder, attributes, mImageGetter);

} else if (mTagHandler != null) {

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

}

}

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

总体效果图

8fd4b10d5f4b

xunhuan.gif

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

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值