Android字体属性Paint.FontMetrics

本文详细解析了Android字体的几个关键参数,包括baseline、ascent、descent等,并展示了如何通过代码调整文本布局,确保文字在指定矩形区域内居中显示。同时,文章还探讨了不同条件下字体大小对属性的影响,以及如何利用这些知识优化应用中的文本显示效果。

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

(一) 字体的几个参数 ,以Android API文档定义为准,见下图

要点如下:

1. 基准点是baseline

2. Ascent是baseline之上至字符最高处的距离

3. Descent是baseline之下至字符最低处的距离

4. Leading文档说的很含糊,这个我还不清楚,但有人说是上一行字符的descent到下一行的ascent之间的距离,也有人说不是

5. Top指的是指的是最高字符到baseline的值,即ascent的最大值

6. 同上,bottom指的是最下字符到baseline的值,即descent的最大值


(二)贴几张图看看:

pic_1


pic_2

pic_3

字体为斜体状态下(你没有看错,中文只设置mPaint.setTypeface(Typeface.create("", Typeface.ITALIC))是斜体不了的):



pic-4


上验证代码:

public class MyView extends View {


	private Paint mPaint = new Paint();

	public MyView(Context context) {
		super(context);
		// TODO Auto-generated constructor stub
	}
	
	public MyView(Context context, AttributeSet attrs) {
		super(context, attrs);
		// TODO Auto-generated constructor stub
	}

	public MyView(Context context, AttributeSet attrs, int defStyle) {
		super(context, attrs, defStyle);
		// TODO Auto-generated constructor stub
	}
	
	@Override
	protected void onDraw(Canvas canvas) {
		// TODO Auto-generated method stub
		mPaint.reset();
		mPaint.setColor(Color.WHITE);
		mPaint.setTextSize(80);
		//设置字体为斜体
		mPaint.setTypeface(Typeface.create("", Typeface.ITALIC));
		// FontMetrics对象
		FontMetrics fontMetrics = mPaint.getFontMetrics();
		String text = "中国话fgiqÃÇŸŒú";
		// 计算每一个坐标
		float textWidth = mPaint.measureText(text);
		float baseX = 30;
		float baseY = 700;
		float topY = baseY + fontMetrics.top;
		float ascentY = baseY + fontMetrics.ascent;
		float descentY = baseY + fontMetrics.descent;
		float bottomY = baseY + fontMetrics.bottom;
		// 绘制文本
		canvas.drawText(text, baseX, baseY, mPaint);
		// BaseLine描画
		mPaint.setColor(Color.RED);
		canvas.drawLine(baseX, baseY, baseX + textWidth, baseY, mPaint);
		mPaint.setTextSize(20);
		canvas.drawText("base", baseX + textWidth, baseY, mPaint);
		// Base描画
		canvas.drawCircle(baseX, baseY, 5, mPaint);
		// TopLine描画
		mPaint.setColor(Color.LTGRAY);
		canvas.drawLine(baseX, topY, baseX + textWidth, topY, mPaint);
		canvas.drawText("top", baseX + textWidth, topY, mPaint);
		// AscentLine描画
		mPaint.setColor(Color.GREEN);
		canvas.drawLine(baseX, ascentY, baseX + textWidth, ascentY, mPaint);
		canvas.drawText("ascent", baseX + textWidth, ascentY + 10, mPaint);
		// DescentLine描画
		mPaint.setColor(Color.YELLOW);
		canvas.drawLine(baseX, descentY, baseX + textWidth, descentY, mPaint);
		canvas.drawText("descent", baseX + textWidth, descentY, mPaint);
		// ButtomLine描画
		mPaint.setColor(Color.MAGENTA);
		canvas.drawLine(baseX, bottomY, baseX + textWidth, bottomY, mPaint);
		canvas.drawText("buttom", baseX + textWidth, bottomY + 10, mPaint);
		super.onDraw(canvas);
	}
}

(三)将字画在矩形背景的正中位置

FontMetrics.top的数值是个负数,其绝对值就是字体绘制边界到baseline的距离。
所以如果是把文字画在 FontMetrics高度的矩形中, drawText就应该传入 -FontMetrics.top。
要画在targetRect的居中位置,baseline的计算公式就是(本人抄的原博主的,也不知道怎么计算的):

