Sometimes we’ll get a UI image with text 24px above the parent layout

There’s a problem if we write this on the layout

<TextView
    android:id="@+id/textview"
    android:layout_width="wrap_content"
    android:layout_height="wrap_content"
    android:background="#2196F3"
    android:text="Digital 3 c"
    android:textSize="20dp"
    android:layout_marginTop="24px"
    app:layout_constraintLeft_toLeftOf="parent"
    app:layout_constraintTop_toTopOf="parent" />
Copy the code

We set the top margin to be 24. But it turns out that the actual distance from the text is greater than 24

So what do you do

Most people would say add this attribute android: includeFontPadding = “false”

It looks a little better, but it’s a little off

My solution

Let’s take a look at a few lines drawn by Textview

  • Top: The highest point that can be drawn
  • Ascent: Recommended upper edge line
  • -Leonard: The base line
  • Decent: Recommended lower edge line
  • Bottom: The lowest point that can be drawn

Say first android: includeFontPadding = “false” this attribute is just put the top down to the ascent, bottom down to there

Then it becomes easy That is to say, We put the ascent, down to the words of the upper edge of the bottom down to the bottom edge of the text Then with the help of android: includeFontPadding = “false” can remove the words on the bottom margin

Then there are two questions

  1. How do I get the top and bottom edges of text

  2. How to modify ascent and decent

Gets the top and bottom edges of the text

val rect = Rect()
paint.getTextBounds(text.toString(), 0, text.length, rect)
Copy the code

GetTextBounds gets the left top right bottom of the text corresponding to the top, bottom, left, and right edges of the text

12. top and ascent are negative at the same time. 12. bottom and decent are positive at the same time

Modify ascent and decent

Use LineHeightSpan to modify the line height of a Textview

Inherit LineHeightSpan and implement chooseHeight to modify the FontMetricsInt passed by the method

chooseHeight(
    text: CharSequence,
    start: Int,
    end: Int,
    spanstartv: Int,
    lineHeight: Int,
    fm: FontMetricsInt
)
Copy the code
public static class FontMetricsInt {* * * * * * * * * */** * The recommended distance above the baseline for singled spaced text. */
    public int   ascent;
    /** * The recommended distance below the baseline for singled spaced text. */
    public intdescent; * * * * * * * * * *}Copy the code

Modify the variables ascent and decent

So the next problem is to set our LineHeightSpan onto our Textview

There is a class called SpannableStringBuilder in Android.text that is used to modify textView text styles

We just need to call the setSpan method in SpannableStringBuilder and set our LineHeightSpan in there

Then we can put our SpannableStringBuilder in textView.settext

Now we have Rect and we know the top and bottom edges of the text. Use FontMetricsInt to move ascent and decent to the edge of the text and set includeFontPadding to True to remove the upper and lower padding

Do you have any questions

If we simply set ascent to top decent at Bottom, we can remove the padding

So if we have two TextViews, one is “one” and the other is “two”, what we found is that the height of the textView varies with the text

Resolve high inconsistencies

We usually set the height of our TextView to wrap_content and we use textSize as the height of our text.

Methods the following

private fun setHeight(fm: FontMetricsInt, rect: Rect) {
  	// Note: Ascent and top are negative if above the baseline
  
  	// Get the height of our text
    val textHeight = max(textSize.toInt(), rect.bottom - rect.top)
  	Return if the size is no larger than the height of text
    if (fm.descent - fm.ascent <= textHeight) return

    when {
      	// If ascent has been moved to rect.top then textHeight is the height of descent except ascent
        fm.ascent == rect.top -> {
            fm.descent = textHeight + fm.ascent
        }
        // If descent has moved to rect.bottom the height at which Descent is textHeight is the height at which ascent is descent
        fm.descent == rect.bottom -> {
            fm.ascent = fm.descent - textHeight
        }
        else- > {/ / otherwise ascent++ downwards along a pixel descent - to move up one pixel
            fm.ascent++
            fm.descent--
          	/ / recursion
            setHeight(fm, rect)
        }
    }
}
Copy the code

And just to make it easier for you to understand so I’m doing it recursively up here and I’m doing it non-recursively for the same reason

/ / textview height
val viewHeight = fm.descent - fm.ascent
// The actual height of the text
val textHeight = max(textSize.toInt(),  rect.bottom - rect.top)
// The current top margin
val paddingTop = abs(fm.ascent - rect.top)
// The current lower margin
val paddingBottom = fm.descent - rect.bottom
// The minimum distance between the top and bottom
val minPadding = min(paddingTop, paddingBottom)
// The remaining height of the text in the view (textView height - text height) divided by 2 to the average margin height
val avgPadding = (viewHeight - textHeight) / 2

when {
    avgPadding < minPadding -> {
        fm.ascent += avgPadding
        fm.descent -= avgPadding
    }
    paddingTop < paddingBottom -> {
        fm.ascent = rect.top
        fm.descent = textHeight + fm.ascent

    }
    else -> {
        fm.descent = rect.bottom
        fm.ascent = fm.descent - textHeight
    }
}
Copy the code

And then we’ll see what it looks like

Below is the complete code

class ExcludeFontPaddingTextView : AppCompatTextView {

    init {
        includeFontPadding = false
    }

    constructor(context: Context) : super(context)

    constructor(context: Context, attrs: AttributeSet?) : super(context, attrs)

    constructor(context: Context, attrs: AttributeSet? , defStyleAttr:Int) : super(
        context,
        attrs,
        defStyleAttr
    )

    override fun setText(text: CharSequence? , type:BufferType?). {
        super.setText(getCustomText(text), type)
    }
    

    private fun getCustomText(text: CharSequence?).: SpannableStringBuilder? {

        returntext? .let {val rect = Rect()
            paint.getTextBounds(text.toString(), 0, text.length, rect)

            val ssb = SpannableStringBuilder(text)
            / / set LineHeightSpan
            ssb.setSpan(
                object : LineHeightSpan {
                    override fun chooseHeight(
                        text: CharSequence,
                        start: Int,
                        end: Int,
                        spanstartv: Int,
                        lineHeight: Int,
                        fm: FontMetricsInt
                    ) {

                        val viewHeight = fm.descent - fm.ascent
                        val textHeight = max(textSize.toInt(), rect.bottom - rect.top)

                        val paddingTop = abs(fm.ascent - rect.top)
                        val paddingBottom = fm.descent - rect.bottom

                        val minPadding = min(paddingTop, paddingBottom)
                        val avgPadding = (viewHeight - textHeight) / 2

                        when {
                            avgPadding < minPadding -> {
                                fm.ascent += avgPadding
                                fm.descent -= avgPadding
                            }
                            paddingTop < paddingBottom -> {
                                fm.ascent = rect.top
                                fm.descent = textHeight + fm.ascent

                            }
                            else -> {
                                fm.descent = rect.bottom
                                fm.ascent = fm.descent - textHeight
                            }
                        }


                    }

                },
                0,
                text.length,
                Spanned.SPAN_EXCLUSIVE_EXCLUSIVE
            )
            ssb
        }


    }


}
Copy the code

You can use it just like a TextView

<com.example.myapplication.ExcludeFontPaddingTextView
    android:id="@+id/textview1"
    android:layout_width="wrap_content"
    android:layout_height="wrap_content"
    android:background="#2196F3"
    android:text="Ten"
    android:textSize="20dp"
    app:layout_constraintLeft_toLeftOf="parent"
    app:layout_constraintTop_toTopOf="parent" />
Copy the code