I had the same problem and I have stumbled upon Layout class when reading some other posts for doing this on EditText
. It provides everything you need to make this happen by manually drawing underline with canvas.
First I defined custom attributes for an easy customization in XML layout files
<declare-styleable name="UnderlinedTextView" >
<attr name="underlineHeight" format="dimension" />
<attr name="underlineOffset" format="dimension" />
<attr name="underlineColor" format="color" />
<attr name="underLinePosition" format="enum">
<enum name="baseline" value="0" />
<enum name="below" value="1" />
</attr>
</declare-styleable>
And a custom TextView
class
class UnderlinedTextView @JvmOverloads constructor(
context: Context, attrs: AttributeSet? = null, defStyleAttr: Int = 0
) : androidx.appcompat.widget.AppCompatTextView(context, attrs, defStyleAttr) {
@Retention(AnnotationRetention.SOURCE)
@IntDef(POSITION_BASELINE, POSITION_BELOW)
annotation class UnderLinePosition {
companion object {
const val POSITION_BASELINE = 0
const val POSITION_BELOW = 1
}
}
private val linePaint = Paint(Paint.ANTI_ALIAS_FLAG).apply {
style = Paint.Style.FILL
}
var lineColor: Int
get() = linePaint.color
set(value) {
if (linePaint.color != value) {
linePaint.color = value
invalidate()
}
}
var lineHeight: Float
get() = linePaint.strokeWidth
set(value) {
if (linePaint.strokeWidth != value) {
linePaint.strokeWidth = value
updateSpacing()
}
}
var lineTopOffset = 0F
set(value) {
if (field != value) {
field = value
updateSpacing()
}
}
@UnderLinePosition
var linePosition = POSITION_BASELINE
private val rect = Rect()
private var internalAdd: Float = lineSpacingExtra
private inline val extraSpace
get() = lineTopOffset + lineHeight
init {
val density = context.resources.displayMetrics.density
val typedArray = context.obtainStyledAttributes(attrs, R.styleable.UnderlinedTextView, defStyleAttr, 0)
lineColor = typedArray.getColor(R.styleable.UnderlinedTextView_underlineColor, currentTextColor)
lineTopOffset = typedArray.getDimension(R.styleable.UnderlinedTextView_underlineOffset, 0f)
lineHeight = typedArray.getDimension(R.styleable.UnderlinedTextView_underlineHeight, density * 1)
linePosition = typedArray.getInt(R.styleable.UnderlinedTextView_underLinePosition, POSITION_BASELINE)
typedArray.recycle()
}
override fun onMeasure(widthMeasureSpec: Int, heightMeasureSpec: Int) {
super.onMeasure(widthMeasureSpec, heightMeasureSpec)
setMeasuredDimension(measuredWidth, measuredHeight + (extraSpace + 0.5f).toInt())
}
override fun onDraw(canvas: Canvas?) {
canvas?.takeIf { !text.isNullOrEmpty() }?.let {
val count = lineCount
val layout = layout
var xStart: Float
var xStop: Float
var yStart: Float
var firstCharInLine: Int
var lastCharInLine: Int
var lastLine: Boolean
var offset: Int
val lineSpacing = lineSpacingExtra * lineSpacingMultiplier
for (i in 0 until count) {
val baseline = getLineBounds(i, rect)
lastLine = i == count - 1
offset = if (lastLine) 0 else 1
firstCharInLine = layout.getLineStart(i)
lastCharInLine = layout.getLineEnd(i)
xStart = layout.getPrimaryHorizontal(firstCharInLine)
xStop = layout.getPrimaryHorizontal(lastCharInLine - offset)
yStart = when (linePosition) {
POSITION_BASELINE -> baseline + lineTopOffset
POSITION_BELOW -> (rect.bottom + lineTopOffset) - if (lastLine) 0F else lineSpacing
else -> throw NotImplementedError("")
}
canvas.drawRect(xStart, yStart, xStop, yStart + lineHeight, linePaint)
}
}
super.onDraw(canvas)
}
private fun updateSpacing() {
setLineSpacing(internalAdd, 1f)
}
override fun setLineSpacing(add: Float, mult: Float) {
internalAdd = add
super.setLineSpacing(add + extraSpace, 1f)
}
}
Then it's usage is simple
<some.package.UnderlinedTextView
android:id="@+id/tvTest"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_alignParentBottom="true"
android:layout_marginBottom="10dp"
android:layout_marginLeft="20dp"
android:layout_marginRight="20dp"
android:gravity="center"
android:text="This is a demo text"
android:textSize="16sp"
app:underlineColor="#ffc112ef"
app:underlineHeight="3dp"/>
Final result
- Multi line
- Single line
与恶龙缠斗过久,自身亦成为恶龙;凝视深渊过久,深渊将回以凝视…