Android - 使用 ReplacementSpan 为 SpannableStringBuilder 添加边距
Android - Add Margin for SpannableStringBuilder using ReplacementSpan
我正在尝试在 TextView/EditText 中格式化标签(比如 Material 设计规范中提到的芯片)。我可以使用 ReplacementSpan
格式化背景。但问题是我无法增加 TextView/EditText 中的行间距。见下图
问题是如何为主题标签添加上下边距?
这是我将背景添加到文本的代码:
/**
* First draw a rectangle
* Then draw text on top
*/
@Override
public void draw(Canvas canvas, CharSequence text, int start, int end, float x, int top, int y, int bottom, Paint paint) {
RectF rect = new RectF(x, top, x + measureText(paint, text, start, end), bottom);
paint.setColor(backgroundColor);
canvas.drawRoundRect(rect, CORNER_RADIUS, CORNER_RADIUS, paint);
paint.setColor(textColor);
canvas.drawText(text, start, end, x, y, paint);
}
Android 中的文本标记的文档非常少,编写这段代码就像在黑暗中摸索。
我已经做了一些,所以我会分享我所知道的。
您可以通过将字元跨度包裹在 LineHeightSpan
中来处理行间距。 LineHeightSpan
是扩展 ParagraphStyle
标记界面的界面,因此这告诉您它会影响段落级别的外观。也许解释它的一个好方法是将您的 ReplacementSpan
子类与 HTML <span>
进行比较,而 ParagraphStyle
跨度 LineHeightSpan
就像 [=71] =] <div>
.
LineHeightSpan
接口包含一个方法:
public void chooseHeight(CharSequence text, int start, int end,
int spanstartv, int v,
Paint.FontMetricsInt fm);
您段落中的每一行都会调用此方法
text
是您的 Spanned
字符串。
start
是当前行开头字符的索引
end
是当前行末尾字符的索引
spanstartv
是 (IIRC) 整个跨度本身的垂直偏移
v
是(IIRC)当前行的垂直偏移量
fm
是FontMetrics
对象,其实是一个返回的(in/out)参数。您的代码将对 fm
进行更改,TextView
将在绘图时使用这些更改。
那么 TextView
要做的就是为它处理的每一行调用一次这个方法。根据参数以及您的 Spanned
字符串,您设置 FontMetrics
以使用您选择的值呈现该行。
这是我为列表中的项目符号项所做的示例(想想 <ol><li>
),我希望每个列表项之间有一些分隔:
@Override
public void chooseHeight(CharSequence text, int start, int end, int spanstartv, int v, Paint.FontMetricsInt fm) {
int incr = Math.round(.36F * fm.ascent); // note: ascent is negative
// first line: add space to the top
if (((Spanned) text).getSpanStart(this) == start) {
fm.ascent += incr;
fm.top = fm.ascent + 1;
}
// last line: add space to the bottom
if (((Spanned) text).getSpanEnd(this) == end) {
fm.bottom -= incr;
}
}
您的版本可能会更简单,只需对调用的每一行以相同的方式更改 FontMetrics
。
在破解FontMetrics
时,记录器和调试器是你的朋友。你只需要不断调整值,直到你得到你喜欢的东西。
BackgroundColorSpan 没用吗?
对于您的具体情况,您还可以为 TextView 设置 lineSpacing。
最后一个选项(未对此进行测试)是计算跨度的高度,使其大于您正在绘制的跨度。您可以查看 DynamicDrawableSpan 中的 getSize 实现,了解如何使用给定的 FontMetrics 实例作为参数来设置跨度的高度。
我刚才遇到了类似的问题,这是我想出的解决方案:
xml 中的托管 TextView:
<TextView
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:paddingTop="18dp"
android:paddingBottom="18dp"
android:paddingLeft="8dp"
android:paddingRight="8dp"
android:gravity="fill"
android:textSize="12sp"
android:lineSpacingExtra="10sp"
android:textStyle="bold"
android:text="@{viewModel.renderedTagBadges}">
ReplacementSpan
的自定义版本
public class TagBadgeSpannable extends ReplacementSpan implements LineHeightSpan {
private static int CORNER_RADIUS = 30;
private final int textColor;
private final int backgroundColor;
private final int lineHeight;
public TagBadgeSpannable(int lineHeight, int textColor, int backgroundColor) {
super();
this.textColor = textColor;
this.backgroundColor = backgroundColor;
this.lineHeight = lineHeight;
}
@Override
public void draw(Canvas canvas, CharSequence text, int start, int end, float x, int top, int y, int bottom, Paint paint) {
final float textSize = paint.getTextSize();
final float textLength = x + measureText(paint, text, start, end);
final float badgeHeight = textSize * 2.25f;
final float textOffsetVertical = textSize * 1.45f;
RectF badge = new RectF(x, y, textLength, y + badgeHeight);
paint.setColor(backgroundColor);
canvas.drawRoundRect(badge, CORNER_RADIUS, CORNER_RADIUS, paint);
paint.setColor(textColor);
canvas.drawText(text, start, end, x, y + textOffsetVertical, paint);
}
@Override
public int getSize(Paint paint, CharSequence text, int start, int end, Paint.FontMetricsInt fm) {
return Math.round(paint.measureText(text, start, end));
}
private float measureText(Paint paint, CharSequence text, int start, int end) {
return paint.measureText(text, start, end);
}
@Override
public void chooseHeight(CharSequence charSequence, int i, int i1, int i2, int i3, Paint.FontMetricsInt fontMetricsInt) {
fontMetricsInt.bottom += lineHeight;
fontMetricsInt.descent += lineHeight;
}
}
最后是创建 Spannable 的构建器
public class AndroidTagBadgeBuilder implements TagBadgeBuilder {
private final SpannableStringBuilder stringBuilder;
private final String textColor;
private final int lineHeight;
public AndroidTagBadgeBuilder(SpannableStringBuilder stringBuilder, int lineHeight, String textColor) {
this.stringBuilder = stringBuilder;
this.lineHeight = lineHeight;
this.textColor = textColor;
}
@Override
public void appendTag(String tagName, String badgeColor) {
final String nbspSpacing = "\u202F\u202F"; // none-breaking spaces
String badgeText = nbspSpacing + tagName + nbspSpacing;
stringBuilder.append(badgeText);
stringBuilder.setSpan(
new TagBadgeSpannable(lineHeight, Color.parseColor(textColor), Color.parseColor(badgeColor)),
stringBuilder.length() - badgeText.length(),
stringBuilder.length()- badgeText.length() + badgeText.length(),
Spanned.SPAN_EXCLUSIVE_EXCLUSIVE
);
stringBuilder.append(" ");
}
@Override
public CharSequence getTags() {
return stringBuilder;
}
@Override
public void clear() {
stringBuilder.clear();
stringBuilder.clearSpans();
}
}
结果将如下所示:
根据自己的喜好调整 TagBadgeSpannable
中的措施。
我已经使用此代码将一个非常简单的示例项目上传到 github,请随时查看。
注意: 示例使用 Android 数据绑定并编写为 MVVM 风格
我正在尝试在 TextView/EditText 中格式化标签(比如 Material 设计规范中提到的芯片)。我可以使用 ReplacementSpan
格式化背景。但问题是我无法增加 TextView/EditText 中的行间距。见下图
问题是如何为主题标签添加上下边距?
这是我将背景添加到文本的代码:
/**
* First draw a rectangle
* Then draw text on top
*/
@Override
public void draw(Canvas canvas, CharSequence text, int start, int end, float x, int top, int y, int bottom, Paint paint) {
RectF rect = new RectF(x, top, x + measureText(paint, text, start, end), bottom);
paint.setColor(backgroundColor);
canvas.drawRoundRect(rect, CORNER_RADIUS, CORNER_RADIUS, paint);
paint.setColor(textColor);
canvas.drawText(text, start, end, x, y, paint);
}
Android 中的文本标记的文档非常少,编写这段代码就像在黑暗中摸索。
我已经做了一些,所以我会分享我所知道的。
您可以通过将字元跨度包裹在 LineHeightSpan
中来处理行间距。 LineHeightSpan
是扩展 ParagraphStyle
标记界面的界面,因此这告诉您它会影响段落级别的外观。也许解释它的一个好方法是将您的 ReplacementSpan
子类与 HTML <span>
进行比较,而 ParagraphStyle
跨度 LineHeightSpan
就像 [=71] =] <div>
.
LineHeightSpan
接口包含一个方法:
public void chooseHeight(CharSequence text, int start, int end,
int spanstartv, int v,
Paint.FontMetricsInt fm);
您段落中的每一行都会调用此方法
text
是您的Spanned
字符串。start
是当前行开头字符的索引end
是当前行末尾字符的索引spanstartv
是 (IIRC) 整个跨度本身的垂直偏移v
是(IIRC)当前行的垂直偏移量fm
是FontMetrics
对象,其实是一个返回的(in/out)参数。您的代码将对fm
进行更改,TextView
将在绘图时使用这些更改。
那么 TextView
要做的就是为它处理的每一行调用一次这个方法。根据参数以及您的 Spanned
字符串,您设置 FontMetrics
以使用您选择的值呈现该行。
这是我为列表中的项目符号项所做的示例(想想 <ol><li>
),我希望每个列表项之间有一些分隔:
@Override
public void chooseHeight(CharSequence text, int start, int end, int spanstartv, int v, Paint.FontMetricsInt fm) {
int incr = Math.round(.36F * fm.ascent); // note: ascent is negative
// first line: add space to the top
if (((Spanned) text).getSpanStart(this) == start) {
fm.ascent += incr;
fm.top = fm.ascent + 1;
}
// last line: add space to the bottom
if (((Spanned) text).getSpanEnd(this) == end) {
fm.bottom -= incr;
}
}
您的版本可能会更简单,只需对调用的每一行以相同的方式更改 FontMetrics
。
在破解FontMetrics
时,记录器和调试器是你的朋友。你只需要不断调整值,直到你得到你喜欢的东西。
BackgroundColorSpan 没用吗?
对于您的具体情况,您还可以为 TextView 设置 lineSpacing。
最后一个选项(未对此进行测试)是计算跨度的高度,使其大于您正在绘制的跨度。您可以查看 DynamicDrawableSpan 中的 getSize 实现,了解如何使用给定的 FontMetrics 实例作为参数来设置跨度的高度。
我刚才遇到了类似的问题,这是我想出的解决方案:
xml 中的托管 TextView:
<TextView
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:paddingTop="18dp"
android:paddingBottom="18dp"
android:paddingLeft="8dp"
android:paddingRight="8dp"
android:gravity="fill"
android:textSize="12sp"
android:lineSpacingExtra="10sp"
android:textStyle="bold"
android:text="@{viewModel.renderedTagBadges}">
ReplacementSpan
public class TagBadgeSpannable extends ReplacementSpan implements LineHeightSpan {
private static int CORNER_RADIUS = 30;
private final int textColor;
private final int backgroundColor;
private final int lineHeight;
public TagBadgeSpannable(int lineHeight, int textColor, int backgroundColor) {
super();
this.textColor = textColor;
this.backgroundColor = backgroundColor;
this.lineHeight = lineHeight;
}
@Override
public void draw(Canvas canvas, CharSequence text, int start, int end, float x, int top, int y, int bottom, Paint paint) {
final float textSize = paint.getTextSize();
final float textLength = x + measureText(paint, text, start, end);
final float badgeHeight = textSize * 2.25f;
final float textOffsetVertical = textSize * 1.45f;
RectF badge = new RectF(x, y, textLength, y + badgeHeight);
paint.setColor(backgroundColor);
canvas.drawRoundRect(badge, CORNER_RADIUS, CORNER_RADIUS, paint);
paint.setColor(textColor);
canvas.drawText(text, start, end, x, y + textOffsetVertical, paint);
}
@Override
public int getSize(Paint paint, CharSequence text, int start, int end, Paint.FontMetricsInt fm) {
return Math.round(paint.measureText(text, start, end));
}
private float measureText(Paint paint, CharSequence text, int start, int end) {
return paint.measureText(text, start, end);
}
@Override
public void chooseHeight(CharSequence charSequence, int i, int i1, int i2, int i3, Paint.FontMetricsInt fontMetricsInt) {
fontMetricsInt.bottom += lineHeight;
fontMetricsInt.descent += lineHeight;
}
}
最后是创建 Spannable 的构建器
public class AndroidTagBadgeBuilder implements TagBadgeBuilder {
private final SpannableStringBuilder stringBuilder;
private final String textColor;
private final int lineHeight;
public AndroidTagBadgeBuilder(SpannableStringBuilder stringBuilder, int lineHeight, String textColor) {
this.stringBuilder = stringBuilder;
this.lineHeight = lineHeight;
this.textColor = textColor;
}
@Override
public void appendTag(String tagName, String badgeColor) {
final String nbspSpacing = "\u202F\u202F"; // none-breaking spaces
String badgeText = nbspSpacing + tagName + nbspSpacing;
stringBuilder.append(badgeText);
stringBuilder.setSpan(
new TagBadgeSpannable(lineHeight, Color.parseColor(textColor), Color.parseColor(badgeColor)),
stringBuilder.length() - badgeText.length(),
stringBuilder.length()- badgeText.length() + badgeText.length(),
Spanned.SPAN_EXCLUSIVE_EXCLUSIVE
);
stringBuilder.append(" ");
}
@Override
public CharSequence getTags() {
return stringBuilder;
}
@Override
public void clear() {
stringBuilder.clear();
stringBuilder.clearSpans();
}
}
结果将如下所示:
根据自己的喜好调整 TagBadgeSpannable
中的措施。
我已经使用此代码将一个非常简单的示例项目上传到 github,请随时查看。
注意: 示例使用 Android 数据绑定并编写为 MVVM 风格