本文示例项目地址
对于 Android 开发者,部分场景下需要实现图文混排的排版方式(即在一个UI 控件中同时显示图片与文字),较为常见的场景如社交类 App 中对文字与表情的展示。
实现图文混排,主要有以下方法:
- 使用
WebView
加载HTML
- 使用
Html.fromHtml(String source, int flags)
获取Spanned
对象后,通过TextView
展示 - 使用
ImageSpan
展示图片
实际上方法 2 与方法 3 是相似的,而方法 3 对于不熟悉 HTML
的 Android 开发者更为友好并且提供了更高的自由度。本文主要分析方法 3 的基本使用与自定义绘制。
基本使用
ImageSpan
是 DynamicDrawableSpan
的直接子类,开发者通过 SpannableString/SpannableStringBuilder
的 setSpan()
方法将字符串的指定部分设置为由 ImageSpan
构造方法传入的图片。ImageSpan
的基本使用可以参考如下代码,完整代码可以查看示例项目的 BasicImageSpanActivity
类。
Drawable fuDrawable = getResources().getDrawable(R.drawable.image_fu);
fuDrawable.setBounds(0, 0, textView.getLineHeight(),textView.getLineHeight());
ImageSpan imageSpan = new ImageSpan(fuDrawable);
spannableString.setSpan(imageSpan, start, end, Spanned.SPAN_INCLUSIVE_INCLUSIVE);
textView.setText(spannableString);
对齐方式
图片与文字的位置关系需要通过设置 ImageSpan
的对齐方式来实现。通过在 ImageSpan
的构造方法中传入 verticalAlignment
参数可以实现对图片与文字的纵向对齐方式的设置。ImageSpan
为开发者提供了两种对齐方式:
ALIGN_BOTTOM
:图片底部与所在行底部对齐。ALIGN_BASELINE
:图片底部与文字基线对齐。文字基线(baseline)是字体排印学的概念之一,其具体含义可以参考 维基百科的基线词条。同时,开发者可以将其简单的理解为文字的“重心”位置。
自定义绘制
对于图文混排,不少场景需要图片与文字居中对齐(即图片的中线与文字的中线重合),而 ImageSpan
并未提供这样的对齐方式。开发者可以通过重写(Override
) ImageSpan
父类 DynamicDrawableSpan
的 draw
方法来实现自己的绘制逻辑,从而实现图片与文字的居中对齐。DynamicDrawableSpan
的 draw
方法的实现如下:
@Overridepublic void draw(Canvas canvas, CharSequence text,int start, int end, float x, int top, int y, int bottom, Paint paint) {Drawable b = getCachedDrawable();canvas.save();int transY = bottom - b.getBounds().bottom;if (mVerticalAlignment == ALIGN_BASELINE) {transY -= paint.getFontMetricsInt().descent;}canvas.translate(x, transY);b.draw(canvas);canvas.restore();}
方法中 bottom
为图片所在行底部坐标(以 TextView
左上角为原点),DynamicDrawableSpan
根据 mVerticalAlignment
的值,使用 canvas
的 translate
方法,将画布移动对应的距离(ALIGN_BOTTOM
移动 bottom
与 drawable
底部差值的距离,ALIGN_BASELINE
在前者的基础上减去 descent
值,descent
为基线到行底部的距离),来实现对应的对齐方式。开发者可以参考 DynamicDrawSpan
的实现方式,来实现居中对齐乃至更多的绘制逻辑。居中对齐的实现代码可以参考如下代码,完整代码可以参考示例项目中 CustomImageSpan
的 draw
方法。
@Overridepublic void draw(Canvas canvas, CharSequence text, int start, int end,float x, int top, int y, int bottom, Paint paint) {Drawable b = getDrawable();Paint.FontMetricsInt fm = paint.getFontMetricsInt();int transY = (y + fm.descent + y + fm.ascent) / 2 - (b.getBounds().bottom + b.getBounds().top) / 2;canvas.save();canvas.translate(x, transY);b.draw(canvas);canvas.restore();}
代码中 y
为基线的纵坐标(以 TextView
左上角为坐标原点),fm.descent
为基线至文字底部的距离(为正值),fm.ascent
为基线至文字顶部的距离(为负值),y + fm.descent
的值为文字底部纵坐标,y + fm.ascent
的值为文字顶部纵坐标,二者相加除以2则得到了文字纵向中点的纵坐标,(b.getBounds().bottom + b.getBounds().top) / 2
则为 drawable 绘制区域纵向中点的纵坐标,而二者的差值即为实现居中对齐画布的纵向偏移。
为验证代码的效果,笔者使用一个高16、宽48的黑色(#000000
)色块作为传入 ImageSpan
的 drawable
,展示了在不同对齐方式下图文混排的效果,该示例的完整代码可以参考示例项目的 CustomImageSpanActivity
类,代码的运行效果如下图:
为了方便读者对比不同对齐方式,截图中开启了开发者选项中的显示布局边界 。
由于开发者可以在 DynamicDrawableSpan
的 draw
方法中实现自己的绘制逻辑,使用该方案来实现图文混排给予了开发者极大的自由度,开发者可以对与文字混排的图片进行更为细致的排版,本文仅以较为常见居中对齐作为示例,相信读者可以实现更多更好的图文混排绘制逻辑。