有时候我们会接到一个这样的UI图,文字距离父布局上方是24px
如果我们在布局上这样写会发现一个问题
<TextView
android:id="@+id/textview"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:background="#2196F3"
android:text="数码3C"
android:textSize="20dp"
android:layout_marginTop="24px"
app:layout_constraintLeft_toLeftOf="parent"
app:layout_constraintTop_toTopOf="parent" />
我们设置的上边距是24。但是发现实际跟文字之间的距离是大于24的
那么要怎么办呢
大部分人会说加上这个属性android:includeFontPadding="false"
看起来是好那么一点点,但是还有一点偏差
我的解决办法
我们先来看看Textview绘制的几条线
top:能绘制的最高点ascent:推荐的上边缘线base:基准线decent:推荐的下边缘线bottom:能绘制的最低点
先说说android:includeFontPadding="false" ,
这个属性为true时,TextView的绘制区域为top至buttom。
为false时,TextView的绘制区域为ascent至decent。
那么就好办了 也就是说 我们把ascent下移到文字的上边缘 把decent上移到文字的下边缘 然后再借助android:includeFontPadding="false"就可以把文字的上下边距去掉了
接下来就有两个问题
-
怎么获取文字的上边缘和下边缘
-
怎么修改
ascent和decent
获取文字的上边缘和下边缘
val rect = Rect()
paint.getTextBounds(text.toString(), 0, text.length, rect)
paint.getTextBounds可以获取文字的left top right bottom 对应着文字的上下左右边缘
注意:top和ascent一样 一般为负值 baseline往上的为负 往下的为正 bottom和decent一般为正
修改ascent和decent
使用LineHeightSpan 可以修改Textview的行高
继承LineHeightSpan后 实现chooseHeight方法就可以修改方法传来的FontMetricsInt
chooseHeight(
text: CharSequence,
start: Int,
end: Int,
spanstartv: Int,
lineHeight: Int,
fm: FontMetricsInt
)
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 int descent;
**********
}
修改变量ascent和decent
接下来的问题就是 这么把我们的LineHeightSpan设置到Textview上呢
android.text 里面有个叫SpannableStringBuilder的类 主要就是给我们用来修改textview文字样式的
我们只需要SpannableStringBuilder 调用里面的setSpan方法 把我们的LineHeightSpan设置进去
然后在Textview.setText的时候把我们的SpannableStringBuilder放进去就可以了
现在我们有
Rect知道了文字的上边缘和下边缘。用FontMetricsInt把ascent和decent移动到文字的边缘 然后把includeFontPadding设置成true就可以让文字的上下padding去掉了
还有问题
如果我们简单粗暴的把ascent设置成top decent设置成bottom 的确能达到去掉上下padding的效果
比如 这两个Textview 一个是 “一” 另一个是 “二” 我们发现 不同文本会导致Textview的高度也一同变化
解决高度不一致问题
一般Textview的高度我们都是设置成wrap_content的 我们使用textsize作为文字的高度(大部分中文的高度都是一这个大小)
方法如下
private fun setHeight(fm: FontMetricsInt, rect: Rect) {
//注意:如果ascent和top在baseline以上的话 会为负值
//拿到我们text的高度
val textHeight = max(textSize.toInt(), rect.bottom - rect.top)
//如果修改后的尺寸不大于text的高度了 就返回
if (fm.descent - fm.ascent <= textHeight) return
when {
//如果ascent已经移动到了rect.top(文字上边缘)了 那么textHeight除去ascent的高度 就是descent的高度
fm.ascent == rect.top -> {
fm.descent = textHeight + fm.ascent
}
//如果descent已经移动到了rect.bottom(文字下边缘)了 那么textHeight除去descent的高度 就是ascent的高度
fm.descent == rect.bottom -> {
fm.ascent = fm.descent - textHeight
}
else -> {
//其他情况 ascent++往下移移一像素descent--往上移一像素
fm.ascent++
fm.descent--
//递归
setHeight(fm, rect)
}
}
}
为了方便大家理解 所以上面是用递归的方法 下面是的不递归的方法 目的是一样的
//textview的高度
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)
//文本在view中剩余的高度(textview的高度-文字的高度)除2 的到平均的边距高度
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
}
}
修改过后我们看看最终效果
完整的代码
class ExcludeFontPaddingTextView @JvmOverloads constructor(context: Context, attrs: AttributeSet? = null, defStyleAttr: Int = 0) : AppCompatTextView(context, attrs, defStyleAttr) {
init {
includeFontPadding = false
}
override fun setText(text: CharSequence?, type: BufferType?) {
super.setText(getCustomText(text), type)
}
private fun getCustomText(text: CharSequence?): SpannableStringBuilder? {
if (text == null) {
return null
}
return SpannableStringBuilder(text).apply {
setSpan(
object : LineHeightSpan {
override fun chooseHeight(
text: CharSequence,
start: Int,
end: Int,
spanstartv: Int,
lineHeight: Int,
fm: FontMetricsInt
) {
val rect = Rect()
paint.getTextBounds(text.toString(), 0, text.length, rect)
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
)
}
}
}
使用方法跟TextView一样
<com.example.myapplication.ExcludeFontPaddingTextView
android:id="@+id/textview1"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:background="#2196F3"
android:text="十"
android:textSize="20dp"
app:layout_constraintLeft_toLeftOf="parent"
app:layout_constraintTop_toTopOf="parent" />