重新认识FontMetrics、Canvas.drawText

本文探讨了在自定义View中使用FontMetrics类精确控制文本的位置,特别是如何找到文本的中间基准点进行居中显示。通过实际案例,展示了不同字符类型(如阿拉伯数字、汉字及表情符号)在FontMetrics中的表现差异。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

常常需要在自定义view中绘制文字时遇到在视图中间位置绘制一行文本,但是每次苦于找不到恰到好处的基准点,每次都要百度上搜来搜去,结果也是有很多种不统一的方法,今天决定好好研究一下FontMetrics这个类,以下一些心得记录:

1,阿拉伯数字、汉字、英文字母大小写、表情符号的FontMetrics基准线是不同的,如下图可以看到数字和大写字母还是比较整齐的。(ascent等是打印出的Paint.FontMetrics参数值,从最上的白线开始,依次为白线(top)、红线(ascent)、灰线(计算出的中间线,下文有解释)、绿线(leading)、黄线(descent),FontMetrics.bottom(蓝线)由于和descent几乎重合,并没有显示出来)



2,代码:

        Paint.FontMetrics metrics = paint.getFontMetrics();
        float basetop = 60;
        canvas.drawText(sample, 0, basetop, paint);

        paint.setColor(Color.RED);//
        canvas.drawLine(0, basetop + metrics.ascent, getWidth(), basetop + metrics.ascent, paint);

        paint.setColor(Color.BLUE);//
        canvas.drawLine(0, basetop + metrics.bottom, getWidth(), basetop + metrics.bottom, paint);

        paint.setColor(Color.YELLOW);//
        canvas.drawLine(0, basetop + metrics.descent, getWidth(), basetop + metrics.descent, paint);

        paint.setColor(Color.GREEN);//基准点
        canvas.drawLine(0, basetop + metrics.leading, getWidth(), basetop + metrics.leading, paint);

        paint.setColor(Color.WHITE);
        canvas.drawLine(0, basetop + metrics.top, getWidth(), basetop + metrics.top, paint);

        paint.setColor(Color.WHITE);//中间线
        canvas.drawLine(0, basetop + (metrics.top + metrics.bottom) / 2, getWidth(), basetop + (metrics.top + metrics.bottom) / 2, paint);

3,如果想把字符串"1234"绘制到view正中间,首先需要计算出drawText的floatY参数,也就是FontMetrics.leading在View视图中的Y坐标值。

我的计算方法是:Y= View.height * 1.0 / 2 - (metrics.bottom + metrics.top) / 2

(metrics.bottom + metrics.top) / 2的正整数刚好是leading到文字中间的长度



