Paint中FontMetrics的一些变量解析

本文详细解析了在自定义View中如何正确地使Text居中显示,包括理解FontMetrics中的关键概念如baseline、ascent、descent等,并提供了一个具体的实现案例。

我们很多时候需要在自定义View中绘制一些Text,这时候需要用到Paint中的drawText

public void drawText(@NonNull String text, float x, float y, @NonNull Paint paint)

这里第三个参数y是指的是baseline位置(很多人都使用getMeasureHeight()/2尴尬

到底baseline是什么呢?我们drawText的时候怎么计算y值?


首先我们先理解FontMetrics中的baseline、ascent、descent、top、bottom的具体含义,先看下面图


baseLine: 灰色的线

ascent: 上面蓝色线距离baseline的距离, 这个是负值

top: 是所有ascent最小数值(因为是负值,就是最高的),图中是上面绿色的线,这个也是负值

descent: 是下面红色的线到baseline的距离,这个是正值

bottom: 是所有字符descent最大值


在自定义View中drawtext的时候,居中显示,需要根据下面公式计算


 	Rect bounds = new Rect();
        Paint.FontMetrics fm = textPaint.getFontMetrics();
	//text高度
        int textH = (int)(fm.descent - fm.ascent);
        textPaint.getTextBounds(data, 0, data.length(), bounds);
        startX = (getMeasuredWidth() - bounds.width())/2;
	//ascent是负值
        startY = (int)((getMeasuredHeight() - textH)/2 - fm.ascent);
        canvas.drawText(data, startX, startY, textPaint);

代码比较简单,就不上传源码了。


下面的代码能不能加一个显示的是剩余时间的功能,给我修改后的完整代码 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 // 计算单行高度(包括ascent和descent) 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、付费专栏及课程。

余额充值