虚线下划线 TextView 不会在 Android 中使用 SpannableString 换行到下一行 [英] Dotted underline TextView not wrapping to the next line using SpannableString in Android

查看:38
本文介绍了虚线下划线 TextView 不会在 Android 中使用 SpannableString 换行到下一行的处理方法,对大家解决问题具有一定的参考价值,需要的朋友们下面随着小编来一起学习吧!

问题描述

我使用这个 预期:

解决方案

问题在于 ReplacementSpan 不能跨越线边界.请参阅

MainActivity.kt

class MainActivity : AppCompatActivity() {private lateinit var textView0: TextViewprivate lateinit var textView1: TextViewprivate lateinit var textView2: TextView覆盖 fun onCreate(savedInstanceState: Bundle?) {super.onCreate(savedInstanceState)setContentView(R.layout.activity_main)textView0 = findViewById(R.id.textView0)textView1 = findViewById(R.id.textView1)textView2 = findViewById(R.id.textView2)如果(已保存的实例状态!= null){textView1.text = SpannableString(savedInstanceState.getCharSequence("textView1"))删除下划线跨度(textView1)textView2.text = SpannableString(savedInstanceState.getCharSequence("textView2"))} 别的 {val stringToUnderline = resources.getString(R.string.string_to_underline)val spannableString0 = SpannableString(stringToUnderline)val spannableString1 = SpannableString(stringToUnderline)val spannableString2 = SpannableString(stringToUnderline)//得到一个很好的带下划线的文本val toUnderline = listOf(生产或转换周期",材料",进入",商品",生产和转换周期,原材料被转换",可销售的成品.")toUnderline.forEach { str ->setAnnotation(spannableString0, str) }textView0.text = spannableString0toUnderline.forEach { str ->setAnnotation(spannableString1, str) }textView1.setText(spannableString1, TextView.BufferType.SPANNABLE)toUnderline.forEach { str ->setAnnotation(spannableString2, str) }textView2.setText(spannableString2, TextView.BufferType.SPANNABLE)}//让布局继续并在绘图发生之前捕获处理以添加下划线.textView1.viewTreeObserver.addOnPreDrawListener(对象:ViewTreeObserver.OnPreDrawListener {覆盖乐趣 onPreDraw(): Boolean {textView1.viewTreeObserver.removeOnPreDrawListener(this)setUnderlinesForAnnotations(textView1)返回假}})}//下面是使用的manifest文件指定//<activity android:configChanges="orientation">;否则,定向处理//发生在 onCreate()覆盖乐趣 onConfigurationChanged(newConfig: Configuration) {super.onConfigurationChanged(newConfig)删除下划线跨度(textView1)textView1.viewTreeObserver.addOnPreDrawListener(对象:ViewTreeObserver.OnPreDrawListener {覆盖乐趣 onPreDraw(): Boolean {textView1.viewTreeObserver.removeOnPreDrawListener(this)setUnderlinesForAnnotations(textView1)返回假}})}覆盖 fun onSaveInstanceState(outState: Bundle) {super.onSaveInstanceState(outState)outState.putCharSequence("textView1", textView1.text)outState.putCharSequence("textView2", textView2.text)}私人乐趣 setAnnotation(spannableString: SpannableString, subStringToUnderline: String) {val dottedAnnotation =注释(ANNOTATION_FOR_UNDERLINE_KEY, ANNOTATION_FOR_UNDERLINE_IS_DOTTED)val start = spannableString.indexOf(subStringToUnderline)如果(开始> = 0){val end = start + subStringToUnderline.lengthspannableString.setSpan(dottedAnnotation,开始,结束,Spanned.SPAN_INCLUSIVE_INCLUSIVE)}}私人乐趣 setUnderlinesForAnnotations(textView: TextView) {val text = SpannableString(textView.text)val 跨度 =text.getSpans(0, text.length, Annotation::class.java).filter { span ->span.key == ANNOTATION_FOR_UNDERLINE_KEY}如果 (spans.isNotEmpty()) {val 布局 = textView.layoutspans.forEach { span ->setUnderlineForAnnotation(文本,跨度,布局)}textView.setText(文本,TextView.BufferType.SPANNABLE)}}私人乐趣 setUnderlineForAnnotation(text: Spannable, span: Annotation, layout: Layout) {//跨度中第一个字符的偏移量val spanStart = text.getSpanStart(span)//第一个字符*过去*跨度结束的偏移量.val spanEnd = text.getSpanEnd(span)//text.removeSpan(span)//跨度从这一行开始val startLine = layout.getLineForOffset(spanStart)//保存跨度最后一个字符的行的偏移​​量.自从//spanEnd 是跨度结束后第一个字符的偏移量,我们需要//如果跨度在一行的末尾结束,则减去一.val endLine = layout.getLineForOffset(spanEnd)for (line in startLine..endLine) {//偏移到行的第一个字符.val lineStart = layout.getLineStart(line)//偏移到刚过此行末尾的字符.val lineEnd = layout.getLineEnd(line)//segStart..segEnd 覆盖了这一行的span部分.val segStart = max(spanStart, lineStart)var segEnd = min(spanEnd, lineEnd)//不想在行尾空白处加下划线.while ((segEnd > segStart) 和 Character.isWhitespace(text[segEnd - 1])) {段结束--}如果 (segEnd > segStart) {val dottedUnderlineSpan = DottedUnderlineSpan()text.setSpan(dottedUnderlineSpan, segStart, segEnd, Spanned.SPAN_INCLUSIVE_INCLUSIVE)}}}私人乐趣 removeUnderlineSpans(textView: TextView) {val text = SpannableString(textView.text)val spans = text.getSpans(0, text.length, DottedUnderlineSpan::class.java)spans.forEach { span ->text.removeSpan(跨度)}textView.setText(文本,TextView.BufferType.SPANNABLE)}伴生对象{const val ANNOTATION_FOR_UNDERLINE_KEY = "下划线"const val ANNOTATION_FOR_UNDERLINE_IS_DOTTED = "虚线"}}

DottedUnderlineSpan

我稍微修改了一下.

class DottedUnderlineSpan(lineColor: Int = Color.RED,dashPathEffect:DashPathEffect =DashPathEffect(floatArrayOf(DASHPATH_INTERVAL_ON, DASHPATH_INTERVAL_OFF), 0f),dashStrokeWidth: Float = DOTTEDSTROKEWIDTH) : ReplacementSpan() {私有 val mPaint = Paint()私有 val mPath = Path()在里面 {与(mPaint){颜色 = 线颜色样式 = Paint.Style.STROKEpathEffect = dashPathEffectstrokeWidth = dashStrokeWidth}}覆盖乐趣 getSize(油漆:油漆,文本:CharSequence,开始:Int,结束:Int,fm:Paint.FontMetricsInt?): 内部 {返回paint.measureText(文本,开始,结束).toInt()}覆盖有趣的画(画布:画布,文本:CharSequence,开始:Int,end: Int, x: Float, top: Int, y: Int, bottom: Int,paint: Paint){canvas.drawText(文本,开始,结束,x,y.toFloat(),油漆)val spanLength = paint.measureText(text.subSequence(start, end).toString())值偏移Y =Paint.fontMetrics.bottom -paint.fontMetrics.descent + TEXT_TO_UNDERLINE_SEPARATIONmPath.reset()mPath.moveTo(x, y + offsetY)mPath.lineTo(x + spanLength, y + offsetY)canvas.drawPath(mPath, mPaint)}伴生对象{const val DOTTEDSTROKEWIDTH = 5fconst val DASHPATH_INTERVAL_ON = 4fconst val DASHPATH_INTERVAL_OFF = 4fconst val TEXT_TO_UNDERLINE_SEPARATION = 3}}

UnderlineTextView

class UnderlineTextView @JvmOverloads 构造函数(上下文:上下文,属性:属性集?= null, defStyleAttr: Int = 0) : AppCompatTextView(context, attrs, defStyleAttr) {私有 val mPath = Path()私有 val mPaint = Paint()在里面 {与(mPaint){颜色 = Color.RED样式 = Paint.Style.STROKE路径效果 =DashPathEffect(floatArrayOf(DASHPATH_INTERVAL_ON, DASHPATH_INTERVAL_OFF), 0f)strokeWidth = DOTTEDSTROKEWIDTH}}覆盖有趣的画(画布:画布){super.draw(画布)//下划线位于文本的顶部.if (text is Spanned && layout != null) {canvas.withTranslation(totalPaddingStart.toFloat(), totalPaddingTop.toFloat()) {drawUnderlines(canvas, text as Spanned)}}}私人乐趣 drawUnderlines(canvas: Canvas, allText: Spanned) {val 跨度 =allText.getSpans(0, allText.length, Annotation::class.java).filter { span ->span.key == ANNOTATION_FOR_UNDERLINE_KEY &&span.value == ANNOTATION_FOR_UNDERLINE_IS_DOTTED}如果 (spans.isNotEmpty()) {spans.forEach { span ->drawUnderline(canvas, allText, span)}}}私人乐趣 drawUnderline(canvas: Canvas, allText: Spanned, span: Annotation) {//跨度中第一个字符的偏移量val spanStart = allText.getSpanStart(span)//第一个字符*过去*跨度结束的偏移量.val spanEnd = allText.getSpanEnd(span)//跨度从这一行开始val startLine = layout.getLineForOffset(spanStart)//保存跨度最后一个字符的行的偏移​​量.自从//spanEnd 是跨度结束后第一个字符的偏移量,我们需要//如果跨度在一行的末尾结束,则减去一.val endLine = layout.getLineForOffset(spanEnd - 1)for (line in startLine..endLine) {//行首字符的偏移量.val lineStart = layout.getLineStart(line)//段总是从起始线上的某个地方开始.对于其他线路,段//从零开始.val segStart = if (line == startLine) {最大(跨度开始,线开始)} 别的 {0}//偏移到刚过此行末尾的字符.val lineEnd = layout.getLineEnd(line)//segStart..segEnd 覆盖了这一行的span部分.val segEnd = min(spanEnd, lineEnd)//获取下划线的 x 轴坐标以计算跨度长度.还行吧//因为我们正在查看的部分仅限于一行.val startStringOnLine = layout.getPrimaryHorizo​​ntal(segStart)val endStringOnLine =如果(segEnd == lineEnd){//如果段在行尾结束,则获取最右边的位置//该行不包括我们不想加下划线的尾随空格.layout.getLineRight(line)} 别的 {//段的结尾在这一行,所以得到最后一个字符结尾的偏移量//在段中.layout.getPrimaryHorizo​​ntal(segEnd)}val spanLength = endStringOnLine - startStringOnLine//获取下划线的 y 坐标.val offsetY = layout.getLineBaseline(line) + TEXT_TO_UNDERLINE_SEPARATION//现在绘制下划线.mPath.reset()mPath.moveTo(startStringOnLine, offsetY)mPath.lineTo(startStringOnLine + spanLength, offsetY)canvas.drawPath(mPath, mPaint)}}有趣的 setUnderlineColor(underlineColor: Int) {mPaint.color = underlineColor}伴生对象{const val DOTTEDSTROKEWIDTH = 5fconst val DASHPATH_INTERVAL_ON = 4fconst val DASHPATH_INTERVAL_OFF = 4fconst val TEXT_TO_UNDERLINE_SEPARATION = 3fconst val ANNOTATION_FOR_UNDERLINE_KEY = "下划线"const val ANNOTATION_FOR_UNDERLINE_IS_DOTTED = "虚线"}}

activity_main.xml

<androidx.constraintlayout.widget.ConstraintLayoutandroid:layout_width="match_parent"android:layout_height="wrap_content"工具:上下文=.MainActivity"><文本视图android:id="@+id/Label0"android:layout_width="wrap_content"android:layout_height="wrap_content"android:layout_marginTop="16dp"android:text="纯文本"应用程序:layout_constraintBottom_toTopOf="@+id/textView0"应用程序:layout_constraintEnd_toEndOf="父"应用程序:layout_constraintStart_toStartOf="parent"应用程序:layout_constraintTop_toTopOf="父"应用程序:layout_constraintVertical_bias="0.0"app:layout_constraintVertical_chainStyle="packed"/><文本视图android:id="@+id/textView0"android:layout_width="188dp"android:layout_height="wrap_content"android:layout_marginTop="8dp"机器人:背景=#DDD6D6"机器人:paddingBottom =2dp"android:text="@string/string_to_underline"android:textAppearance="@style/TextAppearance.AppCompat.Body1"应用程序:layout_constraintBottom_toTopOf="@+id/label1"应用程序:layout_constraintEnd_toEndOf="父"应用程序:layout_constraintStart_toStartOf="parent"app:layout_constraintTop_toBottomOf="@+id/Label0"/><文本视图android:id="@+id/label1"android:layout_width="wrap_content"android:layout_height="wrap_content"android:layout_marginTop="16dp"android:text="DottedUndelineSpan"应用程序:layout_constraintBottom_toTopOf="@+id/textView1"应用程序:layout_constraintEnd_toEndOf="父"应用程序:layout_constraintStart_toStartOf="parent"app:layout_constraintTop_toBottomOf="@+id/textView0"/><文本视图android:id="@+id/textView1"android:layout_width="188dp"android:layout_height="wrap_content"android:layout_marginTop="8dp"机器人:背景=#DDD6D6"机器人:paddingBottom =2dp"android:text="@string/string_to_underline"android:textAppearance="@style/TextAppearance.AppCompat.Body1"应用程序:layout_constraintBottom_toTopOf="@+id/label2"应用程序:layout_constraintEnd_toEndOf="父"应用程序:layout_constraintStart_toStartOf="parent"app:layout_constraintTop_toBottomOf="@+id/label1"/><文本视图android:id="@+id/label2"android:layout_width="wrap_content"android:layout_height="wrap_content"android:layout_marginTop="16dp"android:text="UnderlineTextView"应用程序:layout_constraintBottom_toTopOf="@+id/textView2"应用程序:layout_constraintEnd_toEndOf="父"应用程序:layout_constraintStart_toStartOf="parent"app:layout_constraintTop_toBottomOf="@+id/textView1"/><com.example.dottedunderlinespan.UnderlineTextViewandroid:id="@+id/textView2"android:layout_width="188dp"android:layout_height="wrap_content"android:layout_marginTop="8dp"机器人:背景=#DDD6D6"机器人:paddingBottom =2dp"android:text="@string/string_to_underline"android:textAppearance="@style/TextAppearance.AppCompat.Body1"应用程序:layout_constraintBottom_toBottomOf="parent"应用程序:layout_constraintEnd_toEndOf="父"应用程序:layout_constraintStart_toStartOf="parent"app:layout_constraintTop_toBottomOf="@+id/label2"/></androidx.constraintlayout.widget.ConstraintLayout></滚动视图>

I done the dotted underline textview using this Dotted underline in TextView using SpannableString in Android. But dotted underline textview not wrapping to the next line. I have attached screenshot for reference. Please advice your ideas. Thanks

class DottedUnderlineSpan(mColor: Int, private val mSpan: String) : ReplacementSpan() {
    private val paint: Paint
    private var width: Int = 0
    private var spanLength: Float = 0f
    private val lengthIsCached = false
    internal var strokeWidth: Float = 0f
    internal var dashPathEffect: Float = 0f
    internal var offsetY: Float = 0f

