如何通过Spannable等方式在EditText光标位置插入多图并存入SQLite
问题根因
原有代码无法显示图片的核心问题有3个:
setSpan传入的起止位置都是0,ImageSpan没有绑定到实际长度大于0的占位字符上,系统不会触发图片绘制逻辑- 插入逻辑没有获取当前光标位置,直接调用append无法实现光标处插入的需求
- 没有给Drawable设置
bounds属性,图片没有明确的绘制宽高,即使Span绑定正确也无法正常渲染
光标位置插入多图的实现代码
首先定义全局常量:
// 富文本内嵌对象专用占位符 private static final String IMAGE_PLACEHOLDER = "\uFFFC"; private static final int REQUEST_PICK_IMAGE = 1;
在onActivityResult中使用以下逻辑插入图片,自动适配输入框宽度、避免OOM、支持光标位置插入:
@Override protected void onActivityResult(int requestCode, int resultCode, @Nullable Intent data) { super.onActivityResult(requestCode, resultCode, data); if (requestCode == REQUEST_PICK_IMAGE && resultCode == RESULT_OK && data != null) { Uri imageUri = data.getData(); if (imageUri == null) return; try { // 先读取图片尺寸,不直接加载全量Bitmap BitmapFactory.Options options = new BitmapFactory.Options(); options.inJustDecodeBounds = true; InputStream boundStream = getContentResolver().openInputStream(imageUri); BitmapFactory.decodeStream(boundStream, null, options); if (boundStream != null) boundStream.close(); // 计算缩放比例,适配EditText可用宽度 int targetWidth = editText.getWidth() - editText.getPaddingLeft() - editText.getPaddingRight(); int inSampleSize = 1; if (options.outWidth > targetWidth) { inSampleSize = (int) Math.ceil(options.outWidth * 1.0 / targetWidth); } // 按采样率加载压缩后的Bitmap options.inJustDecodeBounds = false; options.inSampleSize = inSampleSize; InputStream bitmapStream = getContentResolver().openInputStream(imageUri); Bitmap bitmap = BitmapFactory.decodeStream(bitmapStream, null, options); if (bitmapStream != null) bitmapStream.close(); if (bitmap == null) return; // 配置Drawable,必须设置bounds BitmapDrawable drawable = new BitmapDrawable(getResources(), bitmap); int drawableHeight = (int) (targetWidth * 1.0 * drawable.getIntrinsicHeight() / drawable.getIntrinsicWidth()); drawable.setBounds(0, 0, targetWidth, drawableHeight); // 绑定ImageSpan到占位符 ImageSpan imageSpan = new ImageSpan(drawable, ImageSpan.ALIGN_BOTTOM); SpannableString spanStr = new SpannableString(IMAGE_PLACEHOLDER); spanStr.setSpan(imageSpan, 0, 1, Spannable.SPAN_EXCLUSIVE_EXCLUSIVE); // 插入到当前光标位置 int cursorPos = editText.getSelectionStart(); if (cursorPos < 0) cursorPos = editText.getText().length(); editText.getText().insert(cursorPos, spanStr); // 插入图片后追加换行,方便后续输入文字 editText.getText().insert(cursorPos + 1, "\n"); } catch (Exception e) { e.printStackTrace(); } } }
混合内容存入SQLite方案
不要直接把Bitmap或Spannable对象存入数据库,通用方案是图片存应用私有目录、数据库只存带自定义标记的文本,存/取时做双向转换:
- 存储时:遍历EditText的Spannable内容,把所有ImageSpan替换为自定义标记(格式如
[img]图片本地存储路径[/img]),最终把纯标记文本存入数据库 - 读取时:从数据库取出标记文本,正则匹配所有图片标记,逐个替换为绑定了对应本地图片的ImageSpan,再设置回EditText
存储转换代码
private String convertEditableToDbText(Spannable editable) { StringBuilder sb = new StringBuilder(editable.toString()); ImageSpan[] spans = editable.getSpans(0, editable.length(), ImageSpan.class); // 倒序处理Span,避免字符串替换导致的位置索引偏移 for (int i = spans.length - 1; i >= 0; i--) { ImageSpan span = spans[i]; int start = editable.getSpanStart(span); int end = editable.getSpanEnd(span); // 取出Bitmap存到本地,返回文件存储路径(自行实现即可,文件名用时间戳避免重名) BitmapDrawable drawable = (BitmapDrawable) span.getDrawable(); String imagePath = saveBitmapToAppDir(drawable.getBitmap()); // 替换占位符为自定义标记 sb.replace(start, end, "[img]" + imagePath + "[/img]"); } return sb.toString(); }
读取还原代码
private void renderTextFromDb(String dbContent) { SpannableStringBuilder ssb = new SpannableStringBuilder(dbContent); Pattern imgPattern = Pattern.compile("\\[img\\](.*?)\\[/img\\]"); Matcher matcher = imgPattern.matcher(ssb); while (matcher.find()) { String imgPath = matcher.group(1); int start = matcher.start(); int end = matcher.end(); // 加载本地图片做适配 Bitmap bitmap = BitmapFactory.decodeFile(imgPath); if (bitmap == null) continue; BitmapDrawable drawable = new BitmapDrawable(getResources(), bitmap); int targetWidth = editText.getWidth() - editText.getPaddingLeft() - editText.getPaddingRight(); int drawableHeight = (int) (targetWidth * 1.0 * drawable.getIntrinsicHeight() / drawable.getIntrinsicWidth()); drawable.setBounds(0, 0, targetWidth, drawableHeight); ImageSpan span = new ImageSpan(drawable, ImageSpan.ALIGN_BOTTOM); // 替换标记为占位符,绑定Span ssb.replace(start, end, IMAGE_PLACEHOLDER); ssb.setSpan(span, start, start + 1, Spannable.SPAN_EXCLUSIVE_EXCLUSIVE); } editText.setText(ssb); }
注意事项
- 图片必须做采样压缩,否则加载高分辨率原图极易触发OOM
- 不要把图片二进制数据直接存入SQLite的Blob字段,会严重影响数据库性能,甚至引发ANR
- 遍历Span做字符串替换时必须倒序处理,否则会出现图片位置错位
- 上述逻辑天然支持图文混排,可在多张图片之间任意插入文字,不需要额外适配
内容的提问来源于stack exchange,提问作者MS Moshiur
相关产品推荐
相关产品推荐

