android textView显示时间的时候,经常遇到由于每个字符不等距导致宽度跳来跳去。原因是,大部分字体0~9的数字的宽度是不等距的,因此当显示时间数字的时候,由于组合不同,就会导致宽度不同,就会跳来跳去。
因此解决办法有2个:
第一,使用monospace的字体库。系统有自带的,或者让你们公司给一套monospace的字体库集成使用。
第二,就是今天我要说的自定义view来解决了。
解决的思路如下:
- 统计0~9 10个数字的最大宽度,所有的数字字符就这10个,以最大宽度为准;
- 其他字符比如
/
,:
,-
以它原本宽度为准就好;
最后效果,能够支持wrap_content,效果还是不错的。
当然,这里是抛砖引玉,功能是比较有限的,主要是思路和基本功能已经实现。
比如这里我的实现,
是没有写atrrs来支持自定义颜色,字体大小等;可以自行实现;
是不建议修改出现文字大小不一样的情况的,可以简单的继承来实现多段颜色。
open class MonospaceTimeView @JvmOverloads constructor(
context: Context, attrs: AttributeSet? = null, defStyleAttr: Int = 0
) : View(context, attrs, defStyleAttr) {
fun convertDpToPixel(dp: Float, context: Context): Float {
val metrics = context.resources.displayMetrics
val px = dp * (metrics.densityDpi / 160f)
return px
}
open val mColor = Color.BLACK
//字体大小,其实就是整个View的高度;这是一个随意的值,必须通过initParamOnCreate设置。
open val mTextSize = convertDpToPixel(16f, context)
protected lateinit var mPaint:Paint
var mTextChars:CharArray? = null
private set
private var spaceWidth = Int.MAX_VALUE
private var eachNumWidth = 0
private val normalCharMap = mutableMapOf<Char, Int>()
override fun onAttachedToWindow() {
super.onAttachedToWindow()
initPaint()
eachWidthAndHeight()
}
/** 可以继承它;然后给super().initPaint()继续追加自己想要的属性,比如setTypeface
*/
protected open fun initPaint() {
mPaint = Paint().apply {
setColor(mColor) // 设置画笔颜色
textSize = mTextSize
isAntiAlias = true // 抗锯齿
textAlign = Paint.Align.CENTER
}
}
open fun setTimeString(str:String) {
mTextChars = str.toCharArray()
invalidate()
}
/**
* 显示之前,一定要优先调用一下,确定每个字符的宽度。
*/
private fun eachWidthAndHeight() {
val rect = Rect()
var maxWidth = -1
val chars = listOf('0', '1', '2', '3', '4', '5', '6', '7', '8', '9')
var minWidth = Int.MAX_VALUE
for (ch in chars) {
mPaint.getTextBounds("" + ch, 0, 1, rect)
val width = rect.left + rect.right
maxWidth = max(width, maxWidth)
minWidth = min(minWidth, width)
}
val otherChars = listOf(':', '/', '-')
for (ch in otherChars) {
mPaint.getTextBounds("" + ch, 0, 1, rect)
val width = rect.left + rect.right
minWidth = min(minWidth, width)
normalCharMap[ch] = width
}
//由于空格不能单独统计,所以以最小的一半为准
eachNumWidth = maxWidth
this.spaceWidth = minWidth / 2
}
private val canvasRect = Rect()
/**
可以继承给特定的字段特定的paint,这样就能改变颜色。
*/
open fun getOnDrawPaintByTextIndex(index:Int, total:Int) : Paint{
return mPaint
}
private fun charToWidth(ch:Char) : Int{
if (ch in '0'..'9') {
return eachNumWidth
} else if (ch == ' ') {
return spaceWidth
} else if (normalCharMap.containsKey(ch)) {
return normalCharMap[ch]!!
} else {
return eachNumWidth
}
}
override fun onDraw(canvas: Canvas) {
super.onDraw(canvas)
if (mTextChars == null) {
return
}
canvas.getClipBounds(canvasRect)
val cHeight: Int = canvasRect.height()
mPaint.getTextBounds("0", 0, 1, canvasRect) //取任意字符的高度即可
val baseLine: Float = cHeight / 2f + canvasRect.height() / 2f - canvasRect.bottom
var startX = 0f
val size = mTextChars?.size ?: 0
mTextChars?.forEachIndexed { index, ch ->
val w = charToWidth(ch)
canvas.drawText("" + ch, startX + (w.toFloat() / 2), baseLine, getOnDrawPaintByTextIndex(index, size))
startX += w
}
}
override fun onMeasure(widthMeasureSpec: Int, heightMeasureSpec: Int) {
super.onMeasure(widthMeasureSpec, heightMeasureSpec)
val widthSpecMode = MeasureSpec.getMode(widthMeasureSpec)
val widthSpecSize = MeasureSpec.getSize(widthMeasureSpec)
val heightSpecMode = MeasureSpec.getMode(heightMeasureSpec)
val heightSpecSize = MeasureSpec.getSize(heightMeasureSpec)
if (isInEditMode) {
if(widthSpecMode == MeasureSpec.AT_MOST && heightSpecMode == MeasureSpec.AT_MOST) {
setMeasuredDimension(100, (mTextSize + 0.5).toInt())
} else if(widthSpecMode == MeasureSpec.AT_MOST) {
setMeasuredDimension(100, heightSpecSize)
} else if (heightSpecMode == MeasureSpec.AT_MOST) {
setMeasuredDimension(widthSpecSize, (mTextSize + 0.5).toInt())
}
return
}
var wid = 0f
mTextChars?.forEach { ch->
wid += charToWidth(ch)
}
if(widthSpecMode == MeasureSpec.AT_MOST && heightSpecMode == MeasureSpec.AT_MOST) {
setMeasuredDimension((wid + 0.5).toInt(), (mTextSize + 0.5).toInt())
} else if(widthSpecMode == MeasureSpec.AT_MOST) {
setMeasuredDimension((wid + 0.5).toInt(), heightSpecSize)
} else if (heightSpecMode == MeasureSpec.AT_MOST) {
setMeasuredDimension(widthSpecSize, (mTextSize + 0.5).toInt())
}
}
}