    init {
        strokeWidth = 5f
        dashPathEffect = 4f
        offsetY = 14f
        paint = Paint()
        paint.color = mColor
        paint.style = Paint.Style.STROKE
        paint.pathEffect = DashPathEffect(floatArrayOf(dashPathEffect, dashPathEffect), 0f)
        paint.strokeWidth = strokeWidth
    }

    override fun getSize(paint: Paint, text: CharSequence, start: Int, end: Int, fm: Paint.FontMetricsInt?): Int {
        width = paint.measureText(text, start, end).toInt()
        return width
    }

    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, y.toFloat(), paint)

        if (!lengthIsCached)
            spanLength = paint.measureText(mSpan)
        val path = Path()
        path.moveTo(x, y + offsetY)
        path.lineTo(x + spanLength, y + offsetY)
        canvas.drawPath(path, this.paint)
    }
}

*Set dotted line using SpannableStringbuilder *

   DottedUnderlineSpan dottedUnderlineSpan = new DottedUnderlineSpan(underlineColor, dottedString);
                    strBuilder.setSpan(dottedUnderlineSpan, start, end, Spanned.SPAN_INCLUSIVE_INCLUSIVE);

Error:

Expected:

解决方案

The problem is that a ReplacementSpan cannot cross a line boundary. See Drawing a rounded corner background on text for more information on this issue.

You could use the solution from the blog post mentioned above, but we can simplify that solution based upon your requirements as follows:

Here is the general procedure:

