图文混排各种问题。drawableStart / End总是无法满足用法。
如果用drawableStart 是这样的,UI设计稿 盾牌的图片只和第一行对齐。而不是居中对齐。

干脆用ImageSpan直接插入TextView中,自定义VerticalCenterImageSpan。
使用示例和效果图
SpannableStringBuilder sb2 = new SpannableStringBuilder("Your privacy matters. No personally identifiable information will be collected.");
VerticalCenterImageSpan.injectAtStart(mContext, sb2, R.drawable.ic_safe, Util.dpToPx(2));
TextView tvBtnDesc = findViewById(R.id.tv_bottom_desc);
tvBtnDesc.setText(sb2);

如果你只用国内,不做国际化,尤其是RTL适配,下面代码足够。
import android.content.Context;
import android.graphics.Canvas;
import android.graphics.Paint;
import android.graphics.Rect;
import android.graphics.drawable.Drawable;
import android.text.SpannableStringBuilder;
import android.text.Spanned;
import android.text.TextUtils;
import android.text.style.ImageSpan;
import androidx.annotation.DrawableRes;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.annotation.Px;
import androidx.core.content.ContextCompat;
import java.util.ArrayList;
import java.util.List;
import stat.util.ListUtil;
/**
* 垂直居中ImageSpan,仅LTR布局,不支持RTL
*/
public class VerticalCenterImageSpan extends ImageSpan {
public static final String SPACE = "{space}";
private final Rect mMargin = new Rect();
public VerticalCenterImageSpan(@NonNull Drawable drawable) {
super(drawable);
}
public VerticalCenterImageSpan(@NonNull Context context, int resourceId) {
super(context, resourceId);
}
private VerticalCenterImageSpan(@NonNull Context context, @DrawableRes int resourceId, @Px int leftPadding) {
super(context, resourceId);
mMargin.left = leftPadding;
}
private VerticalCenterImageSpan(@NonNull Context context, @DrawableRes int resourceId, @Px int rightPadding) {
super(context, resourceId);
mMargin.right = rightPadding;
}
public VerticalCenterImageSpan(@NonNull Context context, @DrawableRes int resourceId, int width, int height, Rect margin) {
super(ContextCompat.getDrawable(context, resourceId));
if (margin != null) {
mMargin.set(margin);
}
getDrawable().setBounds(0, 0, width, height);
}
@Override
public int getSize(@NonNull Paint paint, CharSequence text, int start, int end, @Nullable Paint.FontMetricsInt fm) {
// 总占位宽度 = 原图宽度 + 左间距 + 右间距
return super.getSize(paint, text, start, end, fm) + mMargin.left + mMargin.right;
}
@Override
public void draw(Canvas canvas, CharSequence text, int start, int end, float x, int top, int y, int bottom, Paint paint) {
final Drawable drawable = getDrawable();
Paint.FontMetricsInt fontMetricsInt = paint.getFontMetricsInt();
// 垂直居中偏移公式不变
int transY = (y + fontMetricsInt.descent + y + fontMetricsInt.ascent) / 2 - drawable.getBounds().bottom / 2;
// LTR 只需要统一向右偏移左margin
canvas.save();
canvas.translate(x + mMargin.left, transY);
drawable.draw(canvas);
canvas.restore();
}
// ====================== 静态注入API(完全兼容原有业务调用) ======================
public static void inject(@NonNull Context context, @NonNull SpannableStringBuilder builder, @DrawableRes int resourceId) {
builder.append(SPACE);
ImageSpan span = new VerticalCenterImageSpan(context, resourceId);
builder.setSpan(span, builder.length() - SPACE.length(), builder.length(), Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
}
// 图标插在文字后方,文字与图标之间的间距 → 左margin
public static void inject(@NonNull Context context,
@NonNull SpannableStringBuilder builder,
@DrawableRes int resourceId,
@Px int drawablePadding) {
builder.append(SPACE);
ImageSpan span = new VerticalCenterImageSpan(context, resourceId, drawablePadding);
builder.setSpan(span, builder.length() - SPACE.length(), builder.length(), Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
}
public static void injectAtStart(@NonNull Context context,
@NonNull SpannableStringBuilder builder,
@DrawableRes int resourceId) {
builder.insert(0, SPACE);
ImageSpan span = new VerticalCenterImageSpan(context, resourceId);
builder.setSpan(span, 0, SPACE.length(), Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
}
// 图标插在文字前方,图标与文字之间的间距 → 右margin
public static void injectAtStart(@NonNull Context context,
@NonNull SpannableStringBuilder builder,
@DrawableRes int resourceId,
@Px int drawablePadding) {
builder.insert(0, SPACE);
ImageSpan span = new VerticalCenterImageSpan(context, resourceId, drawablePadding);
mMargin.right = drawablePadding;
builder.setSpan(span, 0, SPACE.length(), Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
}
public static void inject(@NonNull Context context, @NonNull SpannableStringBuilder builder,
@DrawableRes int resourceId, int width, int height, Rect margin) {
builder.append(SPACE);
ImageSpan span = new VerticalCenterImageSpan(context, resourceId, width, height, margin);
builder.setSpan(span, builder.length() - SPACE.length(), builder.length(), Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
}
public static void inject(@NonNull Context context, @NonNull SpannableStringBuilder builder, @DrawableRes int resourceId, String... strs) {
if (strs == null || strs.length == 0) return;
List<String> list = new ArrayList<>();
for (String str : strs) {
if (!TextUtils.isEmpty(str)) list.add(str);
}
for (int i = 0; i < list.size(); i++) {
builder.append(list.get(i));
if (i != list.size() - 1) inject(context, builder, resourceId);
}
}
public static void inject(@NonNull Context context, @NonNull SpannableStringBuilder builder, @DrawableRes int resourceId, List<String> list) {
ListUtil.removeEmpty(list);
if (ListUtil.isEmpty(list)) return;
for (int i = 0; i < list.size(); i++) {
builder.append(list.get(i));
if (i != list.size() - 1) inject(context, builder, resourceId);
}
}
}
如果需要支持RTL的话用下面代码
/**
* 垂直居中的 ImageSpan。
* <p>
* 带 {@code drawablePadding} 的 inject / injectAtStart 按 layoutDirection 解析间距
* LTR / RTL 均把空隙放在图标与相邻文字之间。
*/
public class VerticalCenterImageSpan extends ImageSpan {
/**
* 占位标记
*/
public static final String SPACE = "{space}";
/**
* 绝对物理边距
*/
private static final int PADDING_ABSOLUTE = 0;
/**
* 图标在逻辑末尾:空隙在「前方文字 ↔ 图标」之间
*/
private static final int PADDING_BEFORE_ICON = 1;
/**
* 图标在逻辑首部:空隙在「图标 ↔ 后方文字」之间
*/
private static final int PADDING_AFTER_ICON = 2;
private final Context mAppContext;
private final Rect mAbsoluteMargin = new Rect();
private int mPaddingMode = PADDING_ABSOLUTE;
@Px
private int mDrawablePadding;
public VerticalCenterImageSpan(@NonNull Drawable drawable) {
super(drawable);
mAppContext = null;
}
public VerticalCenterImageSpan(@NonNull Context context, int resourceId) {
super(context, resourceId);
mAppContext = context.getApplicationContext();
}
private VerticalCenterImageSpan(@NonNull Context context, @DrawableRes int resourceId,
int paddingMode, @Px int drawablePadding) {
super(context, resourceId);
mAppContext = context.getApplicationContext();
mPaddingMode = paddingMode;
mDrawablePadding = Math.max(drawablePadding, 0);
}
public VerticalCenterImageSpan(@NonNull Context context, @DrawableRes int resourceId, int width,
int height, Rect margin) {
super(ContextCompat.getDrawable(context, resourceId));
mAppContext = context.getApplicationContext();
if (margin != null) {
mAbsoluteMargin.set(margin);
}
getDrawable().setBounds(0, 0, width, height);
}
/**
* 相对模式下随 RTL 翻转,保证空隙始终在图标与文字之间。
*/
private int getLeftMargin() {
if (mPaddingMode == PADDING_ABSOLUTE || mDrawablePadding <= 0) {
return mAbsoluteMargin.left;
}
boolean rtl = isRtl(mAppContext);
if (mPaddingMode == PADDING_AFTER_ICON) {
// 首部图标:RTL 左留白
return rtl ? mDrawablePadding : 0;
}
// 末尾图标:LTR 左留白
return rtl ? 0 : mDrawablePadding;
}
private int getRightMargin() {
if (mPaddingMode == PADDING_ABSOLUTE || mDrawablePadding <= 0) {
return mAbsoluteMargin.right;
}
boolean rtl = isRtl(mAppContext);
if (mPaddingMode == PADDING_AFTER_ICON) {
// 首部图标:LTR 右留白
return rtl ? 0 : mDrawablePadding;
}
// 末尾图标:RTL 右留白
return rtl ? mDrawablePadding : 0;
}
@Override
public int getSize(@NonNull Paint paint, CharSequence text, int start, int end,
@Nullable Paint.FontMetricsInt fm) {
return super.getSize(paint, text, start, end, fm) + getLeftMargin() + getRightMargin();
}
@Override
public void draw(Canvas canvas, CharSequence text, int start, int end, float x, int top, int y,
int bottom, Paint paint) {
final Drawable drawable = getDrawable();
Paint.FontMetricsInt fontMetricsInt = paint.getFontMetricsInt();
int transY = (y + fontMetricsInt.descent + y + fontMetricsInt.ascent) / 2 - drawable.getBounds().bottom / 2;
canvas.save();
canvas.translate(x + getLeftMargin(), transY);
drawable.draw(canvas);
canvas.restore();
}
/**
* 将图片注入到SpannableStringBuilder中
*/
public static void inject(@NonNull Context context, @NonNull SpannableStringBuilder builder, @DrawableRes int resourceId) {
builder.append(SPACE);
ImageSpan span = new VerticalCenterImageSpan(context, resourceId);
builder.setSpan(span, builder.length() - SPACE.length(), builder.length(), Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
}
/**
* 将图片注入到SpannableStringBuilder末尾,并设置图片与前方文字的间距(LTR/RTL 自适应)
*
* @param drawablePadding 图片与文字的间距,单位 px
*/
public static void inject(@NonNull Context context,
@NonNull SpannableStringBuilder builder,
@DrawableRes int resourceId,
@Px int drawablePadding) {
builder.append(SPACE);
ImageSpan span = new VerticalCenterImageSpan(
context, resourceId, PADDING_BEFORE_ICON, drawablePadding);
builder.setSpan(span, builder.length() - SPACE.length(), builder.length(),
Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
}
/**
* 将图片注入到SpannableStringBuilder首部
*/
public static void injectAtStart(@NonNull Context context,
@NonNull SpannableStringBuilder builder,
@DrawableRes int resourceId) {
builder.insert(0, SPACE);
ImageSpan span = new VerticalCenterImageSpan(context, resourceId);
builder.setSpan(span, 0, SPACE.length(), Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
}
/**
* 将图片注入到SpannableStringBuilder首部,并设置图片与后方文字的间距(LTR/RTL 自适应)
*
* @param drawablePadding 图片与文字的间距,单位 px
*/
public static void injectAtStart(@NonNull Context context,
@NonNull SpannableStringBuilder builder,
@DrawableRes int resourceId,
@Px int drawablePadding) {
builder.insert(0, SPACE);
ImageSpan span = new VerticalCenterImageSpan(
context, resourceId, PADDING_AFTER_ICON, drawablePadding);
builder.setSpan(span, 0, SPACE.length(), Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
}
/**
* 将图片注入到SpannableStringBuilder中,可以设置图片宽高和左右边距(物理 left/right,不随 RTL 翻转)
*/
public static void inject(@NonNull Context context, @NonNull SpannableStringBuilder builder,
@DrawableRes int resourceId, int width, int height, Rect margin) {
builder.append(SPACE);
ImageSpan span = new VerticalCenterImageSpan(context, resourceId, width, height, margin);
builder.setSpan(span, builder.length() - SPACE.length(),
builder.length(), Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
}
/**
* 将图片及文字注入到SpannableStringBuilder中
*/
public static void inject(@NonNull Context context, @NonNull SpannableStringBuilder builder, @DrawableRes int resourceId, String... strs) {
if (strs == null || strs.length == 0) {
return;
}
List<String> list = new ArrayList<>(strs.length);
for (String str : strs) {
if (!TextUtils.isEmpty(str)) {
// 删除无效数据
list.add(str);
}
}
int size = list.size();
for (int i = 0; i < size; i++) {
builder.append(list.get(i));
if (i != size - 1) {
inject(context, builder, resourceId);
}
}
}
/**
* 将图片及文字注入到SpannableStringBuilder中
*/
public static void inject(@NonNull Context context, @NonNull SpannableStringBuilder builder, @DrawableRes int resourceId, List<String> list) {
ListUtil.removeEmpty(list);
if (ListUtil.isEmpty(list)) {
return;
}
int size = list.size();
for (int i = 0; i < size; i++) {
builder.append(list.get(i));
if (i != size - 1) {
inject(context, builder, resourceId);
}
}
}
public static boolean isRtl(Context context) {
if (context == null) {
return false;
}
return context.getResources().getConfiguration().getLayoutDirection() == View.LAYOUT_DIRECTION_RTL;
}
}

652

被折叠的 条评论
为什么被折叠?



