在我看来,当有多行时,没有调整行高来处理额外的填充。
可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)
}
}
}
回购将其应用于自定义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)
}
}
}
}
并且可以在 XML 中附加所需的跨度可绘制对象,并在TextRoundedBgAttributeReader中定义其他属性。
示例用法:
<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 中对 span 进行注释:
<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)
本文对此进行了深入的解释;并且提到的回购有测试样本。