多行“ReplacementSpan”绘图问题
Multiline `ReplacementSpan` drawing issue
只要文本不太长,我的自定义替换跨度就可以工作,但一旦文本长于一行,跨度图就会完全分开。我的理解是 draw()
在这种情况下被调用两次导致 span 绘制两次。无法将第二次绘制调用与第一次绘制调用区分开来,让您可以控制要绘制的内容和绘制位置。 start
和 end
变得无用,因为它们报告了错误的值。
ReplacementSpan
应该也适用于多行文本吗?如果能帮助解决此问题,我将不胜感激。
当我将所选文本更改为 CustomReplacementSpan
:
时会发生这种情况
CustomReplacementSpan.kt
import android.graphics.Canvas
import android.graphics.Paint
import android.os.Build
import android.text.Layout
import android.text.StaticLayout
import android.text.TextPaint
import android.text.TextUtils
import android.text.style.ReplacementSpan
import androidx.core.graphics.withTranslation
class CustomReplacementSpan(val spanText: String, val color: Int) : ReplacementSpan() {
override fun getSize(paint: Paint, text: CharSequence?, start: Int, end: Int, fm: Paint.FontMetricsInt?): Int {
return paint.measureText(spanText).toInt()
}
override fun draw(
canvas: Canvas,
text: CharSequence?,
start: Int,
end: Int,
x: Float,
top: Int,
y: Int,
bottom: Int,
paint: Paint
) {
paint.color = color
canvas.drawMultilineText(
text = spanText,
textPaint = paint as TextPaint,
width = canvas.width,
x = x,
y = top.toFloat()
)
}
}
fun Canvas.drawMultilineText(
text: CharSequence,
textPaint: TextPaint,
width: Int,
x: Float,
y: Float,
start: Int = 0,
end: Int = text.length,
alignment: Layout.Alignment = Layout.Alignment.ALIGN_NORMAL,
spacingMult: Float = 1f,
spacingAdd: Float = 0f,
includePad: Boolean = true,
ellipsizedWidth: Int = width,
ellipsize: TextUtils.TruncateAt? = null
) {
val staticLayout =
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
StaticLayout.Builder.obtain(text, start, end, textPaint, width)
.setAlignment(alignment)
.setLineSpacing(spacingAdd, spacingMult)
.setIncludePad(includePad)
.setEllipsizedWidth(ellipsizedWidth)
.setEllipsize(ellipsize)
.build()
} else {
StaticLayout(
text, start, end, textPaint, width, alignment,
spacingMult, spacingAdd, includePad, ellipsize, ellipsizedWidth
)
}
staticLayout.draw(this, x, y)
}
private fun StaticLayout.draw(canvas: Canvas, x: Float, y: Float) {
canvas.withTranslation(x, y) {
draw(this)
}
}
MainActivity.kt
import android.os.Bundle
import android.text.Spannable
import android.view.View
import android.widget.EditText
import androidx.appcompat.app.AppCompatActivity
import androidx.core.content.ContextCompat
class MainActivity : AppCompatActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
}
fun applySpan(view: View) {
val editText = findViewById<EditText>(R.id.edit)
if (editText.selectionStart < 0 || editText.selectionEnd < 0) {
return
}
val fullText = editText.text
val text = fullText.subSequence(editText.selectionStart, editText.selectionEnd)
val span = CustomReplacementSpan(text.toString(), ContextCompat.getColor(this, android.R.color.holo_blue_dark))
editText.text.setSpan(span, editText.selectionStart, editText.selectionEnd, Spannable.SPAN_EXCLUSIVE_EXCLUSIVE)
}
}
activity_main.xml
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout 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"
android:orientation="vertical"
tools:context=".MainActivity">
<EditText
android:id="@+id/edit"
style="@style/Widget.AppCompat.EditText"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:textSize="20sp" />
<Button
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:onClick="applySpan"
android:text="Make it span" />
</LinearLayout>
换行显然是ReplacementSpan
做不到的。这是 Florina Muntenescu 发表的一篇文章 Drawing a rounded corner background on text 中的引述,她在博客中介绍了 spans 等内容。 (以下引用中的重点是我的。)
We need to draw a drawable together with the text. We can implement a custom ReplacementSpan to draw the background and the text ourselves. However ReplacementSpans cannot flow into the next line, therefore we will not be able to support a multi-line background. They would rather look like Chip, the Material Design component, where every element must fit on a single line.
这就是您遇到的问题。本文继续介绍您可能想要研究的可能解决方案。例如,可以使用本文中概述的一些技术来定义多个 ReplacementSpans
取决于换行符,就像对背景可绘制对象所做的那样。
还有其他跨度类型可能更适合您的目的。 Here 是它们的列表。
只要文本不太长,我的自定义替换跨度就可以工作,但一旦文本长于一行,跨度图就会完全分开。我的理解是 draw()
在这种情况下被调用两次导致 span 绘制两次。无法将第二次绘制调用与第一次绘制调用区分开来,让您可以控制要绘制的内容和绘制位置。 start
和 end
变得无用,因为它们报告了错误的值。
ReplacementSpan
应该也适用于多行文本吗?如果能帮助解决此问题,我将不胜感激。
当我将所选文本更改为 CustomReplacementSpan
:
CustomReplacementSpan.kt
import android.graphics.Canvas
import android.graphics.Paint
import android.os.Build
import android.text.Layout
import android.text.StaticLayout
import android.text.TextPaint
import android.text.TextUtils
import android.text.style.ReplacementSpan
import androidx.core.graphics.withTranslation
class CustomReplacementSpan(val spanText: String, val color: Int) : ReplacementSpan() {
override fun getSize(paint: Paint, text: CharSequence?, start: Int, end: Int, fm: Paint.FontMetricsInt?): Int {
return paint.measureText(spanText).toInt()
}
override fun draw(
canvas: Canvas,
text: CharSequence?,
start: Int,
end: Int,
x: Float,
top: Int,
y: Int,
bottom: Int,
paint: Paint
) {
paint.color = color
canvas.drawMultilineText(
text = spanText,
textPaint = paint as TextPaint,
width = canvas.width,
x = x,
y = top.toFloat()
)
}
}
fun Canvas.drawMultilineText(
text: CharSequence,
textPaint: TextPaint,
width: Int,
x: Float,
y: Float,
start: Int = 0,
end: Int = text.length,
alignment: Layout.Alignment = Layout.Alignment.ALIGN_NORMAL,
spacingMult: Float = 1f,
spacingAdd: Float = 0f,
includePad: Boolean = true,
ellipsizedWidth: Int = width,
ellipsize: TextUtils.TruncateAt? = null
) {
val staticLayout =
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
StaticLayout.Builder.obtain(text, start, end, textPaint, width)
.setAlignment(alignment)
.setLineSpacing(spacingAdd, spacingMult)
.setIncludePad(includePad)
.setEllipsizedWidth(ellipsizedWidth)
.setEllipsize(ellipsize)
.build()
} else {
StaticLayout(
text, start, end, textPaint, width, alignment,
spacingMult, spacingAdd, includePad, ellipsize, ellipsizedWidth
)
}
staticLayout.draw(this, x, y)
}
private fun StaticLayout.draw(canvas: Canvas, x: Float, y: Float) {
canvas.withTranslation(x, y) {
draw(this)
}
}
MainActivity.kt
import android.os.Bundle
import android.text.Spannable
import android.view.View
import android.widget.EditText
import androidx.appcompat.app.AppCompatActivity
import androidx.core.content.ContextCompat
class MainActivity : AppCompatActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
}
fun applySpan(view: View) {
val editText = findViewById<EditText>(R.id.edit)
if (editText.selectionStart < 0 || editText.selectionEnd < 0) {
return
}
val fullText = editText.text
val text = fullText.subSequence(editText.selectionStart, editText.selectionEnd)
val span = CustomReplacementSpan(text.toString(), ContextCompat.getColor(this, android.R.color.holo_blue_dark))
editText.text.setSpan(span, editText.selectionStart, editText.selectionEnd, Spannable.SPAN_EXCLUSIVE_EXCLUSIVE)
}
}
activity_main.xml
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout 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"
android:orientation="vertical"
tools:context=".MainActivity">
<EditText
android:id="@+id/edit"
style="@style/Widget.AppCompat.EditText"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:textSize="20sp" />
<Button
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:onClick="applySpan"
android:text="Make it span" />
</LinearLayout>
换行显然是ReplacementSpan
做不到的。这是 Florina Muntenescu 发表的一篇文章 Drawing a rounded corner background on text 中的引述,她在博客中介绍了 spans 等内容。 (以下引用中的重点是我的。)
We need to draw a drawable together with the text. We can implement a custom ReplacementSpan to draw the background and the text ourselves. However ReplacementSpans cannot flow into the next line, therefore we will not be able to support a multi-line background. They would rather look like Chip, the Material Design component, where every element must fit on a single line.
这就是您遇到的问题。本文继续介绍您可能想要研究的可能解决方案。例如,可以使用本文中概述的一些技术来定义多个 ReplacementSpans
取决于换行符,就像对背景可绘制对象所做的那样。
还有其他跨度类型可能更适合您的目的。 Here 是它们的列表。