我正在使用一个自定义的替换span来绘制特定单词周围的背景(在单词周围添加填充,单词由周围的符号标识,就像HTML标记一样),甚至可以更改这些单词的文本大小(尽管在本例中我没有这样做)。如果只有一行代码,所有内容都会按预期显示,那么就可以正常工作:
这是通过调整覆盖的getSize
中的FontMetrics,调整顶部和底部来添加背景填充,并调整返回的大小(宽度)来添加相同的填充来完成的,如下所示:
override fun getSize(paint: Paint, text: CharSequence, start: Int, end: Int, fm: Paint.FontMetricsInt?): Int {
if (textSize != null) {
paint.textSize = textSize
}
if (fm != null) {
val newMetrics = paint.fontMetricsInt
fm.descent = newMetrics.descent
fm.ascent = newMetrics.ascent
fm.leading = newMetrics.leading
fm.top = (newMetrics.top - strokeWidth - padding).roundToInt()
fm.bottom = (newMetrics.bottom + strokeWidth + padding).roundToInt()
}
return (padding + strokeWidth + paint.measureText(
text.subSequence(start + 1, end - 1).toString().uppercase()
) + padding + strokeWidth).roundToInt()
}
onDraw
负责将矩形和文本绘制到画布上。
这个问题来自于多行文本。我知道ReplacementSpan的内容不会被包装/拆分,整个ReplacementSpan都会被包装,这在本例中是意料之中的。当我们转到多行时,问题似乎出在芯片内的文本定位上。我得到了一些奇怪的顶部/底部的值,根据getSize()的字体度量,它们的大小与预期的不同。在第一行,文本显示在芯片的底部,第二行的文本显示在芯片的顶部:
在我看来,当有多行时,没有调整行的高度来处理额外的填充。我曾尝试在我的ReplacementSpan中实现LineHeightSpan,但这不起作用,因为它必须应用于整个段落。
我最接近让它工作的方法是应用一个LineHeightSpan,使用显式的高度:
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
mySpan.setSpan(LineHeightSpan.Standard(71),0,narrativeString.length-1,Spannable.SPAN_EXCLUSIVE_EXCLUSIVE)
}
但这并不是一个真正的解决方案,因为它不会根据ReplacementSpan的高度进行调整。而且它看起来有点偏了(方框看起来比正常高度高一点,文本看起来有点靠近底部,而不是居中):
除了为每个单词创建单独的视图并将它们插入FlexboxLayout之类的内容之外,还有什么方法可以让它正常工作吗
更新:我尝试了Zain建议的方法,遵循Zain建议的文章和repo,它也不起作用。
首先,水平填充不会影响“单词”的宽度。如果你把芯片放在两个相邻的单词上,填充就会重叠。其次,垂直填充实际上不会改变行的高度。如果添加垂直填充超出行高或相邻行上的筹码,则添加垂直填充会将背景重叠到其他行上。位于文本视图之外的任何填充(例如,在第一行之上、最后一行之下)都会被截断。
发布于 2021-10-01 23:57:45
getLineForOffset()
可用于检测跨度的多行文本:
val startLine = layout.getLineForOffset(getSpanStart(span))
val endLine = layout.getLineForOffset(getSpanEnd(span))
if (startLine == endLine) // single line span
else // multi-line span
在绘制到画布之前,可以使用唯一的渲染器处理每种情况。这允许使用不同的绘制工具处理文本的第一行、中间行和最后一行,以便跨行的区域看起来是连贯的:
这个repo很好地处理了这一点,它还考虑到了LTR/RTL文本方向:
/*
* Copyright (C) 2018 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.android.example.text.styling.roundedbg
import android.graphics.Canvas
import android.graphics.drawable.Drawable
import android.text.Layout
import kotlin.math.max
import kotlin.math.min
/**
* Base class for single and multi line rounded background renderers.
*
* @param horizontalPadding the padding to be applied to left & right of the background
* @param verticalPadding the padding to be applied to top & bottom of the background
*/
internal abstract class TextRoundedBgRenderer(
val horizontalPadding: Int,
val verticalPadding: Int
) {
/**
* Draw the background that starts at the {@code startOffset} and ends at {@code endOffset}.
*
* @param canvas Canvas to draw onto
* @param layout Layout that contains the text
* @param startLine the start line for the background
* @param endLine the end line for the background
* @param startOffset the character offset that the background should start at
* @param endOffset the character offset that the background should end at
*/
abstract fun draw(
canvas: Canvas,
layout: Layout,
startLine: Int,
endLine: Int,
startOffset: Int,
endOffset: Int
)
/**
* Get the top offset of the line and add padding into account so that there is a gap between
* top of the background and top of the text.
*
* @param layout Layout object that contains the text
* @param line line number
*/
protected fun getLineTop(layout: Layout, line: Int): Int {
return layout.getLineTopWithoutPadding(line) - verticalPadding
}
/**
* Get the bottom offset of the line and add padding into account so that there is a gap between
* bottom of the background and bottom of the text.
*
* @param layout Layout object that contains the text
* @param line line number
*/
protected fun getLineBottom(layout: Layout, line: Int): Int {
return layout.getLineBottomWithoutPadding(line) + verticalPadding
}
}
/**
* Draws the background for text that starts and ends on the same line.
*
* @param horizontalPadding the padding to be applied to left & right of the background
* @param verticalPadding the padding to be applied to top & bottom of the background
* @param drawable the drawable used to draw the background
*/
internal class SingleLineRenderer(
horizontalPadding: Int,
verticalPadding: Int,
val drawable: Drawable
) : TextRoundedBgRenderer(horizontalPadding, verticalPadding) {
override fun draw(
canvas: Canvas,
layout: Layout,
startLine: Int,
endLine: Int,
startOffset: Int,
endOffset: Int
) {
val lineTop = getLineTop(layout, startLine)
val lineBottom = getLineBottom(layout, startLine)
// get min of start/end for left, and max of start/end for right since we don't
// the language direction
val left = min(startOffset, endOffset)
val right = max(startOffset, endOffset)
drawable.setBounds(left, lineTop, right, lineBottom)
drawable.draw(canvas)
}
}
/**
* Draws the background for text that starts and ends on different lines.
*
* @param horizontalPadding the padding to be applied to left & right of the background
* @param verticalPadding the padding to be applied to top & bottom of the background
* @param drawableLeft the drawable used to draw left edge of the background
* @param drawableMid the drawable used to draw for whole line
* @param drawableRight the drawable used to draw right edge of the background
*/
internal class MultiLineRenderer(
horizontalPadding: Int,
verticalPadding: Int,
val drawableLeft: Drawable,
val drawableMid: Drawable,
val drawableRight: Drawable
) : TextRoundedBgRenderer(horizontalPadding, verticalPadding) {
override fun draw(
canvas: Canvas,
layout: Layout,
startLine: Int,
endLine: Int,
startOffset: Int,
endOffset: Int
) {
// draw the first line
val paragDir = layout.getParagraphDirection(startLine)
val lineEndOffset = if (paragDir == Layout.DIR_RIGHT_TO_LEFT) {
layout.getLineLeft(startLine) - horizontalPadding
} else {
layout.getLineRight(startLine) + horizontalPadding
}.toInt()
var lineBottom = getLineBottom(layout, startLine)
var lineTop = getLineTop(layout, startLine)
drawStart(canvas, startOffset, lineTop, lineEndOffset, lineBottom)
// for the lines in the middle draw the mid drawable
for (line in startLine + 1 until endLine) {
lineTop = getLineTop(layout, line)
lineBottom = getLineBottom(layout, line)
drawableMid.setBounds(
(layout.getLineLeft(line).toInt() - horizontalPadding),
lineTop,
(layout.getLineRight(line).toInt() + horizontalPadding),
lineBottom
)
drawableMid.draw(canvas)
}
val lineStartOffset = if (paragDir == Layout.DIR_RIGHT_TO_LEFT) {
layout.getLineRight(startLine) + horizontalPadding
} else {
layout.getLineLeft(startLine) - horizontalPadding
}.toInt()
// draw the last line
lineBottom = getLineBottom(layout, endLine)
lineTop = getLineTop(layout, endLine)
drawEnd(canvas, lineStartOffset, lineTop, endOffset, lineBottom)
}
/**
* Draw the first line of a multiline annotation. Handles LTR/RTL.
*
* @param canvas Canvas to draw onto
* @param start start coordinate for the background
* @param top top coordinate for the background
* @param end end coordinate for the background
* @param bottom bottom coordinate for the background
*/
private fun drawStart(canvas: Canvas, start: Int, top: Int, end: Int, bottom: Int) {
if (start > end) {
drawableRight.setBounds(end, top, start, bottom)
drawableRight.draw(canvas)
} else {
drawableLeft.setBounds(start, top, end, bottom)
drawableLeft.draw(canvas)
}
}
/**
* Draw the last line of a multiline annotation. Handles LTR/RTL.
*
* @param canvas Canvas to draw onto
* @param start start coordinate for the background
* @param top top position for the background
* @param end end coordinate for the background
* @param bottom bottom coordinate for the background
*/
private fun drawEnd(canvas: Canvas, start: Int, top: Int, end: Int, bottom: Int) {
if (start > end) {
drawableLeft.setBounds(end, top, start, bottom)
drawableLeft.draw(canvas)
} else {
drawableRight.setBounds(start, top, end, bottom)
drawableRight.draw(canvas)
}
}
}
存储库将这一点应用于a custom TextView
/**
* A TextView that can draw rounded background to the portions of the text. See
* [TextRoundedBgHelper] for more information.
*
* See [TextRoundedBgAttributeReader] for supported attributes.
*/
class RoundedBgTextView : AppCompatTextView {
private val textRoundedBgHelper: TextRoundedBgHelper
@JvmOverloads
constructor(
context: Context,
attrs: AttributeSet? = null,
defStyleAttr: Int = android.R.attr.textViewStyle
) : super(context, attrs, defStyleAttr) {
val attributeReader = TextRoundedBgAttributeReader(context, attrs)
textRoundedBgHelper = TextRoundedBgHelper(
horizontalPadding = attributeReader.horizontalPadding,
verticalPadding = attributeReader.verticalPadding,
drawable = attributeReader.drawable,
drawableLeft = attributeReader.drawableLeft,
drawableMid = attributeReader.drawableMid,
drawableRight = attributeReader.drawableRight
)
}
override fun onDraw(canvas: Canvas) {
// need to draw bg first so that text can be on top during super.onDraw()
if (text is Spanned && layout != null) {
canvas.withTranslation(totalPaddingLeft.toFloat(), totalPaddingTop.toFloat()) {
textRoundedBgHelper.draw(canvas, text as Spanned, layout)
}
}
super.onDraw(canvas)
}
}
/**
* Helper class to draw multi-line rounded background to certain parts of a text. The start/end
* positions of the backgrounds are annotated with [android.text.Annotation] class. Each annotation
* should have the annotation key set to **rounded**.
*
* i.e.:
* ```
* <!--without the quotes at the begining and end Android strips the whitespace and also starts
* the annotation at the wrong position-->
* <string name="ltr">"this is <annotation key="rounded">a regular</annotation> paragraph."</string>
* ```
*
* **Note:** BiDi text is not supported.
*
* @param horizontalPadding the padding to be applied to left & right of the background
* @param verticalPadding the padding to be applied to top & bottom of the background
* @param drawable the drawable used to draw the background
* @param drawableLeft the drawable used to draw left edge of the background
* @param drawableMid the drawable used to draw for whole line
* @param drawableRight the drawable used to draw right edge of the background
*/
class TextRoundedBgHelper(
val horizontalPadding: Int,
verticalPadding: Int,
drawable: Drawable,
drawableLeft: Drawable,
drawableMid: Drawable,
drawableRight: Drawable
) {
private val singleLineRenderer: TextRoundedBgRenderer by lazy {
SingleLineRenderer(
horizontalPadding = horizontalPadding,
verticalPadding = verticalPadding,
drawable = drawable
)
}
private val multiLineRenderer: TextRoundedBgRenderer by lazy {
MultiLineRenderer(
horizontalPadding = horizontalPadding,
verticalPadding = verticalPadding,
drawableLeft = drawableLeft,
drawableMid = drawableMid,
drawableRight = drawableRight
)
}
/**
* Call this function during onDraw of another widget such as TextView.
*
* @param canvas Canvas to draw onto
* @param text
* @param layout Layout that contains the text
*/
fun draw(canvas: Canvas, text: Spanned, layout: Layout) {
// ideally the calculations here should be cached since they are not cheap. However, proper
// invalidation of the cache is required whenever anything related to text has changed.
val spans = text.getSpans(0, text.length, Annotation::class.java)
spans.forEach { span ->
if (span.value.equals("rounded")) {
val spanStart = text.getSpanStart(span)
val spanEnd = text.getSpanEnd(span)
val startLine = layout.getLineForOffset(spanStart)
val endLine = layout.getLineForOffset(spanEnd)
// start can be on the left or on the right depending on the language direction.
val startOffset = (layout.getPrimaryHorizontal(spanStart)
+ -1 * layout.getParagraphDirection(startLine) * horizontalPadding).toInt()
// end can be on the left or on the right depending on the language direction.
val endOffset = (layout.getPrimaryHorizontal(spanEnd)
+ layout.getParagraphDirection(endLine) * horizontalPadding).toInt()
val renderer = if (startLine == endLine) singleLineRenderer else multiLineRenderer
renderer.draw(canvas, layout, startLine, endLine, startOffset, endOffset)
}
}
}
}
并且所需的跨度可绘制可以附加在具有TextRoundedBgAttributeReader中定义的附加属性的XML中。
示例用法:
<com.android.example.text.styling.roundedbg.RoundedBgTextView
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="@string/my_annotated_text"
app:roundedTextDrawable="@drawable/rounded"
app:roundedTextDrawableLeft="@drawable/rounded_left"
app:roundedTextDrawableMid="@drawable/rounded_mid"
app:roundedTextDrawableRight="@drawable/rounded_right" />
您可以在strings.xml中为跨度添加注释:
<string name="my_annotated_text">"this is <annotation key="rounded">a regular</annotation> paragraph."</string>
或者以编程的方式:
val span = SpannableString("this is my text value that needs to be spanned")
span.setSpan(Annotation("", "rounded"), 0, 10, Spannable.SPAN_EXCLUSIVE_EXCLUSIVE)
span.setSpan(Annotation("", "rounded"), 15, 19, Spannable.SPAN_EXCLUSIVE_EXCLUSIVE)
This article对此进行了深入的解释;提到的存储库具有samples of testing.
https://stackoverflow.com/questions/69410892
复制相似问题