本文罗列Android中字体宽度和高度的相关概念,及测量方法 。
原文请参考Android_FontMetrics、Android字符串进阶之三:字体属性及测量(FontMetrics)、 Android UI 之居中绘制文本内容的正确方法——实现自定义一个TextView。
我们在自定义一个控件的时候,有时候会需要自己来绘制一些文本内容,这样就自然而然遇到确定文本的宽高尺寸和方位的问题,事实上明确了控件和文本的宽高,就可以根据需要确定文本的方位是居中、居上还是左上等。
Canvas 绘制文本时,使用FontMetrics对象,计算文本位置的坐标。
说明如下:
public static class FontMetrics { /** * The maximum distance above the baseline for the tallest glyph in * the font at a given text size. */ public float top; /** * The recommended distance above the baseline for singled spaced text. */ public float ascent; /** * The recommended distance below the baseline for singled spaced text. */ public float descent; /** * The maximum distance below the baseline for the lowest glyph in * the font at a given text size. */ public float bottom; /** * The recommended additional space to add between lines of text. */ public float leading; }
1. 基准点是baseline
2. Ascent是baseline之上至字符最高处的距离
3. Descent是baseline之下至字符最低处的距离
4. Leading文档说的很含糊,其实是上一行字符的descent到下一行的ascent之间的距离
5. Top指的是指的是最高字符到baseline的值,即ascent的最大值
6. 同上,bottom指的是最下字符到baseline的值,即descent的最大值
descent-ascent就可以看作文本的高度。
图示如下:
Paint类有两个方法,也可以获取文本的高度:
ascent():the distance above the baseline(baseline以上的height)
/** * Return the distance above (negative) the baseline (ascent) based on the * current typeface and text size. * * @return the distance above (negative) the baseline (ascent) based on the * current typeface and text size. */ public native float ascent(); /** * Return the distance below (positive) the baseline (descent) based on the * current typeface and text size. * * @return the distance below (positive) the baseline (descent) based on * the current typeface and text size. */ public native float descent();
descent():the distance below the baseline(baseline以下的height)
所以descent()-ascent()也可以看成文字的height。
上面两种方法得到的文字高度是一致的,但从本人经验来说,这种高度对数字来说略高,比如在画折线图上的坐标轴值时就有很明显的体现,这种方式drawText()在Y轴上的值看上去比其值略偏下。
这种情况下,下面这种方式计算出来的文字宽高更准确一些。
下面给出一个综合Demo演示:
Paint pFont = new Paint(); Rect rect = new Rect(); pFont.getTextBounds( "豆", 0, 1, rect); Log.v(TAG, "height:"+rect.height()+ "width:"+rect.width());
打印结果:
package com.example.textmeasure; import android.app.Activity; import android.graphics.Paint; import android.graphics.Rect; import android.graphics.Paint.FontMetrics; import android.os.Bundle; import android.util.DisplayMetrics; import android.util.Log; public class MainActivity extends Activity { Paint mPaint= null; public float screenDensity; public float screenScaledDensity; public float textHeight1; public float textHeight2; public float textHeight3; public float textWidthA; public float textWidthB; @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_main); //获取屏幕密度和字体适用的密度 DisplayMetrics dm = getResources().getDisplayMetrics(); screenDensity = dm.density; screenScaledDensity = dm.scaledDensity; mPaint= new Paint(); mPaint.setTextSize( 15*screenScaledDensity); //第一种获取文本高度的方式 FontMetrics fm=mPaint.getFontMetrics(); textHeight1=fm.descent-fm.ascent; //第二种获取文本高度的方式 textHeight2=mPaint.descent()-mPaint.ascent(); //第三种获取文本高度的方式 Rect bounds= new Rect(); mPaint.getTextBounds( "0.00", 0, "0.00".length(), bounds); textHeight3=bounds.height(); //获取文本宽度 textWidthA=bounds.width(); //获取文本宽度的另一种方式 textWidthB=mPaint.measureText( "0.00"); Log.i( "publish", "textHeight1: "+textHeight1); Log.i( "publish", "textHeight2: "+textHeight2); Log.i( "publish", "textHeight3: "+textHeight3); Log.i( "publish", "textWidthA: "+textWidthA); Log.i( "publish", "textWidthB: "+textWidthB); } }
上面的Demo中使用到屏幕密度,代码中出现的所有尺寸单位都是像素,因此需要使用屏幕密度进行适配,其中dm.density是与控件尺寸相关的密度,dm.scaledDensity是与字体大小相关的密度。
03- 23 12: 54: 16.325: I/publish( 27219): screenDensity: 3.0 03- 23 12: 54: 16.325: I/publish( 27219): screenScaledDensity: 3.0 03- 23 12: 54: 16.325: I/publish( 27219): textHeight1: 52.734375 03- 23 12: 54: 16.325: I/publish( 27219): textHeight2: 52.734375 03- 23 12: 54: 16.325: I/publish( 27219): textHeight3: 32.0 03- 23 12: 54: 16.325: I/publish( 27219): textWidthA: 82.0 03- 23 12: 54: 16.325: I/publish( 27219): textWidthB: 87.0
在Android layout文件中使用sp,dp尺寸已经在一定程度上进行了适配。
下面说一下如何在一个自定义View中drawText()时设置文本居中,这部分内容转载于http://blog.csdn.net/carrey1989/article/details/10399727。
我们在画布中绘制文本的时候,会调用Canvas.drawText(String text, float x, float y, Paint paint)这个方法,其中y的坐标就是上图中baseline的y坐标,x坐标是文本绘制的起始x轴坐标。
因此要想文本居中,应如下计算:
float textCenterVerticalBaselineY = viewHeight / 2 - fm.descent + (fm.bottom - fm.top) / 2;
其中,textCenterVerticalBaselineY就是绘制文本时候的y坐标,viewHeight是控件的高度。这个换算关系不难理解,viewHeight/2-fm.descent的意思是将整个文字区域抬高到控件的1/2,然后我们再加上(fm.bottom - fm.top) / 2的意思就是将文本下沉文本top到bottom长度的一半,从而实现文本垂直居中的目的。
有的人或许会问,为什么最后加上的是bottom到top距离的一半而不是descent到ascent的一半呢?其实这个是我测试的结果,我发现如果用bottom到top距离的一半来设置文本垂直居中,和系统控件TextView的文本居中效果是一样的,我们来看下面的效果:
首先是使用(fm.bottom - fm.top) / 2的:
然后是使用然后是使用(fm.descent - fm.ascent) / 2:
左边绿色的是系统的TextView文字居中效果,右边是我们自定义控件的文字居中效果,可以看出使用(fm.bottom - fm.top) / 2与TextView的效果是一样的,当然,我们不必一定要与TextView的效果相同,所以使用(fm.descent - fm.ascent) / 2也是可以的。
下面自定义一个可以控制内部文本绘制方位的TextView:
//项目代码: //MyTextView.java package com.example.textalignment.mytextview; import com.example.textalignment.util.DisplayParams; import com.example.textalignment.util.DisplayUtil; import android.content.Context; import android.graphics.Bitmap; import android.graphics.Canvas; import android.graphics.Color; import android.graphics.Matrix; import android.graphics.Paint; import android.graphics.RectF; import android.graphics.Paint.Align; import android.graphics.Paint.FontMetrics; import android.graphics.drawable.Drawable; import android.util.AttributeSet; import android.util.DisplayMetrics; import android.view.View; /** * 自定义文本显示控件 * 该自定义控件中的文本可以在9个方位进行控制 * 左上——中上——右上 * 左中——中中——右中 * 左下——中下——右下 * @author carrey * */ public class MyTextView extends View { /** 要显示的文字 */ private String text; /** 文字的颜色 */ private int textColor; /** 文字的大小 */ private int textSize; /** 文字的方位 */ private int textAlign; // public static final int TEXT_ALIGN_CENTER = 0x00000000; public static final int TEXT_ALIGN_LEFT = 0x00000001; public static final int TEXT_ALIGN_RIGHT = 0x00000010; public static final int TEXT_ALIGN_CENTER_VERTICAL = 0x00000100; public static final int TEXT_ALIGN_CENTER_HORIZONTAL = 0x00001000; public static final int TEXT_ALIGN_TOP = 0x00010000; public static final int TEXT_ALIGN_BOTTOM = 0x00100000; /** 文本中轴线X坐标 */ private float textCenterX; /** 文本baseline线Y坐标 */ private float textBaselineY; /** 控件的宽度 */ private int viewWidth; /** 控件的高度 */ private int viewHeight; /** 控件画笔 */ private Paint paint; private FontMetrics fm; /** 场景 */ private Context context; public MyTextView(Context context) { super(context); this.context = context; init(); } public MyTextView(Context context, AttributeSet attrs) { super(context, attrs); this.context = context; init(); } /** * 变量初始化 */ private void init() { paint = new Paint(); paint.setAntiAlias( true); paint.setTextAlign(Align.CENTER); //默认情况下文字居中显示 textAlign = TEXT_ALIGN_CENTER_HORIZONTAL | TEXT_ALIGN_CENTER_VERTICAL; //默认的文本颜色是黑色 this.textColor = Color.BLACK; } @Override protected void onLayout(boolean changed, int left, int top, int right, int bottom) { viewWidth = getWidth(); viewHeight = getHeight(); super.onLayout(changed, left, top, right, bottom); } @Override protected void onDraw(Canvas canvas) { //绘制控件内容 setTextLocation(); canvas.drawText(text, textCenterX, textBaselineY, paint); super.onDraw(canvas); } /** * 定位文本绘制的位置 */ private void setTextLocation() { paint.setTextSize(textSize); paint.setColor(textColor); fm = paint.getFontMetrics(); //文本的宽度 float textWidth = paint.measureText(text); float textCenterVerticalBaselineY = viewHeight / 2 - fm.descent + (fm.descent - fm.ascent) / 2; switch (textAlign) { case TEXT_ALIGN_CENTER_HORIZONTAL | TEXT_ALIGN_CENTER_VERTICAL: textCenterX = ( float)viewWidth / 2; textBaselineY = textCenterVerticalBaselineY; break; case TEXT_ALIGN_LEFT | TEXT_ALIGN_CENTER_VERTICAL: textCenterX = textWidth / 2; textBaselineY = textCenterVerticalBaselineY; break; case TEXT_ALIGN_RIGHT | TEXT_ALIGN_CENTER_VERTICAL: textCenterX = viewWidth - textWidth / 2; textBaselineY = textCenterVerticalBaselineY; break; case TEXT_ALIGN_BOTTOM | TEXT_ALIGN_CENTER_HORIZONTAL: textCenterX = viewWidth / 2; textBaselineY = viewHeight - fm.bottom; break; case TEXT_ALIGN_TOP | TEXT_ALIGN_CENTER_HORIZONTAL: textCenterX = viewWidth / 2; textBaselineY = -fm.ascent; break; case TEXT_ALIGN_TOP | TEXT_ALIGN_LEFT: textCenterX = textWidth / 2; textBaselineY = -fm.ascent; break; case TEXT_ALIGN_BOTTOM | TEXT_ALIGN_LEFT: textCenterX = textWidth / 2; textBaselineY = viewHeight - fm.bottom; break; case TEXT_ALIGN_TOP | TEXT_ALIGN_RIGHT: textCenterX = viewWidth - textWidth / 2; textBaselineY = -fm.ascent; break; case TEXT_ALIGN_BOTTOM | TEXT_ALIGN_RIGHT: textCenterX = viewWidth - textWidth / 2; textBaselineY = viewHeight - fm.bottom; break; } } /** * 设置文本内容 * @param text */ public void setText(String text) { this.text = text; invalidate(); } /** * 设置文本大小 * @param textSizeSp 文本大小,单位是sp */ public void setTextSize(int textSizeSp) { DisplayParams displayParams = DisplayParams.getInstance(context); this.textSize = DisplayUtil.sp2px(textSizeSp, displayParams.fontScale); invalidate(); } /** * 设置文本的方位 */ public void setTextAlign(int textAlign) { this.textAlign = textAlign; invalidate(); } /** * 设置文本的颜色 * @param textColor */ public void setTextColor(int textColor) { this.textColor = textColor; invalidate(); } }
//MainActivity.java package com.example.textalignment; import com.example.textalignment.mytextview.MyTextView; import com.example.textalignment.util.DisplayParams; import com.example.textalignment.util.DisplayUtil; import android.os.Bundle; import android.app.Activity; import android.graphics.Bitmap; import android.graphics.BitmapFactory; import android.graphics.Color; import android.view.Menu; import android.widget.LinearLayout; import android.widget.ScrollView; public class MainActivity extends Activity { @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_main); DisplayParams displayParams = DisplayParams.getInstance( this); LinearLayout container = (LinearLayout) findViewById(R.id.container); MyTextView myTextView1 = new MyTextView( this); myTextView1.setText( "居中的文本"); myTextView1.setTextSize( 30); myTextView1.setTextAlign(MyTextView.TEXT_ALIGN_CENTER_HORIZONTAL | MyTextView.TEXT_ALIGN_CENTER_VERTICAL); myTextView1.setTextColor(Color.BLUE); myTextView1.setBackgroundColor(Color.RED); container.addView(myTextView1, LinearLayout.LayoutParams.MATCH_PARENT, DisplayUtil.dip2px( 150, displayParams.scale)); MyTextView myTextView2 = new MyTextView( this); myTextView2.setText( "居左的文本"); myTextView2.setTextSize( 25); myTextView2.setTextAlign(MyTextView.TEXT_ALIGN_CENTER_VERTICAL | MyTextView.TEXT_ALIGN_LEFT); myTextView2.setTextColor(Color.GREEN); myTextView2.setBackgroundColor(Color.YELLOW); container.addView(myTextView2, LinearLayout.LayoutParams.MATCH_PARENT, DisplayUtil.dip2px( 150, displayParams.scale)); MyTextView myTextView3 = new MyTextView( this); myTextView3.setText( "右下的文本"); myTextView3.setTextSize( 15); myTextView3.setTextAlign(MyTextView.TEXT_ALIGN_BOTTOM | MyTextView.TEXT_ALIGN_RIGHT); myTextView3.setTextColor(Color.RED); myTextView3.setBackgroundColor(Color.BLUE); container.addView(myTextView3, LinearLayout.LayoutParams.MATCH_PARENT, DisplayUtil.dip2px( 150, displayParams.scale)); MyTextView myTextView4 = new MyTextView( this); myTextView4.setText( "左下的文本"); myTextView4.setTextSize( 15); myTextView4.setTextAlign(MyTextView.TEXT_ALIGN_BOTTOM | MyTextView.TEXT_ALIGN_LEFT); myTextView4.setTextColor(Color.YELLOW); myTextView4.setBackgroundColor(Color.GREEN); container.addView(myTextView4, LinearLayout.LayoutParams.MATCH_PARENT, DisplayUtil.dip2px( 150, displayParams.scale)); MyTextView myTextView5 = new MyTextView( this); myTextView5.setText( "中下的文本"); myTextView5.setTextSize( 35); myTextView5.setTextAlign(MyTextView.TEXT_ALIGN_BOTTOM | MyTextView.TEXT_ALIGN_CENTER_HORIZONTAL); myTextView5.setTextColor(Color.GRAY); myTextView5.setBackgroundColor(Color.RED); container.addView(myTextView5, LinearLayout.LayoutParams.MATCH_PARENT, DisplayUtil.dip2px( 150, displayParams.scale)); MyTextView myTextView6 = new MyTextView( this); myTextView6.setText( "居右的文本"); myTextView6.setTextSize( 25); myTextView6.setTextAlign(MyTextView.TEXT_ALIGN_RIGHT | MyTextView.TEXT_ALIGN_CENTER_VERTICAL); myTextView6.setTextColor(Color.BLUE); myTextView6.setBackgroundColor(Color.YELLOW); container.addView(myTextView6, LinearLayout.LayoutParams.MATCH_PARENT, DisplayUtil.dip2px( 150, displayParams.scale)); MyTextView myTextView7 = new MyTextView( this); myTextView7.setText( "左上的文本"); myTextView7.setTextSize( 25); myTextView7.setTextAlign(MyTextView.TEXT_ALIGN_TOP | MyTextView.TEXT_ALIGN_LEFT); myTextView7.setTextColor(Color.GREEN); myTextView7.setBackgroundColor(Color.CYAN); container.addView(myTextView7, LinearLayout.LayoutParams.MATCH_PARENT, DisplayUtil.dip2px( 150, displayParams.scale)); MyTextView myTextView8 = new MyTextView( this); myTextView8.setText( "中上的文本"); myTextView8.setTextSize( 25); myTextView8.setTextAlign(MyTextView.TEXT_ALIGN_TOP | MyTextView.TEXT_ALIGN_CENTER_HORIZONTAL); myTextView8.setTextColor(Color.RED); myTextView8.setBackgroundColor(Color.GREEN); container.addView(myTextView8, LinearLayout.LayoutParams.MATCH_PARENT, DisplayUtil.dip2px( 150, displayParams.scale)); MyTextView myTextView9 = new MyTextView( this); myTextView9.setText( "右上的文本"); myTextView9.setTextSize( 25); myTextView9.setTextAlign(MyTextView.TEXT_ALIGN_TOP | MyTextView.TEXT_ALIGN_RIGHT); myTextView9.setTextColor(Color.YELLOW); myTextView9.setBackgroundColor(Color.BLUE); container.addView(myTextView9, LinearLayout.LayoutParams.MATCH_PARENT, DisplayUtil.dip2px( 150, displayParams.scale)); } }
activity_main.xml <ScrollView xmlns:android= "http://schemas.android.com/apk/res/android" xmlns:tools= "http://schemas.android.com/tools" android:layout_width= "match_parent" android:layout_height= "match_parent" android:paddingBottom= "@dimen/activity_vertical_margin" android:paddingLeft= "@dimen/activity_horizontal_margin" android:paddingRight= "@dimen/activity_horizontal_margin" android:paddingTop= "@dimen/activity_vertical_margin" tools:context= ".MainActivity" > <LinearLayout android:id= "@+id/container" android:layout_width= "match_parent" android:layout_height= "wrap_content" android:orientation= "vertical"/> </ScrollView>
还用到了两个工具类,代码可以参考这篇文章http://blog.csdn.net/carrey1989/article/details/10360613
在进行垂直偏上和垂直偏下的设置时,关键是设置baseline的y坐标分别等于-fm.ascent和viewHeight - fm.bottom,意思就是可以让文字刚好不超过控件的边缘。
来源:oschina
链接:https://my.oschina.net/u/4387051/blog/4334885