  1. Place Annotation spans around the text in the TextView that we want to underline.
  2. Let the text be laid out and catch the TextView just before drawing using a predraw listener. At this point the text is laid out as it will be displayed on the screen.
  3. Replace each Annotation span with one or more DottedUnderlineSpans ensuring that each underline span does not cross a line boundary.
  4. Strip trailing white space from the ReplacementSpan since we don't want to underline trailing white space.
  5. Replace the text in the TextView.

A little complicated, but it will allow the use of the DottedUnderlineSpan class. This may not be a 100% solution since the width of the ReplacementSpan may vary from the width of the text under certain circumstances.

I do, however, recommend that you use a custom TextView with annotations to mark the placement of the underlines. This is probably going to be the easiest to do and to understand and is unlikely to have unforeseen side effects. The general procedure is to mark the text with annotation spans as above, but interpret these annotation spans in the draw() function of a custom text view to produce the underlines.

I have put together a small project to demonstrate these methods. The output looks like the following for a TextView with no underlined text, one with underlined text using the DottedUnderlineSpan and one with underlined text in a custom TextView.

MainActivity.kt

class MainActivity : AppCompatActivity() {
    private lateinit var textView0: TextView
    private lateinit var textView1: TextView
    private lateinit var textView2: TextView

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_main)