targetRect.top + (targetRect.bottom - targetRect.top) / 2 - (FontMetrics.bottom - FontMetrics.top) / 2 - FontMetrics.top

代码应该改成(顺便加入水平居中):

[java]  view plain copy 在CODE上查看代码片 派生到我的代码片
  1. @Override  
  2. public void onDraw (Canvas canvas) {  
  3.     Rect targetRect = new Rect(50501000200);  
  4.     Paint paint = new Paint(Paint.ANTI_ALIAS_FLAG);  
  5.     paint.setStrokeWidth(3);  
  6.     paint.setTextSize(80);  
  7.     String testString = "测试:ijkJQKA:1234";  
  8.     paint.setColor(Color.CYAN);  
  9.     canvas.drawRect(targetRect, paint);  
  10.     paint.setColor(Color.RED);  
  11.     FontMetricsInt fontMetrics = paint.getFontMetricsInt();  
  12.         // 转载请注明出处:http://blog.youkuaiyun.com/hursing  
  13.     int baseline = targetRect.top + (targetRect.bottom - targetRect.top - fontMetrics.bottom + fontMetrics.top) / 2 - fontMetrics.top;  
  14.     // 下面这行是实现水平居中,drawText对应改为传入targetRect.centerX()  
  15.     paint.setTextAlign(Paint.Align.CENTER);  
  16.     canvas.drawText(testString, targetRect.centerX(), baseline, paint);  
  17. }  
效果:



(四)注意:

A:一个字符f得到的测试结果与上边的代码得到的fontmetrics值相同。

B:如果pain设置的textSize均为55,或者textView设置字体大小为55,且为相同的字符串,但是两个获得的FontMetrics属性值并不相同。但是,我们发现,做除法之后,均为1.5倍关系。做出猜测,即Paint下,为mdpi对应的size,而TextView的size已经关联到了显示屏幕本身的320dip。所以获得属性值均为整1.5倍数

C:各种情况下,均未获得leading值。

D:好似还可以直接让CPU来渲染字体,出门右转http://blog.jobbole.com/70468/点击打开链接 


参考:http://blog.youkuaiyun.com/hursing/article/details/18703599

参考:http://blog.jobbole.com/70468/

