以下为原生实现 如果你的富文本编辑更复杂可转看
最新demo源码点击查看
前言
显示富文本是非常容易的,你可以使用span指定位置,html格式,甚至是一个容器组合多个view多种方式。搜索一下很快实现。
编辑富文本并再现则需要用Edittext或者仿类似模式,则比较复杂一点。
html显示很方便,但自定义编辑则比较麻烦。如点击事件,span则自定义很方便。但再现出数据比较麻烦,并加载。
本文是两者结合使用,span是可以直接转为html的,-最开始我就不知道这一点,导致不断的解析span,浪费了大量的时间
public String getHtmlData() {
return Html.toHtml(getText());
}
因为能用网页表达清楚数据后,则通过正则便捷获取便签值方便再现。为什么还要加上自定义标签呢?是方便拓展功能,比如你Edittext还想插入语音并且传到服务器到其他手机上效果再现出来。语音条你可以用图片去代替。点击事件则可以通过自定义标签里面参合其他数据,给图片赋予其他的含义则可以做很多事情。
自定义便签肯定只有自己可以识别,如果没有自定义标签,则生成的数据,可以丢到txt文本改后缀html。就能显示。非常方便生成网页。
注意事项,实际操作中我们需要的是网络图片传输。所以无论是编辑还是再现,得先加载出图片,再生成view数据。
目前放出核心源码,给正在做富文本编辑并再现,且自定义比较高的同学一点灵感,具体讲解等我编辑好直接能依赖的源码,再更新此文。
如果你的富文本编辑自定义并不高,可搜索webview实现的
richeditor-android
但自定义可行性比较低,比如图片的大小怎么自定义调整,点击事件如何添加并再现,如何控制光标。
也有仿markdown编辑器的,但我们的用户只会傻瓜输入,都没有满足我的需求,才着手自己处理
需求是,类似简书或者石墨文档类的编辑器。
主要内容:
1.加粗和插入网络图片原理
2.数据的获取与再现
3.自定义html tag的解析
setText(Html.fromHtml(content,
null, new HtmlNewTagHandler((Activity) getContext(), httpDraws))
先以加粗和插入网络图片为例子。文章借助了glide加载网络图片
/**
* 插入一个在线图片
*
* @param imgPath
*/
public void insertImage(final String imgPath) {
insertImage(imgPath, null);
}
/**
* @param imgPath 图片地址
* @param clickableSpan 点击事件
*/
public void insertImage(final String imgPath, final IClickableSpan clickableSpan) {
is2ndChanged = true;
getText().append("\n");
Glide.with(getContext()).asDrawable().load(imgPath).into(new SimpleTarget<Drawable>() {
@Override
public void onResourceReady(Drawable drawable, Transition<? super Drawable> transition) {
//图片过大的话 此处进行压缩
float bili = drawable.getIntrinsicHeight() * 1.0f / drawable.getIntrinsicWidth();
int height = (int) (getWidth() * bili);
/**
* 1.getText.toString可以得到图片对应部分会显示
* 2.删除图片会删除这一串
* 3.做表情的话可以用这个比较方便
*
*/
String fromat_imgPath = "Custom text messages for ‘getText.toString’"; // edittext内部记录
SpannableString spannableString = new SpannableString(fromat_imgPath);
drawable.setBounds(0, 0, getWidth() / 2, height / 2);
/**
* 1.drawable实际显示图片
* 2.imgPath是内部记录 span.getSource() 或者转html的时候src可以得到
*/
ImageSpan span = new ImageSpan(drawable, imgPath);
is2ndChanged = true;
spannableString.setSpan(span, 0, fromat_imgPath.length(), Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
// 注释4.1效果前后都不包括 例如word编辑,粗体后面继续打字 还是粗体
// 注释4.2 但中间是包括的也就是一串粗体中间插入新的文字
if (clickableSpan != null) {
spannableString.setSpan(clickableSpan, 0, fromat_imgPath.length(), Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
}
int start = getSelectionStart();//
getText().insert(start, spannableString);
setMovementMethod(LinkMovementMethod.getInstance());
//按照需求加一行换行
setSelection(getText().length());
getText().append("\n");
setSelection(getText().length());
//让点击事件生效
setFocusable(true);
setFocusableInTouchMode(true);
requestFocus();//获取焦点 光标出现
}
});
}
public abstract class IClickableSpan extends ClickableSpan {
private String data;
public IClickableSpan(String data) {
this.data = data;
}
@Override
public void onClick(View widget) {
onCustomClick(widget, data);
}
public abstract void onCustomClick(View widget, String data);
@Override
public void updateDrawState(@NonNull TextPaint ds) {
super.updateDrawState(ds);
ds.setColor(Color.RED);
ds.setUnderlineText(false);
ds.clearShadowLayer();
}
加粗(加前景色 背景色 原理类似)
addTextChangedListener(new TextWatcher() {
private CharSequence beforeTextChangedStr = "";
private CharSequence onTextChangedStr = "";
@Override
public void beforeTextChanged(CharSequence s, int start, int count, int after) {
beforeTextChangedStr = s;
}
@Override
public void onTextChanged(CharSequence s, int start, int before, int count) {
if (is2ndChanged) {//处理文本处理中导致的onTextChanged
is2ndChanged = false;
return;
}
//如果before大于0则代表删除的内容
if (count > 0) {//count部分即为新增的str 此时已经添加到了et里面
CharSequence charSequence = s.subSequence(start, start + count);
if (isBold) {//如果加粗模式开启
SpannableString spannableString = new SpannableString(charSequence);
/**
* SPAN_EXCLUSIVE_EXCLUSIVE(前后都不包括)
* Spanned.SPAN_INCLUSIVE_INCLUSIVE:在开始或结尾处处插入新内容时,会与原来的SpannableString混合在一起,组成一个新的SpannableString
*/
spannableString.setSpan(new StyleSpan(android.graphics.Typeface.BOLD),
0, charSequence.length(), Spanned.SPAN_EXCLUSIVE_EXCLUSIVE); //加粗模式下输入的文字 均加粗
//后续功能:加个颜色等功能
//spannableString.setSpan(new ForegroundColorSpan(Color.BLUE), 0, 1, Spannable.SPAN_EXCLUSIVE_EXCLUSIVE);
// if (clickableSpan != null) {
// spannableString.setSpan(clickableSpan, 0, charSequence.length(), Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
// }
int pStart = getSelectionStart();//获取光标位置
needChange();//阻止再次onTextChanged
getText().delete(pStart - count, pStart);//删除输入未加粗数据
needChange();
int pStart2 = getSelectionStart();//获取光标位置
getText().insert(pStart2, spannableString);//插入加粗后的数据
}
}
if (before > 0) {//往前删除
//系统默认处理不会影响span
}
}
@Override
public void afterTextChanged(Editable s) {
}
});
自定义标签的解析和普通标签数据的获取
java获取html标签数据
/**
* 获取指定HTML标签的指定属性的值 * @param source 要匹配的源文本 * @param element 标签名称 * @param attr 标签的属性名称 * @return 属性值列表
*/
public static List<String> match(String source, String element, String attr) {
List<String> result = new ArrayList<String>();
String reg = "<" + element + "[^<>]*?\\s" + attr + "=['\"]?(.*?)['\"]?(\\s.*?)?>";
Matcher m = Pattern.compile(reg).matcher(source);
while (m.find()) {
String r = m.group(1);
result.add(r);
}
return result;
}
package rex.richetlibrary;
import android.app.Activity;
import android.content.Context;
import android.graphics.drawable.Drawable;
import android.text.Editable;
import android.text.Html;
import android.text.Spannable;
import android.text.style.ImageSpan;
import android.util.DisplayMetrics;
import android.view.WindowManager;
import android.widget.Toast;
import org.xml.sax.XMLReader;
import java.lang.reflect.Field;
import java.util.HashMap;
import java.util.Map;
/**
* Created by Rex on 2018/5/17.
* 对html网页标签的自定义处理
custom_img
* 源码依据 Html 846行 *
package rex.richetlibrary;
import android.app.Activity;
import android.content.Context;
import android.graphics.drawable.Drawable;
import android.text.Editable;
import android.text.Html;
import android.text.Spannable;
import android.text.style.ImageSpan;
import android.util.DisplayMetrics;
import android.view.WindowManager;
import android.widget.Toast;
import org.xml.sax.XMLReader;
import java.lang.reflect.Field;
import java.util.HashMap;
import java.util.Map;
/**
* Created by Rex on 2018/5/17.
* 对html网页标签的自定义处理
* 源码依据 Html 846行
*/
public class HtmlNewTagHandler implements Html.TagHandler {
public static final String TAG_NEW_IMG = "custom_img";
private int startIndex = 0;
private int stopIndex = 0;
final HashMap<String, String> attributes = new HashMap<String, String>();
private Activity mContext;
private Map<String, Drawable> httpDraws;
public HtmlNewTagHandler(Activity context, Map<String, Drawable> httpDraws) {
mContext = context;
this.httpDraws = httpDraws;
}
@Override
public void handleTag(boolean opening, String tag, Editable mSpannableStringBuilder, XMLReader mReader) {
processAttributes(mReader);
//该类型只有start处理
//<img src="http://www.1honeywan.com/dachshund/image/7.21/7.21_3_thumb.JPG">
if (tag.equalsIgnoreCase(TAG_NEW_IMG)) {//<>
if (opening) {
startHttpImg(mSpannableStringBuilder);
}
} else {//</>
}
}
public int getScreenWidth(Context context) {
WindowManager wm = (WindowManager) context
.getSystemService(Context.WINDOW_SERVICE);
DisplayMetrics outMetrics = new DisplayMetrics();
wm.getDefaultDisplay().getMetrics(outMetrics);
return outMetrics.widthPixels;
}
public void startHttpImg(Editable text) {
String src = attributes.get("src");
String url = src;
int screenWidth = getScreenWidth(mContext) / 2;//占屏幕一半 根据需求调整
Drawable d = httpDraws.get(url);
if (d == null) {
Toast.makeText(mContext, "图片预加载失败", Toast.LENGTH_SHORT).show();
}
float bili = d.getIntrinsicWidth() * 1.0f / d.getIntrinsicHeight();
int w = screenWidth;
int h = (int) (screenWidth / bili);
d.setBounds(0, 0, w, h);
int len = text.length();
text.append("\uFFFC");
text.setSpan(new ImageSpan(d, src), len, text.length(),
Spannable.SPAN_EXCLUSIVE_EXCLUSIVE);
}
private void processAttributes(final XMLReader xmlReader) {
try {
Field elementField = xmlReader.getClass().getDeclaredField("theNewElement");
elementField.setAccessible(true);
Object element = elementField.get(xmlReader);
Field attsField = element.getClass().getDeclaredField("theAtts");
attsField.setAccessible(true);
Object atts = attsField.get(element);
Field dataField = atts.getClass().getDeclaredField("data");
dataField.setAccessible(true);
String[] data = (String[]) dataField.get(atts);
Field lengthField = atts.getClass().getDeclaredField("length");
lengthField.setAccessible(true);
int len = (Integer) lengthField.get(atts);
for (int i = 0; i < len; i++) {
attributes.put(data[i * 5 + 1], data[i * 5 + 4]);
}
} catch (Exception e) {
}
}
}