        textView0 = findViewById(R.id.textView0)
        textView1 = findViewById(R.id.textView1)
        textView2 = findViewById<UnderlineTextView>(R.id.textView2)

        if (savedInstanceState != null) {
            textView1.text = SpannableString(savedInstanceState.getCharSequence("textView1"))
            removeUnderlineSpans(textView1)
            textView2.text = SpannableString(savedInstanceState.getCharSequence("textView2"))
        } else {
            val stringToUnderline = resources.getString(R.string.string_to_underline)
            val spannableString0 = SpannableString(stringToUnderline)
            val spannableString1 = SpannableString(stringToUnderline)
            val spannableString2 = SpannableString(stringToUnderline)

            // Get a good selection of underlined text
            val toUnderline = listOf(
                "production or conversion cycle",
                "materials",
                "into",
                "goods",
                "production and conversion cycle, where raw materials are transformed",
                "saleable finished goods."
            )

            toUnderline.forEach { str -> setAnnotation(spannableString0, str) }
            textView0.text = spannableString0

            toUnderline.forEach { str -> setAnnotation(spannableString1, str) }
            textView1.setText(spannableString1, TextView.BufferType.SPANNABLE)

            toUnderline.forEach { str -> setAnnotation(spannableString2, str) }
            textView2.setText(spannableString2, TextView.BufferType.SPANNABLE)
        }

