我试图在SpannableString
上使用简单的TextView
,基于我发现的UnderDotSpan课程(here)。
原始UnderDotSpan只在文本本身下方放置一个特定大小和颜色的点(不重叠)。我尝试的是首先正常使用它,然后使用自定义的drawable而不是点。
与正常的跨度使用相反,这个只是没有显示任何东西。甚至不是文本。
以下是正常范围的完成方式:
val text = "1"
val timeSpannable = SpannableString(text)
timeSpannable.setSpan(ForegroundColorSpan(0xff00ff00.toInt()), 0, text.length, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE)
textView.setText(timeSpannable);
它将显示绿色" 1"在TextView中。
但是当我尝试下一个spannable时,它(整个TextView内容:文本和点)根本不显示:
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)
// this also didn't work: textView.setText(spannable)
奇怪的是,在我使用的一个项目中,它在RecyclerView中运行良好,而在另一个项目中,它没有。
这是UnderDotSpan的代码:
class UnderDotSpan(private val mDotSize: Float, private val mDotColor: Int, private val mTextColor: Int) : ReplacementSpan() {
companion object {
@JvmStatic
private val DEFAULT_DOT_SIZE_IN_DP = 4
}
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) {}
override fun getSize(paint: Paint, text: CharSequence, start: Int, end: Int, fm: Paint.FontMetricsInt?) = Math.round(paint.measureText(text, start, end))
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.drawCircle(x + textSize / 2, bottom + mDotSize, mDotSize / 2, paint)
paint.color = mTextColor
canvas.drawText(text, start, end, x, y.toFloat(), paint)
}
}
请注意,TextView没有任何特殊属性,但无论如何我都会展示它:
<android.support.constraint.ConstraintLayout
xmlns:android="http://schemas.android.com/apk/res/android" xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools" android:layout_width="match_parent"
android:layout_height="match_parent" tools:context="com.example.user.myapplication.MainActivity">
<TextView android:id="@+id/textView"
android:layout_width="wrap_content" android:layout_height="wrap_content" android:text="Hello World!"
app:layout_constraintBottom_toBottomOf="parent" app:layout_constraintLeft_toLeftOf="parent"
app:layout_constraintRight_toRightOf="parent" app:layout_constraintTop_toTopOf="parent"/>
</android.support.constraint.ConstraintLayout>
我尝试从其他span类扩展,并尝试以其他方式将文本设置为TextView。
我还尝试过根据UnderDotSpan课程制作的其他跨栏课程。例如:
class UnderDrawableSpan(val drawable: Drawable, val drawableWidth: Int = drawable.intrinsicWidth, val drawableHeight: Int = drawable.intrinsicHeight, val margin: Int = 0) : ReplacementSpan() {
override fun getSize(paint: Paint, text: CharSequence, start: Int, end: Int, fm: Paint.FontMetricsInt?): Int = Math.round(paint.measureText(text, start, end))
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)
canvas.drawText(text, start, end, x, y.toFloat(), paint)
canvas.save()
canvas.translate(x + textSize / 2f - drawableWidth / 2f, y.toFloat() + margin)
if (drawableWidth != 0 && drawableHeight != 0)
drawable.setBounds(0, 0, drawableWidth, drawableHeight)
drawable.draw(canvas)
canvas.restore()
}
}
调试时,我发现draw
函数甚至没有被调用,而getSize
被调用(并返回&gt; 0值)。
为什么跨度不能显示在TextView上?
我使用它的方式有什么问题?
如何修复它,并使用此跨度?
为什么它会在其他更复杂的情况下起作用?
答案 0 :(得分:5)
基本问题是没有为ReplacementSpan
设置高度。正如source for ReplacementSpan
:
如果跨度覆盖整个文本,并且未设置高度,则不会为跨度调用draw(Canvas,CharSequence,int,int,float,int,int,int,Paint)}。
这是Archit Sureja发布的重复内容。在我的原始帖子中,我更新了ReplacementSpan
中getSize()
的高度,但我现在实现了LineHeightSpan.WithDensity
接口来执行相同操作。 (感谢 vovahost here获取此信息。)
但是,您提出的其他问题需要解决。
您提供的项目引发的问题是该点不适合它必须驻留的TextView
。你看到的是点的截断。如果点的大小超过文本宽度或高度,该怎么办?
首先,关于高度,界面chooseHeight()
的{{1}}方法通过将点的大小添加到字体&#39来调整LineHeightSpan.WithDensity
字体的底部。 ; s有效高度。为此,点的高度将添加到字体的底部:
TextView
(这是使用fontMetricsInt.bottom = fm.bottom + mDotSize.toInt();
填充的此答案的最后一次迭代的更改。由于此更改,TextView
不再需要TextView
{1}}类。虽然我添加了UnderDotSpan
,但并不是真的需要它。)
最后一个问题是,如果点宽于文本,则点在开始和结束处被截止。 TextView
在这里不起作用,因为点被截断并不是因为它被剪切到填充,而是因为它被剪切到我们所说的文本宽度在clipToPadding="false"
中。为了解决这个问题,我修改了getSize()
方法以检测点何时比文本测量宽,并增加返回值以匹配点的宽度。一个名为getSize()
的新值是必须应用于文本绘图的数量,以及使点适合的点。
最后一个问题是点的中心是文本底部下方点的半径而不是直径,因此绘制点的代码在mStartShim
中更改为:
draw()
(我还将代码更改为canvas.drawCircle(x + textSize / 2, bottom.toFloat(), mDotSize / 2, paint)
翻译,而不是添加偏移量。效果相同。)
结果如下:
<强> activity_main.xml中强>
Canvas
<强> MainActivity.java 强>
<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" />
<强> UnderDotSpan.kt 强>
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)
}
}
对于在文本下放置一个小的drawable的更一般情况,以下类是有效的,并且基于// 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) {
}
}
:
<强> UnderDrawableSpan.java 强>
UnderDotSpan
使用以下可绘制XML与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) {
}
}
会产生以下结果: (drawable的宽度和高度设置为UnderDrawableSpan
。文本的字体大小为12dp
。)
<强> gradient_drawable.xml 强>
24sp
答案 1 :(得分:2)
您的范围未显示,因为未设置高度未调用绘制方法。
请参阅此链接
https://developer.android.com/reference/android/text/style/ReplacementSpan.html
GetSize() - 返回范围的宽度。扩展类可以通过更新Paint.FontMetricsInt的属性来设置跨度的高度。如果跨度覆盖整个文本,并且未设置高度,则不会为跨度调用draw(Canvas,CharSequence,int,int,float,int,int,int,Paint)。
Paint.FontMetricsInt对象我们得到的所有变量都是0所以没有高度,所以不调用draw方法。
对于Paint.FontMatricsInt的工作方式,您可以参考此链接。
Meaning of top, ascent, baseline, descent, bottom, and leading in Android's FontMetrics
因此,我们在绘图对象的帮助下设置了Paint.FontMetricsInt,我们在getSize的参数中得到了它。
这是我的代码,我改变了与设定高度相关的一些事情。
class UnderDotSpan(private val mDotSize: Float, private val mDotColor: Int, private val mTextColor: Int) : ReplacementSpan() {
companion object {
@JvmStatic
private val DEFAULT_DOT_SIZE_IN_DP = 16
}
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) {}
override fun getSize(paint: Paint, text: CharSequence, start: Int, end: Int, fm: Paint.FontMetricsInt?): Int {
val asd = paint.getFontMetricsInt()
fm?.leading = asd.leading
fm?.top = asd.top
fm?.bottom = asd.bottom
fm?.ascent = asd.ascent
fm?.descent = asd.descent
return Math.round(measureText(paint, text, start, end))
}
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.drawCircle(x + textSize / 2, (bottom /2).toFloat(), mDotSize / 2, paint)
paint.color = mTextColor
canvas.drawText(text, start, end, x, y.toFloat(), paint)
}
private fun measureText(paint: Paint, text: CharSequence, start: Int, end: Int): Float {
return paint.measureText(text, start, end)
}
}
我得到的最终输出如下
更新的答案
用它在文字下面绘制圆圈
class UnderDotSpan(private val mDotSize: Float, private val mDotColor: Int, private val mTextColor: Int) : ReplacementSpan() {
companion object {
@JvmStatic
private val DEFAULT_DOT_SIZE_IN_DP = 4
}
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) {}
override fun getSize(paint: Paint, text: CharSequence, start: Int, end: Int, fm: Paint.FontMetricsInt?): Int {
val asd = paint.getFontMetricsInt()
fm?.leading = asd.leading + mDotSize.toInt()
fm?.top = asd.top
fm?.bottom = asd.bottom
fm?.ascent = asd.ascent
fm?.descent = asd.descent
return Math.round(paint.measureText(text, start, end))
}
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.drawCircle(x + textSize / 2, bottom + mDotSize, mDotSize / 2, paint)
paint.color = mTextColor
canvas.drawText(text, start, end, x, y.toFloat(), paint)
}
}
和最后一个IMP
val text = "1\n"
代替val text = "1"
答案 2 :(得分:-2)
在Time WW
2016-01-01T23:20:00.000000000 201601
2016-01-01T23:20:00.000000000 201601
2016-01-01T23:20:00.000000000 201601
2017-01-01T23:20:00.000000000 201701
2018-01-01T23:20:00.000000000 201801
上设置文字后再使用:
textview
示例:
textview.setMovementMethod(LinkMovementMethod.getInstance());
答案 3 :(得分:-3)
/*
* Set text with hashtag and mentions on TextView
* */
public void setTextOnTextView(String description, TextView tvDescription)
{
SpannableString hashText = new SpannableString(description);
Pattern pattern = Pattern.compile("@([A-Za-z0-9_-]+)");
Matcher matcher = pattern.matcher(hashText);
while (matcher.find()) {
final StyleSpan bold = new StyleSpan(android.graphics.Typeface.BOLD); // Span to make text bold
hashText.setSpan(bold, matcher.start(), matcher.end(), 0);
}
Pattern patternHash = Pattern.compile("#([A-Za-z0-9_-]+)");
Matcher matcherHash = patternHash.matcher(hashText);
while (matcherHash.find()) {
final StyleSpan bold = new StyleSpan(android.graphics.Typeface.BOLD); // Span to make text bold
hashText.setSpan(bold, matcherHash.start(), matcherHash.end(), 0);
}
tvDescription.setText(hashText);
tvDescription.setMovementMethod(LinkMovementMethod.getInstance());
}