下面的代码能能加一个显示的是剩余时间的功能,给我修改后的完整代码 package com.app.decodemo.view import android.content.Context import android.graphics.* import android.text.TextPaint import android.util.AttributeSet import android.view.View import androidx.core.content.res.getBooleanOrThrow import androidx.core.content.res.getColorOrThrow import androidx.core.content.res.getDimensionOrThrow //import androidx.core.content.res.getStringOrNull import com.app.deco.R class CircleIndicator @JvmOverloads constructor( context: Context, attrs: AttributeSet? = null, defStyleAttr: Int = 0 ) : View(context, attrs, defStyleAttr) { // 原有属性 private var process: Int = 0 private var circleColor: Int = Color.LTGRAY private var progressColor: Int = Color.BLUE private var strokeWidth: Float = 20f // 新增文本属性 private var text: String? = null private var textColor: Int = Color.BLACK private var textSize: Float = 40f private var showPercentage: Boolean = true // 绘制工具 private val circlePaint = Paint(Paint.ANTI_ALIAS_FLAG).apply { style = Paint.Style.STROKE } private val progressPaint = Paint(Paint.ANTI_ALIAS_FLAG).apply { style = Paint.Style.STROKE strokeCap = Paint.Cap.ROUND } private val textPaint = TextPaint(Paint.ANTI_ALIAS_FLAG).apply { textAlign = Paint.Align.CENTER } private val rectF = RectF() init { attrs?.let { initAttributes(it) } } private fun initAttributes(attrs: AttributeSet) { val typedArray = context.obtainStyledAttributes( attrs, R.styleable.CircleIndicator, 0, 0 ) try { // 原有属性解析 process = typedArray.getInt(R.styleable.CircleIndicator_process, 0) circleColor = typedArray.getColor(R.styleable.CircleIndicator_circleColor, Color.LTGRAY) progressColor = typedArray.getColor(R.styleable.CircleIndicator_progressColor, Color.BLUE) strokeWidth = typedArray.getDimension(R.styleable.CircleIndicator_strokeWidth, 20f) // 新增文本属性解析 text = typedArray.getString(R.styleable.CircleIndicator_text) textColor = typedArray.getColor(R.styleable.CircleIndicator_textColor, Color.BLACK) textSize = typedArray.getDimension(R.styleable.CircleIndicator_textSize, 40f) showPercentage = typedArray.getBoolean(R.styleable.CircleIndicator_showPercentage, true) // 初始化文本画笔 textPaint.color = textColor textPaint.textSize = textSize } finally { typedArray.recycle() } } override fun onSizeChanged(w: Int, h: Int, oldw: Int, oldh: Int) { super.onSizeChanged(w, h, oldw, oldh) val halfStroke = strokeWidth / 2 rectF.set( paddingLeft + halfStroke, paddingTop + halfStroke, w - paddingRight - halfStroke, h - paddingBottom - halfStroke ) } override fun onDraw(canvas: Canvas) { super.onDraw(canvas) // 绘制背景圆环 circlePaint.color = circleColor circlePaint.strokeWidth = strokeWidth canvas.drawOval(rectF, circlePaint) // 绘制进度弧 if (process > 0) { progressPaint.color = progressColor progressPaint.strokeWidth = strokeWidth val sweepAngle = 360 * process / 100f canvas.drawArc(rectF, -90f, sweepAngle, false, progressPaint) } // 绘制居中文本 drawCenterText(canvas) } private fun drawCenterText(canvas: Canvas) { val displayText = when { !text.isNullOrEmpty() -> text!! showPercentage -> "$process%" else -> process.toString() } // 计算垂直居中位置 val textBounds = Rect() textPaint.getTextBounds(displayText, 0, displayText.length, textBounds) val y = rectF.centerY() - textBounds.exactCenterY() canvas.drawText(displayText, rectF.centerX(), y, textPaint) } private fun drawCenterTextTwoLine(canvas: Canvas) { if (!text.isNullOrEmpty()) { // 计算两行文本 val line1 = text val line2 = "$process%" // 获取字体测量信息 val fontMetrics = textPaint.fontMetrics // 计算单行高度(包括ascentdescent) val lineHeight = fontMetrics.descent - fontMetrics.ascent val lineSpacing = 5f // 行间距 // 计算文本块总高度 val totalTextHeight = 2 * lineHeight + lineSpacing // 计算文本块起始Y坐标(使整个文本块垂直居中) val startY = rectF.centerY() - totalTextHeight / 2 // 绘制第一行文本(水平居中+垂直定位) val line1Width = textPaint.measureText(line1) // 第一行基线位置:起始Y + 行高 - 字体下降部分 val baseline1 = startY + lineHeight - fontMetrics.descent canvas.drawText( line1!!, rectF.centerX() - line1Width / 2, // 水平居中 baseline1, textPaint ) // 绘制第二行文本(水平居中+垂直定位) val line2Width = textPaint.measureText(line2) // 第二行基线位置:起始Y + 行高 + 行间距 + 行高 - 字体下降部分 val baseline2 = startY + lineHeight + lineSpacing + lineHeight - fontMetrics.descent canvas.drawText( line2, rectF.centerX() - line2Width / 2, // 水平居中 baseline2, textPaint ) } else if (showPercentage) { // 原有逻辑:只显示进度百分比 val text = "$process%" val textWidth = textPaint.measureText(text) val textHeight = textPaint.descent() - textPaint.ascent() // 垂直居中计算 val baseline = rectF.centerY() + textHeight / 2 - textPaint.descent() canvas.drawText(text, rectF.centerX() - textWidth / 2, baseline, textPaint) } } // 设置进度并刷新视图 fun setProcess(value: Int) { process = value.coerceIn(0, 100) invalidate() } // 设置自定义文本 fun setText(customText: String?) { text = customText invalidate() } // 设置是否显示百分比 fun setShowPercentage(show: Boolean) { showPercentage = show invalidate() } }
最新发布
08-23
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值