如何使用自定义跨度在带占位符的 TextView 格式化文本上设置多个跨度?
How to set multiple spans on a TextView's formatted text with placeholder, using customized span?
背景
我知道如何在静态文本中的部分文本上设置多个跨度,正如我所问的 here :
final SpannableString text = new SpannableString("Hello Whosebug");
text.setSpan(new RelativeSizeSpan(1.5f), text.length() - "Whosebug".length(), text.length(),
Spannable.SPAN_EXCLUSIVE_EXCLUSIVE);
text.setSpan(new ForegroundColorSpan(Color.RED), 3, text.length() - 3, Spannable.SPAN_EXCLUSIVE_EXCLUSIVE);
tv.setText(text);
这会将 "Whosebug" 设置为自身有 2 个 span。
我也知道如何在文本的一部分上设置可绘制范围,正如我所问的。
问题
现在我需要在使用占位符格式化文本生成的文本上设置 2 个跨度,同时仍像往常一样设置其他样式。
例如,假设我在 strings.xml 中有下一个文本:
<string name="potential_free_upgrade_1_d_months">
<![CDATA[
Potential free upgrade: <uu><b><font color=\'#3792e5\'>%1$d months</font></b></uu>]]>
</string>
计划是“%1$d 个月”的文本颜色为“#3792e5”,并且有一个特殊的下划线,比默认下划线稍低一些。我为特殊的下划线使用了一个特殊的自定义标签"uu",在代码中处理。
问题是,无论我做什么,我都找不到如何同时显示文本颜色和下划线。
我试过的
由于这个问题有一个占位符(并且要格式化的文本周围的文本可能不同),我不得不使用 "Html.FromHtml" :
String formattedStr = getString(R.string.potential_free_upgrade_1_d_months, 9);
Spanned textToShow = Html.fromHtml(formattedStr, null, new TagHandler() {
int start;
@Override
public void handleTag(final boolean opening, final String tag, Editable output, final XMLReader xmlReader) {
switch (tag) {
case "uu":
if (opening)
start = output.length();
else {
int end = output.length();
//output.setSpan(new ForegroundColorSpan(0xff3792e5), start, end, Spannable.SPAN_EXCLUSIVE_EXCLUSIVE);
output.setSpan(
new DrawableSpan(ResourcesCompat.getDrawable(getResources(), R.drawable.bit_below_underline, null)),
start, end, Spannable.SPAN_EXCLUSIVE_EXCLUSIVE);
}
}
}
});
titleTextView.setText(textToShow);
DrawableSpan.java
public class DrawableSpan extends ReplacementSpan {
private Drawable mDrawable;
private final Rect mPadding;
public DrawableSpan(Drawable drawable) {
super();
mDrawable = drawable;
mPadding = new Rect();
mDrawable.getPadding(mPadding);
}
@Override
public void draw(@NonNull Canvas canvas, CharSequence text, int start, int end, float x, int top, int y, int bottom, @NonNull Paint paint) {
RectF rect = new RectF(x, top, x + measureText(paint, text, start, end), bottom);
mDrawable.setBounds((int) rect.left - mPadding.left, (int) rect.top - mPadding.top, (int) rect.right + mPadding.right, (int) rect.bottom + mPadding.bottom);
canvas.drawText(text, start, end, x, y, paint);
mDrawable.draw(canvas);
}
@Override
public int getSize(@NonNull 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);
}
}
res/drawable/bit_below_underline.xml
<shape
xmlns:android="http://schemas.android.com/apk/res/android"
android:shape="line">
<padding android:bottom="30dp"/>
<stroke
android:width="1dp"
android:color="#3792e5"/>
</shape>
我尝试将 2 "setSpan" 调用在一起(首先在上面的代码中进行了注释),但没有帮助。
问题
如何在文本部分设置 2 个跨度,正如我在上面指定的那样(带占位符的部分文本),以便一个是文本颜色,另一个是自定义下划线?
测试您的代码时,似乎 ReplacementSpans
是在任何 CharacterStyleSpan
之前绘制的。所以尝试使用 MetricAffectingSpan
。它会在 ReplacementSpans
之前绘制。
所以使用自定义 MetricAffectingSpan
而不是 ForegroundColorSpan
import android.text.TextPaint;
import android.text.style.MetricAffectingSpan;
public class BGColorSpan extends MetricAffectingSpan {
private int color;
public BGColorSpan(int color) {
this.color = color;
}
@Override
public void updateMeasureState(TextPaint textPaint) {
textPaint.setColor(color);
}
@Override
public void updateDrawState(TextPaint textPaint) {
textPaint.setColor(color);
}
}
好的。我试图从 android SDK 源代码中解释这个问题,在这个 for 循环中有 continue
关键字它将跳过 CharacterStyleSpan
的渲染
final float originalX = x;
for (int i = start, inext; i < measureLimit; i = inext) {
TextPaint wp = mWorkPaint;
wp.set(mPaint);
inext = mMetricAffectingSpanSpanSet.getNextTransition(mStart + i, mStart + limit) -
mStart;
int mlimit = Math.min(inext, measureLimit);
ReplacementSpan replacement = null;
for (int j = 0; j < mMetricAffectingSpanSpanSet.numberOfSpans; j++) {
// Both intervals [spanStarts..spanEnds] and [mStart + i..mStart + mlimit] are NOT
// empty by construction. This special case in getSpans() explains the >= & <= tests
if ((mMetricAffectingSpanSpanSet.spanStarts[j] >= mStart + mlimit) ||
(mMetricAffectingSpanSpanSet.spanEnds[j] <= mStart + i)) continue;
MetricAffectingSpan span = mMetricAffectingSpanSpanSet.spans[j];
if (span instanceof ReplacementSpan) {
replacement = (ReplacementSpan)span;
} else {
// We might have a replacement that uses the draw
// state, otherwise measure state would suffice.
span.updateDrawState(wp);
}
}
if (replacement != null) {
x += handleReplacement(replacement, wp, i, mlimit, runIsRtl, c, x, top, y,
bottom, fmi, needWidth || mlimit < measureLimit);
// I think this line making your issue
continue;
}
for (int j = i, jnext; j < mlimit; j = jnext) {
jnext = mCharacterStyleSpanSet.getNextTransition(mStart + j, mStart + inext) -
mStart;
int offset = Math.min(jnext, mlimit);
wp.set(mPaint);
for (int k = 0; k < mCharacterStyleSpanSet.numberOfSpans; k++) {
// Intentionally using >= and <= as explained above
if ((mCharacterStyleSpanSet.spanStarts[k] >= mStart + offset) ||
(mCharacterStyleSpanSet.spanEnds[k] <= mStart + j)) continue;
CharacterStyle span = mCharacterStyleSpanSet.spans[k];
span.updateDrawState(wp);
}
// Only draw hyphen on last run in line
if (jnext < mLen) {
wp.setHyphenEdit(0);
}
x += handleText(wp, j, jnext, i, inext, runIsRtl, c, x,
top, y, bottom, fmi, needWidth || jnext < measureLimit, offset);
}
}
背景
我知道如何在静态文本中的部分文本上设置多个跨度,正如我所问的 here :
final SpannableString text = new SpannableString("Hello Whosebug");
text.setSpan(new RelativeSizeSpan(1.5f), text.length() - "Whosebug".length(), text.length(),
Spannable.SPAN_EXCLUSIVE_EXCLUSIVE);
text.setSpan(new ForegroundColorSpan(Color.RED), 3, text.length() - 3, Spannable.SPAN_EXCLUSIVE_EXCLUSIVE);
tv.setText(text);
这会将 "Whosebug" 设置为自身有 2 个 span。
我也知道如何在文本的一部分上设置可绘制范围,正如我所问的
问题
现在我需要在使用占位符格式化文本生成的文本上设置 2 个跨度,同时仍像往常一样设置其他样式。
例如,假设我在 strings.xml 中有下一个文本:
<string name="potential_free_upgrade_1_d_months">
<![CDATA[
Potential free upgrade: <uu><b><font color=\'#3792e5\'>%1$d months</font></b></uu>]]>
</string>
计划是“%1$d 个月”的文本颜色为“#3792e5”,并且有一个特殊的下划线,比默认下划线稍低一些。我为特殊的下划线使用了一个特殊的自定义标签"uu",在代码中处理。
问题是,无论我做什么,我都找不到如何同时显示文本颜色和下划线。
我试过的
由于这个问题有一个占位符(并且要格式化的文本周围的文本可能不同),我不得不使用 "Html.FromHtml" :
String formattedStr = getString(R.string.potential_free_upgrade_1_d_months, 9);
Spanned textToShow = Html.fromHtml(formattedStr, null, new TagHandler() {
int start;
@Override
public void handleTag(final boolean opening, final String tag, Editable output, final XMLReader xmlReader) {
switch (tag) {
case "uu":
if (opening)
start = output.length();
else {
int end = output.length();
//output.setSpan(new ForegroundColorSpan(0xff3792e5), start, end, Spannable.SPAN_EXCLUSIVE_EXCLUSIVE);
output.setSpan(
new DrawableSpan(ResourcesCompat.getDrawable(getResources(), R.drawable.bit_below_underline, null)),
start, end, Spannable.SPAN_EXCLUSIVE_EXCLUSIVE);
}
}
}
});
titleTextView.setText(textToShow);
DrawableSpan.java
public class DrawableSpan extends ReplacementSpan {
private Drawable mDrawable;
private final Rect mPadding;
public DrawableSpan(Drawable drawable) {
super();
mDrawable = drawable;
mPadding = new Rect();
mDrawable.getPadding(mPadding);
}
@Override
public void draw(@NonNull Canvas canvas, CharSequence text, int start, int end, float x, int top, int y, int bottom, @NonNull Paint paint) {
RectF rect = new RectF(x, top, x + measureText(paint, text, start, end), bottom);
mDrawable.setBounds((int) rect.left - mPadding.left, (int) rect.top - mPadding.top, (int) rect.right + mPadding.right, (int) rect.bottom + mPadding.bottom);
canvas.drawText(text, start, end, x, y, paint);
mDrawable.draw(canvas);
}
@Override
public int getSize(@NonNull 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);
}
}
res/drawable/bit_below_underline.xml
<shape
xmlns:android="http://schemas.android.com/apk/res/android"
android:shape="line">
<padding android:bottom="30dp"/>
<stroke
android:width="1dp"
android:color="#3792e5"/>
</shape>
我尝试将 2 "setSpan" 调用在一起(首先在上面的代码中进行了注释),但没有帮助。
问题
如何在文本部分设置 2 个跨度,正如我在上面指定的那样(带占位符的部分文本),以便一个是文本颜色,另一个是自定义下划线?
测试您的代码时,似乎 ReplacementSpans
是在任何 CharacterStyleSpan
之前绘制的。所以尝试使用 MetricAffectingSpan
。它会在 ReplacementSpans
之前绘制。
所以使用自定义 MetricAffectingSpan
而不是 ForegroundColorSpan
import android.text.TextPaint;
import android.text.style.MetricAffectingSpan;
public class BGColorSpan extends MetricAffectingSpan {
private int color;
public BGColorSpan(int color) {
this.color = color;
}
@Override
public void updateMeasureState(TextPaint textPaint) {
textPaint.setColor(color);
}
@Override
public void updateDrawState(TextPaint textPaint) {
textPaint.setColor(color);
}
}
好的。我试图从 android SDK 源代码中解释这个问题,在这个 for 循环中有 continue
关键字它将跳过 CharacterStyleSpan
final float originalX = x;
for (int i = start, inext; i < measureLimit; i = inext) {
TextPaint wp = mWorkPaint;
wp.set(mPaint);
inext = mMetricAffectingSpanSpanSet.getNextTransition(mStart + i, mStart + limit) -
mStart;
int mlimit = Math.min(inext, measureLimit);
ReplacementSpan replacement = null;
for (int j = 0; j < mMetricAffectingSpanSpanSet.numberOfSpans; j++) {
// Both intervals [spanStarts..spanEnds] and [mStart + i..mStart + mlimit] are NOT
// empty by construction. This special case in getSpans() explains the >= & <= tests
if ((mMetricAffectingSpanSpanSet.spanStarts[j] >= mStart + mlimit) ||
(mMetricAffectingSpanSpanSet.spanEnds[j] <= mStart + i)) continue;
MetricAffectingSpan span = mMetricAffectingSpanSpanSet.spans[j];
if (span instanceof ReplacementSpan) {
replacement = (ReplacementSpan)span;
} else {
// We might have a replacement that uses the draw
// state, otherwise measure state would suffice.
span.updateDrawState(wp);
}
}
if (replacement != null) {
x += handleReplacement(replacement, wp, i, mlimit, runIsRtl, c, x, top, y,
bottom, fmi, needWidth || mlimit < measureLimit);
// I think this line making your issue
continue;
}
for (int j = i, jnext; j < mlimit; j = jnext) {
jnext = mCharacterStyleSpanSet.getNextTransition(mStart + j, mStart + inext) -
mStart;
int offset = Math.min(jnext, mlimit);
wp.set(mPaint);
for (int k = 0; k < mCharacterStyleSpanSet.numberOfSpans; k++) {
// Intentionally using >= and <= as explained above
if ((mCharacterStyleSpanSet.spanStarts[k] >= mStart + offset) ||
(mCharacterStyleSpanSet.spanEnds[k] <= mStart + j)) continue;
CharacterStyle span = mCharacterStyleSpanSet.spans[k];
span.updateDrawState(wp);
}
// Only draw hyphen on last run in line
if (jnext < mLen) {
wp.setHyphenEdit(0);
}
x += handleText(wp, j, jnext, i, inext, runIsRtl, c, x,
top, y, bottom, fmi, needWidth || jnext < measureLimit, offset);
}
}