        // Let the layout proceed and catch processing before drawing occurs to add underlines.
        textView1.viewTreeObserver.addOnPreDrawListener(
            object : ViewTreeObserver.OnPreDrawListener {
                override fun onPreDraw(): Boolean {
                    textView1.viewTreeObserver.removeOnPreDrawListener(this)
                    setUnderlinesForAnnotations(textView1)
                    return false
                }
            }
        )
    }

    // The following is used of the manifest file specifies
    // <activity android:configChanges="orientation">; otherwise, orientation processing
    // occurs in onCreate()
    override fun onConfigurationChanged(newConfig: Configuration) {
        super.onConfigurationChanged(newConfig)

        removeUnderlineSpans(textView1)
        textView1.viewTreeObserver.addOnPreDrawListener(
            object : ViewTreeObserver.OnPreDrawListener {
                override fun onPreDraw(): Boolean {
                    textView1.viewTreeObserver.removeOnPreDrawListener(this)
                    setUnderlinesForAnnotations(textView1)
                    return false
                }
            }
        )
    }

    override fun onSaveInstanceState(outState: Bundle) {
        super.onSaveInstanceState(outState)
        outState.putCharSequence("textView1", textView1.text)
        outState.putCharSequence("textView2", textView2.text)
    }

    private fun setAnnotation(spannableString: SpannableString, subStringToUnderline: String) {
        val dottedAnnotation =
            Annotation(ANNOTATION_FOR_UNDERLINE_KEY, ANNOTATION_FOR_UNDERLINE_IS_DOTTED)
        val start = spannableString.indexOf(subStringToUnderline)
        if (start >= 0) {
            val end = start + subStringToUnderline.length
            spannableString.setSpan(dottedAnnotation, start, end, Spanned.SPAN_INCLUSIVE_INCLUSIVE)
        }
    }

