为什么我的spannable没有显示?

时间:2017-12-25 10:34:07

标签: android textview spannable spanned

背景

我试图在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上?

我使用它的方式有什么问题?

如何修复它,并使用此跨度?

为什么它会在其他更复杂的情况下起作用?

4 个答案:

答案 0 :(得分:5)

基本问题是没有为ReplacementSpan设置高度。正如source for ReplacementSpan

中所述
  

如果跨度覆盖整个文本,并且未设置高度,则不会为跨度调用draw(Canvas,CharSequence,int,int,float,int,int,int,Paint)}。

这是Archit Sureja发布的重复内容。在我的原始帖子中,我更新了ReplacementSpangetSize()的高度,但我现在实现了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) 翻译,而不是添加偏移量。效果相同。)

结果如下:

enter image description here

<强> 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。)

enter image description here

<强> 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)
    }
}

我得到的最终输出如下

enter image description here

  

更新的答案

用它在文字下面绘制圆圈

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());
}