基本问题是没有为ReplacementSpan
. 如来源中ReplacementSpan
所述:
如果 span 覆盖了整个文本,并且没有设置高度,则不会为 span 调用 draw(Canvas, CharSequence, int, int, float, int, int, int, Paint)}。
这是 Archit Sureja 发布的内容的重复。ReplacementSpan
在我原来的帖子中,我更新了in的高度,getSize()
但我现在实现了LineHeightSpan.WithDensity
接口来做同样的事情。(感谢vovahost提供 此信息。)
但是,您还提出了其他需要解决的问题。
您提供的项目提出的问题是该点不适合TextView
它必须驻留的位置。你看到的是截断点。如果点的大小超过文本的宽度或高度怎么办?
首先,关于高度,chooseHeight()
界面的方法通过将点的大小添加到字体的有效高度来LineHeightSpan.WithDensity
调整被认为是字体的底部。TextView
为此,将点的高度添加到字体的底部:
fontMetricsInt.bottom = fm.bottom + mDotSize.toInt();
(这是与使用TextView
' 填充的此答案的最后一次迭代相比的更改。由于此更改,该类TextView
不再需要UnderDotSpan
。虽然我添加了TextView
,但它并不是真正需要的。)
最后一个问题是,如果点比文本宽,则点在开始和结束处被截断。clipToPadding="false"
在这里不起作用,因为点被截断不是因为它被剪裁到填充,而是因为它被剪裁到我们所说的文本宽度在getSize()
. 为了解决这个问题,我修改了getSize()
方法来检测点何时比文本测量值更宽,并增加返回值以匹配点的宽度。一个名为的新值mStartShim
是必须应用于绘制文本和点以使其适合的数量。
最后一个问题是点的中心是文本底部下方点的半径而不是直径,因此绘制点的代码更改draw()
为:
canvas.drawCircle(x + textSize / 2, bottom.toFloat(), mDotSize / 2, paint)
(我也改了代码做Canvas
翻译而不是添加偏移量。效果是一样的。)
结果如下:
activity_main.xml
<android.support.constraint.ConstraintLayout android:layout_width="match_parent" android:layout_height="match_parent" android:background="@android:color/darker_gray">
<TextView
android:id="@+id/textView"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="24dp"
android:background="@android:color/white"
app:layout_constraintLeft_toLeftOf="parent"
app:layout_constraintRight_toRightOf="parent"
app:layout_constraintTop_toTopOf="parent" />
</android.support.constraint.ConstraintLayout>
MainActivity.java
class MainActivity : AppCompatActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
val text = "1"
val spannable = SpannableString(text)
spannable.setSpan(UnderDotSpan(this@MainActivity, 0xFF039BE5.toInt(), textView.currentTextColor),
0, text.length, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE)
textView.setText(spannable, TextView.BufferType.SPANNABLE)
}
}
UnderDotSpan.kt
// From the original UnderDotSpan: Also implement the LineHeightSpan.WithDensity interface to
// compute the height of our "dotted" font.
class UnderDotSpan(private val mDotSize: Float, private val mDotColor: Int, private val mTextColor: Int) : ReplacementSpan(), LineHeightSpan.WithDensity {
companion object {
@JvmStatic
private val DEFAULT_DOT_SIZE_IN_DP = 16
}
// Additional horizontal space to the start, if needed, to fit the dot
var mStartShim = 0;
constructor(context: Context, dotColor: Int, textColor: Int)
: this(TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, DEFAULT_DOT_SIZE_IN_DP.toFloat(),
context.resources.displayMetrics), dotColor, textColor)
// ReplacementSpan override to determine the size (length) of the text.
override fun getSize(paint: Paint, text: CharSequence, start: Int, end: Int, fm: Paint.FontMetricsInt?): Int {
val baseTextWidth = paint.measureText(text, start, end)
// If the width of the text is less than the width of our dot, increase the text width
// to match the dot's width; otherwise, just return the width of the text.
mStartShim = if (baseTextWidth < mDotSize) ((mDotSize - baseTextWidth) / 2).toInt() else 0
return Math.round(baseTextWidth + mStartShim * 2)
}
override fun draw(canvas: Canvas, text: CharSequence, start: Int, end: Int, x: Float, top: Int,
y: Int, bottom: Int, paint: Paint) {
if (TextUtils.isEmpty(text)) {
return
}
val textSize = paint.measureText(text, start, end)
paint.color = mDotColor
canvas.save()
// Draw the circle in the horizontal center and under the text. Add in the
// offset (mStartShim) if we had to increase the length of the text to accommodate our dot.
canvas.translate(mStartShim.toFloat(), -mDotSize / 2)
// Draw a circle, but this could be any other shape or drawable. It just has
// to fit into the allotted space which is the size of the dot.
canvas.drawCircle(x + textSize / 2, bottom.toFloat(), mDotSize / 2, paint)
paint.color = mTextColor
// Keep the starting shim, but reset the y-translation to write the text.
canvas.translate(0f, mDotSize / 2)
canvas.drawText(text, start, end, x, y.toFloat(), paint)
canvas.restore()
}
// LineHeightSpan.WithDensity override to determine the height of the font with the dot.
override fun chooseHeight(charSequence: CharSequence, i: Int, i1: Int, i2: Int, i3: Int,
fontMetricsInt: Paint.FontMetricsInt, textPaint: TextPaint) {
val fm = textPaint.fontMetricsInt
fontMetricsInt.top = fm.top
fontMetricsInt.ascent = fm.ascent
fontMetricsInt.descent = fm.descent
// Our "dotted" font now must accommodate the size of the dot, so change the bottom of the
// font to accommodate the dot.
fontMetricsInt.bottom = fm.bottom + mDotSize.toInt();
fontMetricsInt.leading = fm.leading
}
// LineHeightSpan.WithDensity override that is needed to satisfy the interface but not called.
override fun chooseHeight(charSequence: CharSequence, i: Int, i1: Int, i2: Int, i3: Int,
fontMetricsInt: Paint.FontMetricsInt) {
}
}
对于在文本下放置一个小型可绘制对象的更一般情况,以下类起作用并基于UnderDotSpan
:
UnderDrawableSpan.java
public class UnderDrawableSpan extends ReplacementSpan implements LineHeightSpan.WithDensity {
final private Drawable mDrawable;
final private int mDrawableWidth;
final private int mDrawableHeight;
final private int mMargin;
// How much we need to jog the text to line up with a larger-than-text-width drawable.
private int mStartShim = 0;
UnderDrawableSpan(Context context, Drawable drawable, int drawableWidth, int drawableHeight,
int margin) {
DisplayMetrics metrics = context.getResources().getDisplayMetrics();
mDrawable = drawable;
mDrawableWidth = (int) TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP,
(float) drawableWidth, metrics);
mDrawableHeight = (int) TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP,
(float) drawableHeight, metrics);
mMargin = (int) TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP,
(float) margin, metrics);
}
@Override
public void draw(@NonNull Canvas canvas, CharSequence text, int start, int end, float x, int top, int y,
int bottom, @NonNull Paint paint) {
if (TextUtils.isEmpty(text)) {
return;
}
float textWidth = paint.measureText(text, start, end);
float offset = mStartShim + x + (textWidth - mDrawableWidth) / 2;
mDrawable.setBounds(0, 0, mDrawableWidth, mDrawableHeight);
canvas.save();
canvas.translate(offset, bottom - mDrawableHeight);
mDrawable.draw(canvas);
canvas.restore();
canvas.save();
canvas.translate(mStartShim, 0);
canvas.drawText(text, start, end, x, y, paint);
canvas.restore();
}
// ReplacementSpan override to determine the size (length) of the text.
@Override
public int getSize(@NonNull Paint paint, CharSequence text, int start, int end, Paint.FontMetricsInt fm) {
float baseTextWidth = paint.measureText(text, start, end);
// If the width of the text is less than the width of our drawable, increase the text width
// to match the drawable's width; otherwise, just return the width of the text.
mStartShim = (baseTextWidth < mDrawableWidth) ? (int) (mDrawableWidth - baseTextWidth) / 2 : 0;
return Math.round(baseTextWidth + mStartShim * 2);
}
// LineHeightSpan.WithDensity override to determine the height of the font with the dot.
@Override
public void chooseHeight(CharSequence charSequence, int i, int i1, int i2, int i3,
Paint.FontMetricsInt fontMetricsInt, TextPaint textPaint) {
Paint.FontMetricsInt fm = textPaint.getFontMetricsInt();
fontMetricsInt.top = fm.top;
fontMetricsInt.ascent = fm.ascent;
fontMetricsInt.descent = fm.descent;
// Our font now must accommodate the size of the drawable, so change the bottom of the
// font to accommodate the drawable.
fontMetricsInt.bottom = fm.bottom + mDrawableHeight + mMargin;
fontMetricsInt.leading = fm.leading;
}
// Required but not used.
@Override
public void chooseHeight(CharSequence charSequence, int i, int i1, int i2, int i3,
Paint.FontMetricsInt fontMetricsInt) {
}
}
使用以下可绘制 XMLUnderDrawableSpan
会产生以下结果:(drawable的宽度和高度设置为12dp
。文本的字体大小为24sp
。)
gradient_drawable.xml
<shape xmlns:android="http://schemas.android.com/apk/res/android"
android:shape="oval">
<size
android:width="4dp"
android:height="4dp" />
<gradient
android:type="radial"
android:gradientRadius="60%p"
android:endColor="#e96507"
android:startColor="#ece6e1" />
</shape>
我最近有机会重新审视这个问题和答案。我正在发布更灵活的UnderDrawableSpan代码版本。GitHub 上有一个演示项目。
UnderDrawableSpan.kt(更新)
/**
* Place a drawable at the bottom center of text within a span. Because this class is extended
* from [ReplacementSpan], the span must reside on a single line and cannot span lines.
*/
class UnderDrawableSpan(
context: Context, drawable: Drawable, drawableWidth: Int, drawableHeight: Int, margin: Int
) : ReplacementSpan(), LineHeightSpan.WithDensity {
// The image to draw under the spanned text. The image and text will be horizontally centered.
private val mDrawable: Drawable
// The width if the drawable in dip
private var mDrawableWidth: Int
// The width if the drawable in dip
private var mDrawableHeight: Int
// Margin in dip to place around the drawable
private var mMargin: Int
// Amount to offset the text from the start.
private var mTextOffset = 0f
// Amount to offset the drawable from the start.
private var mDrawableOffset = 0f
// Descent specified in font metrics of the TextPaint.
private var mBaseDescent = 0f
init {
val metrics: DisplayMetrics = context.resources.displayMetrics
mDrawable = drawable
mDrawableWidth = TypedValue.applyDimension(
TypedValue.COMPLEX_UNIT_DIP, drawableWidth.toFloat(), metrics
).toInt()
mDrawableHeight = TypedValue.applyDimension(
TypedValue.COMPLEX_UNIT_DIP, drawableHeight.toFloat(), metrics
).toInt()
mMargin = TypedValue.applyDimension(
TypedValue.COMPLEX_UNIT_DIP, margin.toFloat(), metrics
).toInt()
}
override fun draw(
canvas: Canvas, text: CharSequence, start: Int, end: Int, x: Float, top: Int, y: Int,
bottom: Int, paint: Paint
) {
canvas.drawText(text, start, end, x + mTextOffset, y.toFloat(), paint)
mDrawable.setBounds(0, 0, mDrawableWidth, mDrawableHeight)
canvas.save()
canvas.translate(x + mDrawableOffset + mMargin, y + mBaseDescent + mMargin)
mDrawable.draw(canvas)
canvas.restore()
}
// ReplacementSpan override to determine the width that the text and drawable should occupy.
// The computed width is determined by the greater of the text width and the drawable width
// plus the requested margins.
override fun getSize(
paint: Paint, text: CharSequence?, start: Int, end: Int, fm: Paint.FontMetricsInt?
): Int {
val textWidth = paint.measureText(text, start, end)
val additionalWidthNeeded = mDrawableWidth + mMargin * 2 - textWidth
// If the width of the text is less than the width of our drawable, increase the text width
// to match the drawable's width; otherwise, just return the width of the text.
return if (additionalWidthNeeded >= 0) {
// Drawable is wider than text, so we need to offset the text to center it.
mTextOffset = additionalWidthNeeded / 2
textWidth + additionalWidthNeeded
} else {
// Text is wider than the drawable, so we need to offset the drawable to center it.
// We do not need to expand the width.
mDrawableOffset = -additionalWidthNeeded / 2
textWidth
}.toInt()
}
// Determine the height for the ReplacementSpan.
override fun chooseHeight(
text: CharSequence?, start: Int, end: Int, spanstartv: Int, lineHeight: Int,
fm: Paint.FontMetricsInt, paint: TextPaint
) {
// The text height must accommodate the size of the drawable. To make the accommodation,
// change the bottom of the font so there is enough room to fit the drawable between the
// font bottom and the font's descent.
val tpMetric = paint.fontMetrics
mBaseDescent = tpMetric.descent
val spaceAvailable = fm.descent - mBaseDescent
val spaceNeeded = mDrawableHeight + mMargin * 2
if (spaceAvailable < spaceNeeded) {
fm.descent += (spaceNeeded - spaceAvailable).toInt()
fm.bottom = fm.descent + (tpMetric.bottom - tpMetric.descent).toInt()
}
}
// StaticLayout prefers LineHeightSpan.WithDensity over this function.
override fun chooseHeight(
charSequence: CharSequence?, i: Int, i1: Int, i2: Int, i3: Int, fm: Paint.FontMetricsInt
) = throw IllegalStateException("LineHeightSpan.chooseHeight() called but is not supported.")
}