    private fun setUnderlinesForAnnotations(textView: TextView) {
        val text = SpannableString(textView.text)
        val spans =
            text.getSpans(0, text.length, Annotation::class.java).filter { span ->
                span.key == ANNOTATION_FOR_UNDERLINE_KEY
            }
        if (spans.isNotEmpty()) {
            val layout = textView.layout
            spans.forEach { span ->
                setUnderlineForAnnotation(text, span, layout)
            }
            textView.setText(text, TextView.BufferType.SPANNABLE)
        }
    }

    private fun setUnderlineForAnnotation(text: Spannable, span: Annotation, layout: Layout) {
        // Offset of first character in span
        val spanStart = text.getSpanStart(span)

        // Offset of first character *past* the end of the span.
        val spanEnd = text.getSpanEnd(span)
//        text.removeSpan(span)

        // The span starts on this line
        val startLine = layout.getLineForOffset(spanStart)

        // Offset of the line that holds the last character of the span. Since
        // spanEnd is the offset of the first character past the end of the span, we need
        // to subtract one in case the span ends at the end of a line.
        val endLine = layout.getLineForOffset(spanEnd)

        for (line in startLine..endLine) {

            // Offset to first character of the line.
            val lineStart = layout.getLineStart(line)

            // Offset to the character just past the end of this line.
            val lineEnd = layout.getLineEnd(line)

            // segStart..segEnd covers the part of the span on this line.
            val segStart = max(spanStart, lineStart)
            var segEnd = min(spanEnd, lineEnd)

            // Don't want to underline end-of-line white space.
            while ((segEnd > segStart) and Character.isWhitespace(text[segEnd - 1])) {
                segEnd--
            }
            if (segEnd > segStart) {
                val dottedUnderlineSpan = DottedUnderlineSpan()
                text.setSpan(
                    dottedUnderlineSpan, segStart, segEnd, Spanned.SPAN_INCLUSIVE_INCLUSIVE
                )
            }
        }
    }

    private fun removeUnderlineSpans(textView: TextView) {
        val text = SpannableString(textView.text)
        val spans = text.getSpans(0, text.length, DottedUnderlineSpan::class.java)
        spans.forEach { span ->
            text.removeSpan(span)
        }
        textView.setText(text, TextView.BufferType.SPANNABLE)
    }

    companion object {
        const val ANNOTATION_FOR_UNDERLINE_KEY = "underline"
        const val ANNOTATION_FOR_UNDERLINE_IS_DOTTED = "dotted"
    }
}

DottedUnderlineSpan

I reworked this a little.

class DottedUnderlineSpan(
    lineColor: Int = Color.RED,
    dashPathEffect: DashPathEffect =
        DashPathEffect(
            floatArrayOf(DASHPATH_INTERVAL_ON, DASHPATH_INTERVAL_OFF), 0f
        ),
    dashStrokeWidth: Float = DOTTEDSTROKEWIDTH
) : ReplacementSpan() {
    private val mPaint = Paint()
    private val mPath = Path()

    init {
        with(mPaint) {
            color = lineColor
            style = Paint.Style.STROKE
            pathEffect = dashPathEffect
            strokeWidth = dashStrokeWidth
        }
    }

    override fun getSize(
        paint: Paint, text: CharSequence, start: Int, end: Int, fm: Paint.FontMetricsInt?
    ): Int {
        return paint.measureText(text, start, end).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, y.toFloat(), paint)

        val spanLength = paint.measureText(text.subSequence(start, end).toString())
        val offsetY =
            paint.fontMetrics.bottom - paint.fontMetrics.descent + TEXT_TO_UNDERLINE_SEPARATION
        mPath.reset()
        mPath.moveTo(x, y + offsetY)
        mPath.lineTo(x + spanLength, y + offsetY)
        canvas.drawPath(mPath, mPaint)
    }

