需求:android 实现富文本编辑器,并且实现html解析和生成。
功能点:
- 字体加粗,斜体,下划线,删除线
- 字体设置大小 默认大(18px),中(16px),小(14px)
- 字体设置颜色
- 换行插入图片
- 编辑内容生成html
- 解析html并且显示
主要实现方式
- EditText + Span 的实现方式
- WebView + JavaScript 的实现方式
webview方式存在兼容性问题,所以还是得走原生路线。EditText + Span
知识准备
span是设置 EditText 内容效果的 对象,是内容表达的载体;span派生类有StyleSpan(加粗斜体),UnderlineSpan(下划线),StrikethroughSpan(删除线)等等。
Android中各种Span的用法
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.startend){
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。
加粗斜体效果
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);
}
插入图片效果
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("");
}
if (style[j] instanceof ImageSpan) {
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("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看看效果
copy出来在W3School上看显示效果
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标签处理。
总体效果图
已上传github,喜欢的朋友,可以收藏给个心;
地址
Github : https://github.com/awarmisland
CSDN博客:https://blog.csdn.net/ljzdyh