我们在做项目的时候会遇到很多地方需要使用图文混排的功能。
特别是聊天、论坛和贴吧之类的。
这里我主要以EditText这个控件来说,TextView同样适用。因为EditText上实现图文混排比Textview上要复杂,且所实现的功能Textview上同样适用。
这里利用了正则表达式和Spannable来进行图文混排。
1,BiaoQingData 添加图片资源,预备使用
/**
* 将资源类设置成单例,全局只需要一个对象即可
*/
private BiaoQingData() {
for (int i = 0; i < emojis.length; i++) {
biaoqingMap.put(Pattern.compile(Pattern.quote(emojis[i])), icons[i]);
}
}
private static class BiaoQingDatas {
private static BiaoQingData biaoqingUtil = new BiaoQingData();
}
public static BiaoQingData getInstance() {
return BiaoQingDatas.biaoqingUtil;
}
/**
* 表情集合
*/
public final Map<Pattern, Object> biaoqingMap = new HashMap<>();
//表情资源对应的id
public final String ee_1 = "[ee_1]";
public final String ee_2 = "[ee_2]";
public final String ee_3 = "[ee_3]";
public final String ee_4 = "[ee_4]";
public final String ee_5 = "[ee_5]";
public final String ee_6 = "[ee_6]";
public final String ee_7 = "[ee_7]";
public final String ee_8 = "[ee_8]";
public final String ee_9 = "[ee_9]";
public final String ee_10 = "[ee_10]";
//存放资源对应id的数组
public String[] emojis = new String[]{
ee_1,
ee_2,
ee_3,
ee_4,
ee_5,
ee_6,
ee_7,
ee_8,
ee_9,
ee_10,
};
//存放资源的数组
public int[] icons = new int[]{
R.drawable.ee_1,
R.drawable.ee_2,
R.drawable.ee_3,
R.drawable.ee_4,
R.drawable.ee_5,
R.drawable.ee_6,
R.drawable.ee_7,
R.drawable.ee_8,
R.drawable.ee_9,
R.drawable.ee_10,
};
2,EditTexts 自定义EditView,监听复制、粘贴动作中的表情替换
/**
* 重写EditText的监听事件,目的是为了监听粘贴
*/
public class EditTexts extends EditText {
private boolean isCopy;//是否是复制的文本
public EditTexts(Context context) {
super(context);
}
public EditTexts(Context context, AttributeSet attrs) {
super(context, attrs);
}
public boolean isCopy() {
return isCopy;
}
public void setCopy(boolean copy) {
isCopy = copy;
}
@Override
public boolean onTextContextMenuItem(int id) {
/**
* id:16908319
全选
id:16908328
选择
id:16908320
剪贴
id:16908321
复制
id:16908322
粘贴
id:16908324
输入法
**/
if (id == 16908322) {
setCopy(true);
}
return super.onTextContextMenuItem(id);
}
}
3,BiaoqingUtil 最关键的适配表情类
public class BiaoqingUtil {
/**
* Pattern.compile(String str);//编译正则表达式,在此过程中创建一个新的模式实例。
* Pattern.quote(String str);//引用给定字符串使用" \ Q "和" \ E”,这样所有其它元字符失去他们的特殊意义。
*/
private BiaoqingUtil() {
}
private static class BiaoqingUtils {
private static BiaoqingUtil biaoqingUtil = new BiaoqingUtil();
}
public static BiaoqingUtil getInstance() {
return BiaoqingUtils.biaoqingUtil;
}
/**
* 仅作参考,不是方法,不可调用
*
* @param context
* @param message
* @param editText
*/
private void getTextStr(Context context, String message, EditText editText) {
Spannable span = showBiaoqing(context, message);
// 设置内容
editText.setText(span, TextView.BufferType.SPANNABLE);
}
/**
* 适配所有表情
*
* @param context
* @param text
* @return 标记出所有的表情符号
*/
public Spannable showBiaoqing(Context context, CharSequence text) {
//获得工厂类的SpannableString对象
Spannable spannable = Spannable.Factory.getInstance().newSpannable(text);
addSmiles(context, spannable);
return spannable;
}
/**
* 添加表情
*
* @param context
* @return
*/
private void addBiaoqing(Context context, EditText editText, int drawableId, String biaoqingKey) {
SpannableString spannableString = new SpannableString(biaoqingKey);
//获取Drawable资源
Drawable drawable = context.getResources().getDrawable(drawableId);
//这句话必须,不然图片不显示
drawable.setBounds(0, 0, drawable.getIntrinsicWidth(), drawable.getIntrinsicHeight());
//创建ImageSpan
ImageSpan span = new ImageSpan(drawable, ImageSpan.ALIGN_BASELINE);
//用ImageSpan替换文本
spannableString.setSpan(span, 0, spannableString.length(), Spannable.SPAN_EXCLUSIVE_EXCLUSIVE);
Editable e = editText.getText();
int st = editText.getSelectionStart();
int en = editText.getSelectionEnd();
e.replace(st, en, spannableString);
}
/**
* 删除表情
*/
public void removeBiaoqing(EditText editText) {
int selectionStart = editText.getSelectionStart();// 获取光标的位置
if (selectionStart > 0) {
String body = editText.getText().toString();
if (!TextUtils.isEmpty(body)) {
String tempStr = body.substring(0, selectionStart);
if (tempStr.contains("[")) {//说明是资源表情
int i = tempStr.lastIndexOf("[");// 获取最后一个表情的位置
if (i != -1) {
String cs = tempStr.substring(i, selectionStart);
if (cs.indexOf("[ee_") != -1) {// 判断是否是一个表情
editText.getEditableText().delete(i, selectionStart);
return;
}
}
editText.getEditableText().delete(tempStr.length() - 1, selectionStart);
} else if (tempStr.contains("file:///")) {//说明是本地文件表情
int i = tempStr.lastIndexOf("file:///");// 获取最后一个表情的位置
if (i != -1) {
String cs = tempStr.substring(i, selectionStart);
if (cs.indexOf(";") != -1) {// 判断是否是一个表情
editText.getEditableText().delete(i, selectionStart);
return;
}
}
editText.getEditableText().delete(tempStr.length() - 1, selectionStart);
} else {
editText.getEditableText().delete(tempStr.length() - 1, selectionStart);
}
}
}
}
/**
* 供外部调用的添加表情的方法
*
* @param context
* @param editText
* @param viewId
*/
public void addBiaoqing(Context context, EditText editText, int viewId) {
String ee = null;
//取出map集合中的键和值
switch (viewId) {
case R.id.ee_del:
//这个图片是一个删除按钮,按下这个图片并不是为了添加图片,而是为了删除光标前一个图片
BiaoqingUtil.getInstance().removeBiaoqing(editText);
break;
case R.id.ee_1:
ee = BiaoQingData.getInstance().ee_1;
break;
case R.id.ee_2:
ee = BiaoQingData.getInstance().ee_2;
break;
case R.id.ee_3:
ee = BiaoQingData.getInstance().ee_3;
break;
case R.id.ee_4:
ee = BiaoQingData.getInstance().ee_4;
break;
case R.id.ee_5:
ee = BiaoQingData.getInstance().ee_5;
break;
case R.id.ee_6:
ee = BiaoQingData.getInstance().ee_6;
break;
case R.id.ee_7:
ee = BiaoQingData.getInstance().ee_7;
break;
case R.id.ee_8:
ee = BiaoQingData.getInstance().ee_8;
break;
case R.id.ee_9:
ee = BiaoQingData.getInstance().ee_9;
break;
case R.id.ee_10:
ee = BiaoQingData.getInstance().ee_10;
break;
}
if (!TextUtils.isEmpty(ee)) {
for (Map.Entry<Pattern, Object> entry : BiaoQingData.getInstance().biaoqingMap.entrySet()) {
Matcher matcher = entry.getKey().matcher(ee);
if (matcher.find()) {
addBiaoqing(context, editText, (Integer) entry.getValue(), ee);
}
}
}
}
/**
* 用表情代替现有spannable
* replace existing spannable with smiles
*
* @param context
* @param spannable
* @return
*/
private void addSmiles(Context context, Spannable spannable) {
//取出map集合中的键和值,map的键是正则表达式对象
for (Map.Entry<Pattern, Object> entry : BiaoQingData.getInstance().biaoqingMap.entrySet()) {
//解析正则表达式,这里拿到的是这个表情的具体正则表达式,例如[ee_00]代表笑脸,则用户输入的文字中是否出现了[ee_00],如果出现,则替换成表情
Matcher matcher = entry.getKey().matcher(spannable);
while (matcher.find()) {
boolean set = true;
/**将工厂类的Spannable确定为imageSpan类型,并且截取span以正则表达式“[xxx]”为规定*/
for (ImageSpan span : spannable.getSpans(matcher.start(), matcher.end(), ImageSpan.class)) {
/**如果在规定的正则表达式“[xxx]”中,从matcher.start开始到结束属于这个正则表达式则说明该字符串是一个表情*/
if (spannable.getSpanStart(span) >= matcher.start() && spannable.getSpanEnd(span) <= matcher.end()) {
//如果是表情,则从这个sannable中删除这段字符串
spannable.removeSpan(span);
} else {
set = false;
}
}
//如果spannbale中包含表情,则替换表情
if (set) {
Object value = entry.getValue();
//判断value是不是来自本地sd卡(String为绝对路径)并且不是来自网络
if (value instanceof String && !((String) value).startsWith("http")) {
String filePath = value.toString();
filePath = filePath.substring(filePath.indexOf("file:///"), filePath.length() - 1);
File file = new File(filePath);
//如果文件路径不是文件夹并且存在则替换表情
if (file.exists() && !file.isDirectory()) {
spannable.setSpan(new ImageSpan(context, Uri.fromFile(file)),
matcher.start(), matcher.end(),
Spannable.SPAN_EXCLUSIVE_EXCLUSIVE);
}
} else {
//如果表情不来自本地文件,则适配资源表情,资源文件是int型
spannable.setSpan(new ImageSpan(context, (int) value), matcher.start(), matcher.end(), Spannable.SPAN_EXCLUSIVE_EXCLUSIVE);
}
}
}
}
}
}
这个工具类中,我已经将注释写的很清楚了,大家可以根据注释来理解代码;这个图文混排的代码我是参照了环信sdk的图文混排来加以优化的。
外界只需要调用方法:
showBiaoqing(Context context, CharSequence text);//将整段文字传入,适配这段文字中的所有表情。
addBiaoqing(Context context, EditText editText, int viewId);//添加表情,传入上下文,控件,以及资源id就可以了
removeBiaoqing(EditText editText);//清除表情,根据用户所点击的光标的位置来删除,传入控件即可
注意:
其中除了资源文件的适配以外还加入了sd卡资源适配。sd卡资源适配,有些地方需要加以修改。
如果你读懂了这个图文混排,相信很容易就能晚上sd卡资源适配。
需要注意的是,就连qq也没有适配sd卡图片的图文混排,所以这个有一定难度,但是是可以做出来的。