Multi Line Dotted or Dashed Text-Underline

multi line dotted or dashed text-underline

h2 {
border-bottom: 1px dashed #999;
display: inline;
}

So you receive what you need.

But you have to keep in mind that <h2> is then (of course) no longer a block element. But you can "avoid" that by putting a <h2> in a <div>.

How to Add a Dotted Underline Beneath HTML Text

It's impossible without CSS. In fact, the <u> tag is simply adding text-decoration:underline to the text with the browser's built-in CSS.

Here's what you can do:

<html>
<head>
<!-- Other head stuff here, like title or meta -->

<style type="text/css">
u {
border-bottom: 1px dotted #000;
text-decoration: none;
}
</style>
</head>
<!-- Body, content here -->
</html>

Need to display multiline text with underlining lines of equal length

You can use an inline element such as <span> which will treat border-bottom like underline:

<p>
<span>
<del>Lorem ipsum dolor sit amet, consectetur adipisicing elit,</del> sed do
eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam
</span>
</p>

and CSS:

span {
border-bottom: 4px solid black;
}
del {
color: red;
}

Demo here.

Result using the markup above:

result in Chrome

EDIT:

1.

Extending @aefxx's answer, if you can use CSS3 try this:

.strike {
background-image: -moz-linear-gradient(top , rgba(255, 255, 255, 0) 34px, #000000 34px, #000000 38px);
background-image: -webkit-linear-gradient(rgba(255, 255, 255, 0) 34px, #000000 34px, #000000 38px);
background-image: linear-gradient(to bottom, rgba(255, 255, 255, 0) 34px, #000000 34px, #000000 38px);
background-repeat: repeat-y;
background-size: 100% 38px;
}
p {
line-height: 38px;
}
p:before {
background: #fff;
content:"\00a0";
display: inline-block;
height: 38px;
width: 50px;
}
del span {
color: red;
}

​Demo here - this will only work in the latest browsers including Firefox and Chrome.

Result in Chrome:

result using gradient


2.

If you're happy with justified text:

p,span {
border-bottom: 4px solid black;
line-height: 30px;
text-align: justify;
text-indent: 50px;
}
p>span {
padding-bottom: 5px;
vertical-align: top;
}
del span {
border-bottom: 0 none;
color: red;
}

Demo here. ​There are some issues with line-height but should be easy to figure out.

Result in Chrome:

Sample Image


Other than that, I'm afraid you might have to wrap your lines in some containers.

Dotted underline TextView not wrapping to the next line using SpannableString in Android

The problem is that a ReplacementSpan cannot cross a line boundary. See Drawing a rounded corner background on text for more information on this issue.

You could use the solution from the blog post mentioned above, but we can simplify that solution based upon your requirements as follows:

Here is the general procedure:

  1. Place Annotation spans around the text in the TextView that we want to underline.
  2. Let the text be laid out and catch the TextView just before drawing using a predraw listener. At this point the text is laid out as it will be displayed on the screen.
  3. Replace each Annotation span with one or more DottedUnderlineSpans ensuring that each underline span does not cross a line boundary.
  4. Strip trailing white space from the ReplacementSpan since we don't want to underline trailing white space.
  5. Replace the text in the TextView.

A little complicated, but it will allow the use of the DottedUnderlineSpan class. This may not be a 100% solution since the width of the ReplacementSpan may vary from the width of the text under certain circumstances.

I do, however, recommend that you use a custom TextView with annotations to mark the placement of the underlines. This is probably going to be the easiest to do and to understand and is unlikely to have unforeseen side effects. The general procedure is to mark the text with annotation spans as above, but interpret these annotation spans in the draw() function of a custom text view to produce the underlines.

I have put together a small project to demonstrate these methods. The output looks like the following for a TextView with no underlined text, one with underlined text using the DottedUnderlineSpan and one with underlined text in a custom TextView.

Sample Image

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

I reworked this a little.

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>

Dotted underlines don't render properly in Chrome

Use text-decoration-skip-ink: none (The default was changed to auto in https://crrev.com/514104.)

If you look closely, the gaps in the underline correspond to the loops at the bottom of the letters, which lie very close to the baseline of the font. It appears that the skip-ink algorithm is choosing to cut the underlines off here — but only for dotted and dashed, and only at certain font sizes.

https://crbug.com/808603 suggests that the underlying reason is that the dots and dashes are two pixels tall.

Any rules/conventions on using dashed and dotted hyperlinks?

There was a time, way back in the day, when a few folks tried to stick with the idea that dashed underlines were for contextual help. I think that was a carry over from old Windows help files.

But, since then, no, there is no rule or standards as to what the style of underline means in a hyperlink. For better or worse, the underline, itself, isn't even a standard anymore as lots of sites forgo them (which, IMHO, is more often than not a bad idea).

All that said, I do like the idea and the attempt and differentiating on-page interaction vs. a link that actually takes you somewhere else.

How to increase the gap between text and underlining in CSS

No, but you could go with something like border-bottom: 1px solid #000 and padding-bottom: 3px.

If you want the same color of the "underline" (which in my example is a border), you just leave out the color declaration, i.e. border-bottom-width: 1px and border-bottom-style: solid.

For multiline, you can wrap you multiline texts in a span inside the element. E.g. <a href="#"><span>insert multiline texts here</span></a> then just add border-bottom and padding on the <span> - Demo



Related Topics



Leave a reply



Submit