我们在做项目时,可能会用到图文混排功能,例如做日记app,github上有很多RichText(富文本编辑器),但是看来看去都不能满足自己的要求,因为图片地址也要随着文本一起上传到服务器,以便于下次展示,用户使用时可以用TextView浏览,也可以使用EditText编辑。
下面就说说我的实现方法:
主要使用到Android SDK带的Html类的两个方法
public static android.text.Spanned fromHtml(java.lang.String source,
Html.ImageGetter imageGetter,
Html.TagHandler tagHandler)
和
public static java.lang.String toHtml(android.text.Spanned text)
fromHtml是将图文混排文本(也就是带有html标签的文本)转为TextView和EditText可以识别的图文用于展示,toHtml是将TextView和EditText编辑好的图文转为图文混排文本,再进行保存等下一步操作。两个方法参数具体含义不明白的自己去了解。
例如这样一个文本
"您消耗的总热量约等于4杯" + "<img src='http://calendar.bj.bcebos.com/img/icon_shwnl.png'/>" + "\n+5只" + "<img src='" + R.mipmap.ic_launcher + "'/>" + "+10个" + "<img src='" + R.mipmap.ic_launcher + "'/>"这里的文本包括图片内容都是用img标签来包含,上面的fromHtml方法中的ImageGetter参数,是一个接口,实现这个接口后,需要实现它唯一的方法
public Drawable getDrawable(String source),source参数就是每个img标签src里面的内容,可以根据source中的内容来分别处理不同情况。
下面附上ImageGetter的代码
ImageGetter.java
package com.example.zhouwenpeng.myapplication.widgets;
import android.content.Context;
import android.graphics.Bitmap;
import android.graphics.drawable.BitmapDrawable;
import android.graphics.drawable.Drawable;
import android.text.Html;
import android.text.Spanned;
import android.view.View;
import android.widget.TextView;
import com.nostra13.universalimageloader.core.DisplayImageOptions;
import com.nostra13.universalimageloader.core.ImageLoader;
import com.nostra13.universalimageloader.core.ImageLoaderConfiguration;
import com.nostra13.universalimageloader.core.assist.FailReason;
import com.nostra13.universalimageloader.core.listener.ImageLoadingListener;
import com.nostra13.universalimageloader.utils.MemoryCacheUtils;
import java.io.File;
import java.util.List;
/**
* Created by 周文鹏 on 16/7/11.
*
*/
public class ImageGetter implements Html.ImageGetter {
private TextView textView;
private Context context;
private ImageLoader imageLoader;
public ImageGetter(TextView textView) {
this.textView = textView;
this.context = textView.getContext();
imageLoader = ImageLoader.getInstance();
if (!imageLoader.isInited()) {
DisplayImageOptions opts = new DisplayImageOptions.Builder().cacheOnDisk(true).cacheInMemory(true).build();
ImageLoaderConfiguration config = new ImageLoaderConfiguration.Builder(context)
.defaultDisplayImageOptions(opts).build();
imageLoader.init(config);
}
}
@Override
public Drawable getDrawable(String source) {
Drawable drawable = null;
if (source.startsWith("http")) {
List<Bitmap> bitmaps = MemoryCacheUtils.findCachedBitmapsForImageUri(source, imageLoader.getMemoryCache());
if (bitmaps.size() > 0) {
Bitmap bitmap = bitmaps.get(0);
drawable = new BitmapDrawable(context.getResources(), bitmap);
} else {
File file = imageLoader.getDiskCache().get(source);
if (file != null) {
drawable = Drawable.createFromPath(file.getAbsolutePath());
}
}
if (drawable == null) {
imageLoader.loadImage(source, new ImageLoadingListener() {
@Override
public void onLoadingStarted(String imageUri, View view) {
}
@Override
public void onLoadingFailed(String imageUri, View view, FailReason failReason) {
}
@Override
public void onLoadingComplete(String imageUri, View view, Bitmap loadedImage) {
CharSequence text = textView.getText();
if (text instanceof Spanned) {
String html = Html.toHtml((Spanned) text);
textView.setText(html);
} else {
textView.setText(text);
}
}
@Override
public void onLoadingCancelled(String imageUri, View view) {
}
});
}
} else if (isNumeric(source)) {
int id = Integer.parseInt(source);
drawable = context.getResources().getDrawable(id);
} else {
drawable = Drawable.createFromPath(source);
}
if (drawable != null) {
drawable.setBounds(0, 0, drawable.getIntrinsicWidth(), drawable.getIntrinsicHeight());
}
return drawable;
}
// 加入ZPUtil
public static boolean isNumeric(String str) {
for (int i = str.length(); --i >= 0; ) {
if (!Character.isDigit(str.charAt(i))) {
return false;
}
}
return true;
}
}
这里主要有三种情况,图片是http网络上的图片,图片是本地文件中的图片,和图片是项目中的资源文件。这里用到了UniversalImageLoader这个库,是github的开源库,需要自行导入。
ImageGetter只是个工具,主要给TextView和EditText使用,下面附上TextView和EditText代码
ZPMixedTextView.java
package com.example.zhouwenpeng.myapplication.widgets;
import android.content.Context;
import android.text.Html;
import android.text.Spanned;
import android.util.AttributeSet;
import android.widget.TextView;
/**
* Created by 周文鹏 on 16/7/11.
* 图文混排
*/
public class ZPMixedTextView extends TextView {
public ZPMixedTextView(Context context) {
this(context, null);
}
public ZPMixedTextView(Context context, AttributeSet attrs) {
this(context, attrs, 0);
}
public ZPMixedTextView(Context context, AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
}
@Override
public void setText(CharSequence text, BufferType type) {
if (text instanceof Spanned) {
super.setText(text, type);
} else {
Spanned spanned = Html.fromHtml(text.toString(), new ImageGetter(this), null);
super.setText(spanned, type);
}
}
public CharSequence getText2() {
CharSequence text = super.getText();
if (text instanceof Spanned) {
String html = Html.toHtml((Spanned) text);
html = html.replaceFirst("<p dir=\"ltr\">", "");
html = replaceLast(html, "</p>", "");
return html;
} else {
return text;
}
}
@Override
public void append(CharSequence text, int start, int end) {
if (text instanceof Spanned) {
super.append(text, start, end);
} else {
Spanned spanned = Html.fromHtml(text.toString(), new ImageGetter(this), null);
super.append(spanned, start, spanned.length());
}
}
// 放在ZPUtil中
public static String replaceLast(String text, String regex, String replacement) {
return text.replaceFirst("(?s)" + regex + "(?!.*?" + regex + ")", replacement);
}
}
package com.example.zhouwenpeng.myapplication.widgets;
import android.content.Context;
import android.text.Editable;
import android.text.Html;
import android.text.SpannableStringBuilder;
import android.text.Spanned;
import android.text.TextWatcher;
import android.util.AttributeSet;
import android.widget.EditText;
import java.util.ArrayList;
import java.util.List;
/**
* 记忆+图文混排EditText
* @author 周文鹏
*/
public class ZPComplexEditText extends EditText {
private List<EditHistory> undoHistories; // 撤销文字
private List<EditHistory> redoHistories; // 重做文字
private boolean activate = false; // 是否激活redo
private boolean clear = true; // 是否清空redo
private OnTextChangeListener listener; // 文字改变监听器
public ZPComplexEditText(Context context) {
this(context, null);
}
public ZPComplexEditText(Context context, AttributeSet attrs) {
super(context, attrs);
init();
}
public ZPComplexEditText(Context context, AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
init();
}
/**
* 初始化
*/
private void init() {
undoHistories = new ArrayList<>();
redoHistories = new ArrayList<>();
addTextChangedListener(new TextWatcher() {
@Override
public void onTextChanged(CharSequence s, int start, int before, int count) {
if (listener != null) {
listener.onTextChange(ZPComplexEditText.this, s);
}
}
/**
*
* @param s 改变之前的字符串
* @param start 改变的起始位置
* @param count 按键盘back键时删除的长度
* @param after 增加内容时增加的长度
*/
@Override
public void beforeTextChanged(CharSequence s, int start, int count,
int after) {
if (activate) {
redoHistories.add(new EditHistory(new SpannableStringBuilder(s), start, count, after));
activate = false;
} else {
// count和after同时大于0只有输入英文带下划线才会有这种情况,相当于先back,再增加
if (count > 0 && after > 0) {
int position = undoHistories.size() - 1;
if (position >= 0 && undoHistories.get(position).start == start) {
undoHistories.remove(position);
}
count = 0;
}
undoHistories.add(new EditHistory(new SpannableStringBuilder(s), start, count, after));
if (clear) {
redoHistories.clear();
}
clear = true;
}
}
@Override
public void afterTextChanged(Editable s) {
}
});
}
/**
* 撤销
*/
public void undo() {
if (canUndo()) {
activate = true;
Editable editable = getEditableText();
EditHistory history = undoHistories.remove(undoHistories.size() - 1);
if (history.count > 0) {
// 使用insert解决在中间删除后,使用undo会追加到末尾的问题,下同
// editable.append(history.ssb.subSequence(history.start, history.start + history.count));
editable.insert(history.start, history.ssb.subSequence(history.start, history.start + history.count));
}
if (history.after > 0) {
editable.delete(history.start, history.start + history.after);
}
}
}
/**
* 重做
*/
public void redo() {
if (canRedo()) {
clear = false;
Editable editable = getEditableText();
EditHistory history = redoHistories.remove(redoHistories.size() - 1);
if (history.count > 0) {
// editable.append(history.ssb.subSequence(history.start, history.start + history.count));
editable.insert(history.start, history.ssb.subSequence(history.start, history.start + history.count));
}
if (history.after > 0) {
editable.delete(history.start, history.start + history.after);
}
}
}
/**
* 是否可以撤销
*/
public boolean canUndo() {
return undoHistories.size() > 0;
}
/**
* 是否可以重做
*/
public boolean canRedo() {
return redoHistories.size() > 0;
}
/**
* 获取文字改变监听器
*/
public OnTextChangeListener getOnTextChangeListener() {
return listener;
}
/**
* 设置文字改变监听器
*/
public void setOnTextChangeListener(OnTextChangeListener listener) {
this.listener = listener;
}
/**
* 存储Edit操作
* @author 周文鹏
*/
private class EditHistory {
public SpannableStringBuilder ssb;
public int start;
public int count;
public int after;
public EditHistory(SpannableStringBuilder ssb, int start, int count, int after) {
this.ssb = ssb;
this.start = start;
this.count = count;
this.after = after;
}
@Override
public String toString() {
return "EditHistory [text=" + ssb + ", start=" + start
+ ", count=" + count + ", after=" + after + "]";
}
}
/**
* 文字改变监听器
* @author 周文鹏
*/
public interface OnTextChangeListener {
void onTextChange(ZPComplexEditText editText, CharSequence text);
}
// 以下是图文混排
@Override
public void setText(CharSequence text, BufferType type) {
if (text instanceof Spanned) {
super.setText(text, type);
} else {
Spanned spanned = Html.fromHtml(text.toString(), new ImageGetter(this), null);
super.setText(spanned, type);
}
}
public CharSequence getText2() {
String html = Html.toHtml(super.getText());
html = html.replaceFirst("<p dir=\"ltr\">", "");
html = replaceLast(html, "</p>", "");
return html;
}
@Override
public void append(CharSequence text, int start, int end) {
if (text instanceof Spanned) {
super.append(text, start, end);
} else {
Spanned spanned = Html.fromHtml(text.toString(), new ImageGetter(this), null);
super.append(spanned, start, spanned.length());
}
}
// 放在ZPUtil中
public static String replaceLast(String text, String regex, String replacement) {
return text.replaceFirst("(?s)" + regex + "(?!.*?" + regex + ")", replacement);
}
}
上面两个类,EditText明显比TextView代码多很多,是因为EditText增加了undo,redo功能,也就是撤销,重做功能,图文混排主要就是重写了setText和append函数,而getText函数本来也可以重写,但是我在调试时发现,不管是TextView还是EditText在进行文本处理时都频繁调用自己的getText方法,为了性能也为了避免不必要的bug,所以就自己重新写了个getText2,这个方法只有一个用途,就是获取编辑好的带有html标签图文混排文本,注意这里我去掉了最外层的标签,否则下次使用时,如果添加内容会自动带有换行。
getText2获取的文本是这样的
"您消耗的总热量约等于4杯<img src=\"http://calendar.bj.bcebos.com/img/icon_shwnl.png\"> +5只<img src=\"2130903040\">+10个<img src=\"2130903040\">"
中文已经被转为unicode编码,这个不用管,下次直接用Html.fromHtml转换,显示出来的还是一样的,当然你也可以自己转成中文。