    companion object {
        const val DOTTEDSTROKEWIDTH = 5f
        const val DASHPATH_INTERVAL_ON = 4f
        const val DASHPATH_INTERVAL_OFF = 4f
        const val TEXT_TO_UNDERLINE_SEPARATION = 3
    }
}

UnderlineTextView

class UnderlineTextView @JvmOverloads constructor(
    context: Context, attrs: AttributeSet? = null, defStyleAttr: Int = 0
) : AppCompatTextView(context, attrs, defStyleAttr) {

    private val mPath = Path()
    private val mPaint = Paint()

    init {
        with(mPaint) {
            color = Color.RED
            style = Paint.Style.STROKE
            pathEffect =
                DashPathEffect(
                    floatArrayOf(DASHPATH_INTERVAL_ON, DASHPATH_INTERVAL_OFF), 0f
                )
            strokeWidth = DOTTEDSTROKEWIDTH
        }
    }

    override fun draw(canvas: Canvas) {
        super.draw(canvas)

        // Underline goes on top of the text.
        if (text is Spanned && layout != null) {
            canvas.withTranslation(totalPaddingStart.toFloat(), totalPaddingTop.toFloat()) {
                drawUnderlines(canvas, text as Spanned)
            }
        }
    }

    private fun drawUnderlines(canvas: Canvas, allText: Spanned) {
        val spans =
            allText.getSpans(0, allText.length, Annotation::class.java).filter { span ->
                span.key == ANNOTATION_FOR_UNDERLINE_KEY && span.value == ANNOTATION_FOR_UNDERLINE_IS_DOTTED
            }
        if (spans.isNotEmpty()) {
            spans.forEach { span ->
                drawUnderline(canvas, allText, span)
            }
        }
    }

    private fun drawUnderline(canvas: Canvas, allText: Spanned, span: Annotation) {
        // Offset of first character in span
        val spanStart = allText.getSpanStart(span)

        // Offset of first character *past* the end of the span.
        val spanEnd = allText.getSpanEnd(span)

        // The span starts on this line
        val startLine = layout.getLineForOffset(spanStart)

        // Offset of the line that holds the last character of the span. Since
        // spanEnd is the offset of the first character past the end of the span, we need
        // to subtract one in case the span ends at the end of a line.
        val endLine = layout.getLineForOffset(spanEnd - 1)

        for (line in startLine..endLine) {
            // Offset of first character of the line.
            val lineStart = layout.getLineStart(line)

            // The segment always start somewhere on the start line. For other lines, the segment
            // starts at zero.
            val segStart = if (line == startLine) {
                max(spanStart, lineStart)
            } else {
                0
            }

            // Offset to the character just past the end of this line.
            val lineEnd = layout.getLineEnd(line)

            // segStart..segEnd covers the part of the span on this line.
            val segEnd = min(spanEnd, lineEnd)

            // Get x-axis coordinate for the underline to compute the span length. This is OK
            // since the segment we are looking at is confined to a single line.
            val startStringOnLine = layout.getPrimaryHorizontal(segStart)
            val endStringOnLine =
                if (segEnd == lineEnd) {
                    // If segment ends at the line's end, then get the rightmost position on
                    // the line not imcluding trailing white space which we don't want to underline.
                    layout.getLineRight(line)
                } else {
                    // The segment's end is on this line, so get offset to end of the last character
                    // in the segment.
                    layout.getPrimaryHorizontal(segEnd)
                }
            val spanLength = endStringOnLine - startStringOnLine

            // Get the y-coordinate for the underline.
            val offsetY = layout.getLineBaseline(line) + TEXT_TO_UNDERLINE_SEPARATION

            // Now draw the underline.
            mPath.reset()
            mPath.moveTo(startStringOnLine, offsetY)
            mPath.lineTo(startStringOnLine + spanLength, offsetY)
            canvas.drawPath(mPath, mPaint)

        }
    }

    fun setUnderlineColor(underlineColor: Int) {
        mPaint.color = underlineColor
    }

    companion object {
        const val DOTTEDSTROKEWIDTH = 5f
        const val DASHPATH_INTERVAL_ON = 4f
        const val DASHPATH_INTERVAL_OFF = 4f
        const val TEXT_TO_UNDERLINE_SEPARATION = 3f
        const val ANNOTATION_FOR_UNDERLINE_KEY = "underline"
        const val ANNOTATION_FOR_UNDERLINE_IS_DOTTED = "dotted"
    }
}

