Android 中使用 SpannableString 的虚线下划线 TextView 未换行到下一行
Dotted underline TextView not wrapping to the next line using SpannableString in Android
我使用这个 完成了虚线下划线文本视图。但是虚线下划线 textview 没有换行到下一行 。我附上截图以供参考。请提出您的想法。谢谢
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)
}
}
*使用 SpannableStringbuilder 设置虚线 *
DottedUnderlineSpan dottedUnderlineSpan = new DottedUnderlineSpan(underlineColor, dottedString);
strBuilder.setSpan(dottedUnderlineSpan, start, end, Spanned.SPAN_INCLUSIVE_INCLUSIVE);
错误:
预计:
问题是 ReplacementSpan 不能跨越线边界。有关此问题的更多信息,请参阅 Drawing a rounded corner background on text。
您可以使用上述博客 post 中的解决方案,但我们可以根据您的要求简化该解决方案,如下所示:
这是一般程序:
- 将 Annotation 放置在 TextView 中我们想要加下划线的文本周围。
- 放置文本并在使用预绘制侦听器绘制之前捕获 TextView。此时,文本已按照将在屏幕上显示的方式进行布局。
- 用一个或多个 DottedUnderlineSpans 替换每个 Annotation 范围,确保每个下划线范围不跨越线边界。
- 从 ReplacementSpan 中去除尾随白色 space,因为我们不想在尾随白色 space.
下划线
- 替换 TextView 中的文本。
有点复杂,但它允许使用 DottedUnderlineSpan class。 这可能不是 100% 的解决方案,因为在某些情况下 ReplacementSpan 的宽度可能与文本的宽度不同。
不过,我确实建议您使用带有注释的自定义 TextView 来标记下划线的位置。这可能是最容易做到和理解的,并且不太可能产生不可预见的副作用。一般过程是如上所述用注释范围标记文本,但在自定义文本视图的 draw()
函数中解释这些注释范围以生成下划线。
我整理了一个小项目来演示这些方法。对于没有下划线文本的 TextView 的输出如下所示,一个使用 DottedUnderlineSpan 的下划线文本和一个在自定义 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
我稍微修改了一下。
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>
我在 Github (https://github.com/jaindiv26/DottedTextSample) 上发布了一个简单的示例。我已经按照 Deva 的方法进行了一些调整,它也适用于多行。看看这个例子。
在 Android.
中使用 SpannableString 在 TextView 中使用虚线下划线
1.使 DottedLineSpan 通用 Class.
class DottedLineSpan extends ReplacementSpan {
private Paint p = new Paint();
private int mWidth;
private String mSpan;
private float mSpanLength = 0F;
private boolean mLengthIsCached = false;
private Float mOffsetY = 0f;
DottedLineSpan(int _color, String _spannedText, Context context){
float mStrokeWidth = context.getResources().getDimension(R.dimen.stroke_width);
float mDashPathEffect = context.getResources().getDimension(R.dimen.dash_path_effect);
mOffsetY = context.getResources().getDimension(R.dimen.offset_y);
p = new Paint();
p.setColor(_color);
p.setStyle(Paint.Style.STROKE);
p.setPathEffect(new DashPathEffect(new float[]{mDashPathEffect, mDashPathEffect}, 0));
p.setStrokeWidth(mStrokeWidth);
mSpan = _spannedText;
mSpanLength = _spannedText.length();
}
@Override
public int getSize(Paint paint, CharSequence text, int start, int end, Paint.FontMetricsInt fm) {
mWidth = (int) paint.measureText(text, start, end);
return mWidth;
}
@Override
public void draw(Canvas canvas, CharSequence text, int start, int end, float x, int top, int y, int bottom, Paint paint) {
canvas.drawText(text, start, end, x, y, paint);
if(!mLengthIsCached)
mSpanLength = paint.measureText(mSpan);
Path path = new Path();
path.moveTo(x, y + mOffsetY);
path.lineTo(x + mSpanLength, y + mOffsetY);
canvas.drawPath(path, this.p);
}
}
2。在您的 activity.
中使用此代码
public class MainActivity extends AppCompatActivity {
private TextView textView;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
textView = findViewById(R.id.textView);
String string = "Android is a mobile operating system based on a modified version of the Linux kernel and other open source software, designed primarily for touchscreen mobile devices such as smartphones and tablets. ";
String textToUnderline = "modified version of the Linux kernel";
SpannableString text = new SpannableString(string);
int[] range = getStartingAndEndOfSentence(string, textToUnderline);
DottedLineSpan dottedLineSpan = new DottedLineSpan(R.color.colorPrimary, textToUnderline, this);
text.setSpan(dottedLineSpan, range[0], range[1], Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
textView.setText(text);
}
int[] getStartingAndEndOfSentence(String wholeString, String partOfAString) {
int[] range = new int[2];
String[] s1 = wholeString.split("\s+");
String[] s2 = partOfAString.split("\s+");
if (s2.length == 1) {
String word = s2[0];
range[0] = wholeString.indexOf(word);
range[1] = range[0] + word.length();
} else {
int length = 0;
for (int i = 0; i < s1.length; i++) {
length = length + s1[i].length() + 1;
if (s1[i].equals(s2[0])) {
if(s1[i+1].equals(s2[1])) {
range[0] = length - (s1[i].length() + 1);
range[1] = range[0] + partOfAString.length();
break;
}
}
}
}
return range;
}
}
我使用这个
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)
}
}
*使用 SpannableStringbuilder 设置虚线 *
DottedUnderlineSpan dottedUnderlineSpan = new DottedUnderlineSpan(underlineColor, dottedString);
strBuilder.setSpan(dottedUnderlineSpan, start, end, Spanned.SPAN_INCLUSIVE_INCLUSIVE);
错误:
问题是 ReplacementSpan 不能跨越线边界。有关此问题的更多信息,请参阅 Drawing a rounded corner background on text。
您可以使用上述博客 post 中的解决方案,但我们可以根据您的要求简化该解决方案,如下所示:
这是一般程序:
- 将 Annotation 放置在 TextView 中我们想要加下划线的文本周围。
- 放置文本并在使用预绘制侦听器绘制之前捕获 TextView。此时,文本已按照将在屏幕上显示的方式进行布局。
- 用一个或多个 DottedUnderlineSpans 替换每个 Annotation 范围,确保每个下划线范围不跨越线边界。
- 从 ReplacementSpan 中去除尾随白色 space,因为我们不想在尾随白色 space. 下划线
- 替换 TextView 中的文本。
有点复杂,但它允许使用 DottedUnderlineSpan class。 这可能不是 100% 的解决方案,因为在某些情况下 ReplacementSpan 的宽度可能与文本的宽度不同。
不过,我确实建议您使用带有注释的自定义 TextView 来标记下划线的位置。这可能是最容易做到和理解的,并且不太可能产生不可预见的副作用。一般过程是如上所述用注释范围标记文本,但在自定义文本视图的 draw()
函数中解释这些注释范围以生成下划线。
我整理了一个小项目来演示这些方法。对于没有下划线文本的 TextView 的输出如下所示,一个使用 DottedUnderlineSpan 的下划线文本和一个在自定义 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
我稍微修改了一下。
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>
我在 Github (https://github.com/jaindiv26/DottedTextSample) 上发布了一个简单的示例。我已经按照 Deva 的方法进行了一些调整,它也适用于多行。看看这个例子。
在 Android.
中使用 SpannableString 在 TextView 中使用虚线下划线1.使 DottedLineSpan 通用 Class.
class DottedLineSpan extends ReplacementSpan {
private Paint p = new Paint();
private int mWidth;
private String mSpan;
private float mSpanLength = 0F;
private boolean mLengthIsCached = false;
private Float mOffsetY = 0f;
DottedLineSpan(int _color, String _spannedText, Context context){
float mStrokeWidth = context.getResources().getDimension(R.dimen.stroke_width);
float mDashPathEffect = context.getResources().getDimension(R.dimen.dash_path_effect);
mOffsetY = context.getResources().getDimension(R.dimen.offset_y);
p = new Paint();
p.setColor(_color);
p.setStyle(Paint.Style.STROKE);
p.setPathEffect(new DashPathEffect(new float[]{mDashPathEffect, mDashPathEffect}, 0));
p.setStrokeWidth(mStrokeWidth);
mSpan = _spannedText;
mSpanLength = _spannedText.length();
}
@Override
public int getSize(Paint paint, CharSequence text, int start, int end, Paint.FontMetricsInt fm) {
mWidth = (int) paint.measureText(text, start, end);
return mWidth;
}
@Override
public void draw(Canvas canvas, CharSequence text, int start, int end, float x, int top, int y, int bottom, Paint paint) {
canvas.drawText(text, start, end, x, y, paint);
if(!mLengthIsCached)
mSpanLength = paint.measureText(mSpan);
Path path = new Path();
path.moveTo(x, y + mOffsetY);
path.lineTo(x + mSpanLength, y + mOffsetY);
canvas.drawPath(path, this.p);
}
}
2。在您的 activity.
中使用此代码public class MainActivity extends AppCompatActivity {
private TextView textView;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
textView = findViewById(R.id.textView);
String string = "Android is a mobile operating system based on a modified version of the Linux kernel and other open source software, designed primarily for touchscreen mobile devices such as smartphones and tablets. ";
String textToUnderline = "modified version of the Linux kernel";
SpannableString text = new SpannableString(string);
int[] range = getStartingAndEndOfSentence(string, textToUnderline);
DottedLineSpan dottedLineSpan = new DottedLineSpan(R.color.colorPrimary, textToUnderline, this);
text.setSpan(dottedLineSpan, range[0], range[1], Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
textView.setText(text);
}
int[] getStartingAndEndOfSentence(String wholeString, String partOfAString) {
int[] range = new int[2];
String[] s1 = wholeString.split("\s+");
String[] s2 = partOfAString.split("\s+");
if (s2.length == 1) {
String word = s2[0];
range[0] = wholeString.indexOf(word);
range[1] = range[0] + word.length();
} else {
int length = 0;
for (int i = 0; i < s1.length; i++) {
length = length + s1[i].length() + 1;
if (s1[i].equals(s2[0])) {
if(s1[i+1].equals(s2[1])) {
range[0] = length - (s1[i].length() + 1);
range[1] = range[0] + partOfAString.length();
break;
}
}
}
}
return range;
}
}