package com.example.demo; import android.content.Context; import android.graphics.Canvas; import android.graphics.Color; import android.graphics.Paint; import android.util.AttributeSet; import android.util.Log; import android.view.Gravity; import android.view.View; import android.view.ViewGroup; import android.widget.Button; import android.widget.TextView; public class CustomView extends ViewGroup { public CustomView(Context context) { super(context); init(context); } public CustomView(Context context, AttributeSet attrs) { super(context, attrs); init(context); } public CustomView(Context context, AttributeSet attrs, int defStyleAttr) { super(context, attrs, defStyleAttr); init(context); } private void init(Context context) { // 动态添加第一个子View:TextView TextView textView = new TextView(context); textView.setText("Hello"); addView(textView); // 动态添加第二个子View:Button Button button = new Button(context); button.setText("Click me"); LayoutParams params = new LayoutParams(LayoutParams.WRAP_CONTENT, LayoutParams.WRAP_CONTENT); params.width = Gravity.BOTTOM | Gravity.END; addView(button, params); } @Override protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { // 自定义测量逻辑 int desiredWidth = 200; int desiredHeight = 100; int widthMode = MeasureSpec.getMode(widthMeasureSpec); int widthSize = MeasureSpec.getSize(widthMeasureSpec); int heightMode = MeasureSpec.getMode(heightMeasureSpec); int heightSize = MeasureSpec.getSize(heightMeasureSpec); int width; int height; if (widthMode == MeasureSpec.EXACTLY) { width = widthSize; } else if (widthMode == MeasureSpec.AT_MOST) { width = Math.min(desiredWidth, widthSize); } else { width = desiredWidth; } if (heightMode == MeasureSpec.EXACTLY) { height = heightSize; } else if (heightMode == MeasureSpec.AT_MOST) { height = Math.min(desiredHeight, heightSize); } else { height = desiredHeight; } setMeasuredDimension(width, height); } @Override protected void onLayout(boolean changed, int left, int top, int right, int bottom) { // 遍历子View并设置其位置 for (int i = 0; i < getChildCount(); i++) { View child = getChildAt(i); // 简单示例:将子View放置在父View中心 int childWidth = child.getMeasuredWidth(); int childHeight = child.getMeasuredHeight(); int childLeft = (getWidth() - childWidth) / 2; int childTop = (getHeight() - childHeight) / 2; child.layout(childLeft, childTop, childLeft + childWidth, childTop + childHeight); } } @Override protected void onDraw(Canvas canvas) { super.onDraw(canvas); // 在此绘制内容,例如一个红色矩形 // Paint paint = new Paint(); // paint.setColor(Color.BLUE); // // canvas.drawRect(0, 0, getWidth(), getHeight(), paint); // 使用测量后的尺寸 Log.d("CustomView", "onDraw called"); // String text = "CustomView"; // paint.setTextSize(25); // paint.setColor(Color.YELLOW); // float textWidth = paint.measureText(text); // float centerX = getWidth() / 2f; // float centerY = getHeight() / 2f; // // // 获取字体度量 // Paint.FontMetrics fontMetrics = paint.getFontMetrics(); // // 计算基线位置:使文本垂直居中 // float baseline = centerY - (fontMetrics.top + fontMetrics.bottom) / 2f; // // // 绘制文本 // canvas.drawText(text, centerX - textWidth/2, baseline, paint); } } 我这对吗
最新发布
07-22
<think>我们正在讨论的是在自定义ViewGroup中动态添加子View(TextView和Button),并验证onMeasure和onLayout的实现是否正确。 根据引用[2]可知,onMeasure中的widthMeasureSpec和heightMeasureSpec是由父容器传递过来的测量要求。 引用[4]中提到了自定义视图组件,并展示了在onLayout方法中处理布局变化。 因此,我们需要实现一个自定义的ViewGroup,在其中动态添加TextView和Button,并正确实现onMeasure和onLayout方法。 步骤: 1. 自定义ViewGroup(例如继承自ViewGroup) 2. 在构造方法中动态添加子View(TextView和Button) 3. 重写onMeasure方法:测量所有子View,并根据子View的测量结果和自身的布局特性来确定自己的尺寸。 4. 重写onLayout方法:布局所有子View,即确定每个子View的位置。 注意:在自定义ViewGroup中,我们需要负责子View的测量和布局。 下面是一个示例代码,展示如何实现: 自定义ViewGroup类(例如叫CustomLayout):</think>### 在自定义 ViewGroup 中动态添加子视图的正确实现 以下是验证自定义 ViewGroup 中动态添加 TextView/Button 并实现 `onMeasure()` 和 `onLayout()` 的标准方法,结合引用内容分析关键点: --- ### 1. 动态添加子视图(构造函数中) ```java public class CustomLayout extends ViewGroup { private TextView mTextView; private Button mButton; public CustomLayout(Context context, AttributeSet attrs) { super(context, attrs); // 动态创建并添加子视图 mTextView = new TextView(context); mTextView.setText("动态文本"); addView(mTextView); // 引用[1]的动态加载思想 mButton = new Button(context); mButton.setText("点击按钮"); addView(mButton); } } ``` --- ### 2. 测量阶段 (`onMeasure`) ```java @Override protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { // 1. 测量所有子视图 measureChildren(widthMeasureSpec, heightMeasureSpec); // 关键:遍历测量子View // 2. 计算父容器尺寸(基于子视图需求) int maxChildWidth = 0; int totalHeight = 0; for (int i = 0; i < getChildCount(); i++) { View child = getChildAt(i); maxChildWidth = Math.max(maxChildWidth, child.getMeasuredWidth()); totalHeight += child.getMeasuredHeight(); } // 3. 处理父容器约束(考虑padding) int widthMode = MeasureSpec.getMode(widthMeasureSpec); int widthSize = MeasureSpec.getSize(widthMeasureSpec); int heightMode = MeasureSpec.getMode(heightMeasureSpec); int heightSize = MeasureSpec.getSize(heightMeasureSpec); // 4. 根据测量模式确定最终尺寸 int finalWidth = (widthMode == MeasureSpec.EXACTLY) ? widthSize : maxChildWidth + getPaddingLeft() + getPaddingRight(); int finalHeight = (heightMode == MeasureSpec.EXACTLY) ? heightSize : totalHeight + getPaddingTop() + getPaddingBottom(); // 5. 保存测量结果(必须调用) setMeasuredDimension(finalWidth, finalHeight); // 引用[2]的测量原理 } ``` --- ### 3. 布局阶段 (`onLayout`) ```java @Override protected void onLayout(boolean changed, int l, int t, int r, int b) { // 1. 获取可用区域(考虑padding) int currY = getPaddingTop(); int contentWidth = r - l - getPaddingLeft() - getPaddingRight(); // 2. 遍历布局所有子视图 for (int i = 0; i < getChildCount(); i++) { View child = getChildAt(i); // 3. 计算子视图位置(垂直排列示例) int childWidth = child.getMeasuredWidth(); int childHeight = child.getMeasuredHeight(); // 水平居中 int childLeft = getPaddingLeft() + (contentWidth - childWidth) / 2; int childTop = currY; int childRight = childLeft + childWidth; int childBottom = childTop + childHeight; // 4. 应用布局 child.layout(childLeft, childTop, childRight, childBottom); // 引用[4]的布局逻辑 // 5. 更新垂直位置 currY = childBottom + 10; // 添加间距 } } ``` --- ### ✅ 正确性验证要点 | 阶段 | 验证要点 | 错误示例 | |-------------|--------------------------------------------------------------------------|---------------------------| | **测量阶段** | 是否调用 `measureChildren()` 或手动测量每个子视图 | 未测量子视图直接布局 | | | 是否正确处理 `MeasureSpec.EXACTLY/AT_MOST/UNSPECIFIED` 模式 | 忽略父容器约束 | | | 是否包含 `setMeasuredDimension()` 调用 | 遗漏导致测量结果无效 | | **布局阶段** | 是否根据测量结果计算位置(使用 `getMeasuredWidth/Height` 而非 `getWidth`) | 使用未测量的尺寸 | | | 是否考虑 padding 和子视图间距 | 子视图重叠或超出边界 | | | 每个子视图必须调用 `layout(left, top, right, bottom)` | 遗漏部分子视图布局 | | **动态添加** | 在构造函数或 `onAttachedToWindow()` 中添加视图 | 在 `onDraw()` 中添加 | --- ### 滚动处理的额外说明(引用[3]) 若需支持滚动效果(如引用[3]的 `scrollTo`): ```java // 在CustomLayout中重写 @Override public void scrollTo(int x, int y) { // 约束滚动范围 int maxScrollY = Math.max(0, getTotalHeight() - getHeight()); y = Math.min(Math.max(y, 0), maxScrollY); super.scrollTo(x, y); } // 计算内容总高度 private int getTotalHeight() { int height = 0; for (int i = 0; i < getChildCount(); i++) { height += getChildAt(i).getHeight(); } return height; } ``` --- ### 性能优化建议 1. **避免过度测量**:在 `onMeasure()` 中缓存测量结果,避免重复计算 2. **增量更新**:使用 `requestLayout()` 而非 `invalidate()` 触发重布局 3. **异步加载**:大量子视图使用 `View.post()` 分批添加 4. **复用视图**:动态移除不可见视图(如 `removeViewAt()`) > 正确实现的核心在于:**测量阶段确定尺寸约束** → **布局阶段应用位置计算** → **滚动/绘制阶段处理视觉效果**。动态添加视图需遵循 Android 视图树的生命周期管理[^1][^2][^4]。 --- ### 相关问题 1. 如何在自定义 ViewGroup 中正确处理子视图的 `margin` 属性? 2. 动态添加子视图时如何保存和恢复状态(如滚动位置)? 3. 自定义 ViewGroup 中如何处理触摸事件的分发和拦截? 4. 如何优化包含大量动态子视图的滚动性能?
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值