activity_main.xml

<ScrollView 
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:padding="16dp"
    tools:context=".MainActivity">

    <androidx.constraintlayout.widget.ConstraintLayout
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        tools:context=".MainActivity">

        <TextView
            android:id="@+id/Label0"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:layout_marginTop="16dp"
            android:text="Plain Text"
            app:layout_constraintBottom_toTopOf="@+id/textView0"
            app:layout_constraintEnd_toEndOf="parent"
            app:layout_constraintStart_toStartOf="parent"
            app:layout_constraintTop_toTopOf="parent"
            app:layout_constraintVertical_bias="0.0"
            app:layout_constraintVertical_chainStyle="packed" />

        <TextView
            android:id="@+id/textView0"
            android:layout_width="188dp"
            android:layout_height="wrap_content"
            android:layout_marginTop="8dp"
            android:background="#DDD6D6"
            android:paddingBottom="2dp"
            android:text="@string/string_to_underline"
            android:textAppearance="@style/TextAppearance.AppCompat.Body1"
            app:layout_constraintBottom_toTopOf="@+id/label1"
            app:layout_constraintEnd_toEndOf="parent"
            app:layout_constraintStart_toStartOf="parent"
            app:layout_constraintTop_toBottomOf="@+id/Label0" />

        <TextView
            android:id="@+id/label1"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:layout_marginTop="16dp"
            android:text="DottedUndelineSpan"
            app:layout_constraintBottom_toTopOf="@+id/textView1"
            app:layout_constraintEnd_toEndOf="parent"
            app:layout_constraintStart_toStartOf="parent"
            app:layout_constraintTop_toBottomOf="@+id/textView0" />

        <TextView
            android:id="@+id/textView1"
            android:layout_width="188dp"
            android:layout_height="wrap_content"
            android:layout_marginTop="8dp"
            android:background="#DDD6D6"
            android:paddingBottom="2dp"
            android:text="@string/string_to_underline"
            android:textAppearance="@style/TextAppearance.AppCompat.Body1"
            app:layout_constraintBottom_toTopOf="@+id/label2"
            app:layout_constraintEnd_toEndOf="parent"
            app:layout_constraintStart_toStartOf="parent"
            app:layout_constraintTop_toBottomOf="@+id/label1" />

        <TextView
            android:id="@+id/label2"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:layout_marginTop="16dp"
            android:text="UnderlineTextView"
            app:layout_constraintBottom_toTopOf="@+id/textView2"
            app:layout_constraintEnd_toEndOf="parent"
            app:layout_constraintStart_toStartOf="parent"
            app:layout_constraintTop_toBottomOf="@+id/textView1" />

        <com.example.dottedunderlinespan.UnderlineTextView
            android:id="@+id/textView2"
            android:layout_width="188dp"
            android:layout_height="wrap_content"
            android:layout_marginTop="8dp"
            android:background="#DDD6D6"
            android:paddingBottom="2dp"
            android:text="@string/string_to_underline"
            android:textAppearance="@style/TextAppearance.AppCompat.Body1"
            app:layout_constraintBottom_toBottomOf="parent"
            app:layout_constraintEnd_toEndOf="parent"
            app:layout_constraintStart_toStartOf="parent"
            app:layout_constraintTop_toBottomOf="@+id/label2" />


    </androidx.constraintlayout.widget.ConstraintLayout>
</ScrollView>

这篇关于虚线下划线 TextView 不会在 Android 中使用 SpannableString 换行到下一行的文章就介绍到这了,希望我们推荐的答案对大家有所帮助,也希望大家多多支持IT屋!

查看全文
登录 关闭
扫码关注1秒登录
发送“验证码”获取 | 15天全站免登陆