参考:自定义控件之绘图篇( 五):drawText()详解
博客笔记:自定义View之绘图(1)–drawText
Android 自定义View-怎么绘制居中文本?
drawText方法参数介绍:
public void drawText(String text, float x, float y, Paint paint)
y:所代表的是基线的位置
x:代表所要绘制文字所在矩形的相对位置。相对位置就是指指定点(x,y)在在所要绘制矩形的位置。我们知道所绘制矩形的纵坐标是由Y值来确定的,而相对x坐标的位置,只有左、中、右三个位置了。也就是所绘制矩形可能是在x坐标的左侧绘制,也有可能在x坐标的中间,也有可能在x坐标的右侧。
而定义在x坐标在所绘制矩形相对位置的函数是:
/**
* 其中Align的取值为:Panit.Align.LEFT,Paint.Align.CENTER,Paint.Align.RIGHT
*/
Paint::setTextAlign(Align align);
我们如果要画”harvic’s blog”这几个字,这个(x,y)坐标应当是下图中绿色小点的位置:
设置: paint.setTextAlign(Paint.Align.LEFT);
x坐标在所绘制矩形相对位置:左
设置paint.setTextAlign(Paint.Align.CENTER);
x坐标在所绘制矩形相对位置:中间
设置paint.setTextAlign(Paint.Align.RIGHT);
x坐标在所绘制矩形相对位置:右
FontMetrics
系统在画文字时的五条线,baseline、ascent、descent、top、bottom,我们知道baseline的位置是我们在构造drawText()时的参数y来决定的,那ascent,descent,top,bottom这些线的位置要怎么计算出来呢?
Android给我们提供了一个类:FontMetrics,它里面有四个成员变量:
FontMetrics::ascent;
FontMetrics::descent;
FontMetrics::top;
FontMetrics::bottom;
他们的意义与值的计算方法分别如下:
ascent = ascent线的y坐标 - baseline线的y坐标;//ascent变量的值是负的。
descent = descent线的y坐标 - baseline线的y坐标;//descent变量的值是正数。
top = top线的y坐标 - baseline线的y坐标;//top 变量的值是负的。
bottom = bottom线的y坐标 - baseline线的y坐标;//bottom变量的值是正数。
Text四线格的各线位置:
ascent线Y坐标 = baseline线的y坐标 + fontMetric.ascent;
descent线Y坐标 = baseline线的y坐标 + fontMetric.descent;
top线Y坐标 = baseline线的y坐标 + fontMetric.top;
bottom线Y坐标 = baseline线的y坐标 + fontMetric.bottom;
-1> topLine–可绘制最高线
-2> ascentLine–建议绘制单行字符最高线
-3> descentLine–建议绘制单行字符最低线
-4> bottomLine–可绘制最低线
获取FontMetrics对象
Paint paint = new Paint();
Paint.FontMetrics fm = paint.getFontMetrics();
Paint.FontMetricsInt fmInt = paint.getFontMetricsInt();
通过paint.getFontMetrics()得到对应的FontMetrics对象。
这里还有另外一个FontMetrics同样的类叫做FontMetricsInt,它的意义与FontMetrics完全相同,只是得到的值的类型不一样而已,FontMetricsInt中的四个成员变量的值都是Int类型,而FontMetrics得到的四个成员变量的值则都是float类型的。
字符串所占高度:
Paint.FontMetricsInt fm = paint.getFontMetricsInt();
int top = baseLineY + fm.top;
int bottom = baseLineY + fm.bottom;
//所占高度
int height = bottom - top;
字符串所占宽度:
Paint paint = new Paint();
//设置paint
paint.setTextSize(120); //以px为单位
//获取宽度
int width = (int)paint.measureText("harvic\'s blog");
字符串所占最小矩形:
要获取最小矩形,也是通过系统函数来获取的,函数及意义如下:
/**
* 获取指定字符串所对应的最小矩形,以(0,0)点所在位置为基线
* @param text 要测量最小矩形的字符串
* @param start 要测量起始字符在字符串中的索引
* @param end 所要测量的字符的长度
* @param bounds 接收测量结果
*/
public void getTextBounds(String text, int start, int end, Rect bounds);
示例:
String text = "harvic\'s blog";
Paint paint = new Paint();
//设置paint
paint.setTextSize(120); //以px为单位
Rect minRect = new Rect();
paint.getTextBounds(text,0,text.length(),minRect);
Log.e("qijian",minRect.toShortString());
有时候会发现左上角为负数,比如:左上角位置为(8,-90),右下角的位置为(634,26);
原因:从代码中,我们也可以看到,我们并没有给getTextBounds()传递基线位置。那它就是以(0,0)为基线来得到这个最小矩形的!所以这个最小矩形的位置就是以(0,0)为基线的结果!
由于paint.getTextBounds()得到最小矩形的基线是y = 0;那我们直接将这个矩形移动baseline的距离就可以得到这个矩形实际应当在的位置了。
所以矩形应当所在实际位置的坐标是:
Rect minRect = new Rect();
paint.getTextBounds(text,0,text.length(),minRect);
//最小矩形,实际top位置
int minTop = bounds.top + baselineY;
//最小矩形,实际bottom位置
int minBottom = bounds.bottom + baselineY;
示例:
MyTextView
package com.android.imooc;
import android.content.Context;
import android.graphics.Canvas;
import android.graphics.Color;
import android.graphics.Paint;
import android.graphics.Rect;
import android.support.annotation.Nullable;
import android.util.AttributeSet;
import android.util.Log;
import android.view.View;
public class MyTextView extends View {
public MyTextView(Context context) {
this(context, null);
}
public MyTextView(Context context, @Nullable AttributeSet attrs) {
this(context, attrs, 0);
}
public MyTextView(Context context, @Nullable AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
}
@Override
protected void onDraw(Canvas canvas) {
super.onDraw(canvas);
Paint paint = new Paint();
String mText = "xu qiang";
int baseLineX = 0;
int baseLineY = 200;
paint.setTextSize(180); //以px为单位
paint.setTextAlign(Paint.Align.LEFT);//会影响Paint在字符串哪个位置开始绘制
canvas.drawText(mText, baseLineX, baseLineY, paint);
/*---------------------------------五条线-------------------------------------*/
//计算各线在位置
Paint.FontMetrics fm = paint.getFontMetrics();
float ascent = baseLineY + fm.ascent;
float descent = baseLineY + fm.descent;
float top = baseLineY + fm.top;
float bottom = baseLineY + fm.bottom;
Log.e("111", "top==" + top);//9.892578
Log.e("111", "ascent==" + ascent);//33.007812
Log.e("111", "baseLineY==" + baseLineY);//200
Log.e("111", "descent==" + descent);//243.94531
Log.e("111", "bottom==" + bottom);//248.7793
//画top线
paint.setColor(Color.BLUE);
paint.setStrokeWidth(2);
canvas.drawLine(baseLineX, top, 3000, top, paint);
//画ascent线
paint.setColor(Color.GREEN);
paint.setStrokeWidth(2);
canvas.drawLine(baseLineX, ascent, 3000, ascent, paint);
//画基线线
paint.setColor(Color.RED);
paint.setStrokeWidth(2);
canvas.drawLine(baseLineX, baseLineY, 3000, baseLineY, paint);
//画descent线
paint.setColor(Color.CYAN);
paint.setStrokeWidth(2);
canvas.drawLine(baseLineX, descent, 3000, descent, paint);
//画bottom线
paint.setColor(Color.BLACK);
paint.setStrokeWidth(2);
canvas.drawLine(baseLineX, bottom, 3000, bottom, paint);
/*----------------------------------矩形区域--------------------------------*/
//字符串所占高度、宽度和最小矩形
Paint.FontMetricsInt fmInt = paint.getFontMetricsInt();
int str_top = baseLineY + fmInt.top;
int str_bottom = baseLineY + fmInt.bottom;
//字符串所占高度
int str_height = str_bottom - str_top;
//字符串所占宽度
int str_width = (int) paint.measureText(mText);
//字符串所占最小矩形
Rect minRect = new Rect();
paint.getTextBounds(mText, 0, mText.length(), minRect);
Log.e("111", "str_top==" + str_top);//9
Log.e("111", "str_bottom==" + str_bottom);//249
Log.e("111", "str_height==" + str_height);//240
Log.e("111", "str_width==" + str_width);//676
Log.e("111", "最小矩形==" + minRect.toString());//Rect(3, -130 - 664, 38)
Log.e("111", "最小矩形==" + minRect.toShortString());//[3,-130][664,38]
//画text所占的区域:高度从top到bottom(绿色区域)
int width = (int) paint.measureText(mText);
Rect rect = new Rect(baseLineX, str_top, baseLineX + width, str_bottom);
paint.setColor(Color.GREEN);
canvas.drawRect(rect, paint);
//画最小区域矩形(红色区域)
minRect.top = baseLineY + minRect.top;
minRect.bottom = baseLineY + minRect.bottom;
paint.setColor(Color.RED);
canvas.drawRect(minRect, paint);
//写文字(黑色字体)
paint.setColor(Color.BLACK);
paint.setTextSize(180); //以px为单位
paint.setTextAlign(Paint.Align.LEFT);//会影响Paint在字符串哪个位置开始绘制
canvas.drawText(mText, baseLineX, baseLineY, paint);
}
}
布局:
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:id="@+id/container"
android:background="#ff4081"
android:layout_width="match_parent"
android:layout_height="match_parent">
<com.android.imooc.MyTextView
android:layout_width="300dip"
android:layout_height="200dip"
android:background="#ffffff"/>
</LinearLayout>
效果图:
其他:
获取文本宽度:
public class MyTextView extends View {
private int baseline;
public MyTextView(Context context) {
this(context, null);
}
public MyTextView(Context context, @Nullable AttributeSet attrs) {
this(context, attrs, 0);
}
public MyTextView(Context context, @Nullable AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
}
@Override
protected void onDraw(Canvas canvas) {
super.onDraw(canvas);
String testString = "测试文本宽度";
Paint mPaint = new Paint();
mPaint.setTextSize(100);
mPaint.setTextAlign(Paint.Align.LEFT);
mPaint.setColor(Color.RED);
Rect mBound = new Rect();
mPaint.getTextBounds(testString, 0, testString.length(), mBound);
int x = getWidth() / 2 - (int) mPaint.measureText(testString) / 2;
Paint.FontMetricsInt fontMetrics = mPaint.getFontMetricsInt();
baseline = getHeight() / 2 - fontMetrics.descent + (fontMetrics.bottom - fontMetrics.top) / 2;
canvas.drawText(testString, x, baseline, mPaint);
//在使用Canvas绘制文字时,需要得到字符串的长度,Paint类内给了两个方法,measureText(),getTextBound();
//getTextBounds() 得到的宽度总是比 measureText() 得到的宽度要小一点。
//measureTextWidth来获取文本宽度
//Paint的getTextBounds方法--获取边界
int getTextBoundsWidth = mBound.right - mBound.left;
mPaint.setColor(Color.GREEN);
mPaint.setStrokeWidth(5);
Log.e("111", "getTextBounds===" + getTextBoundsWidth);//getTextBounds===592
canvas.drawLine(x, baseline + 15, x + getTextBoundsWidth, baseline + 15, mPaint);
//Paint的measureText方法--获取宽度
int measureTextWidth = (int) mPaint.measureText(testString);
Log.e("111", "measureTextWidth===" + measureTextWidth);//measureTextWidth===600
mPaint.setStrokeWidth(5);
mPaint.setColor(Color.BLACK);
canvas.drawLine(x, baseline + 10, x + measureTextWidth, baseline + 10, mPaint);
}
}
效果图:
绘制文本在中间:
一般常见方法,MyTextView1(不推荐)
/*
* 控件宽度/2 - 文字宽度/2
*/
float startX = getWidth() / 2 - mBound.width() / 2;
/*
* 控件高度/2 + 文字高度/2,绘制文字从文字左下角开始,因此"+"
*/
float startY = getHeight() / 2 + mBound.height() / 2;
// 绘制文字
canvas.drawText(mText, startX, startY, mPaint);
示例:
public class MyTextView1 extends View {
private int baseline;
public MyTextView1(Context context) {
this(context, null);
}
public MyTextView1(Context context, @Nullable AttributeSet attrs) {
this(context, attrs, 0);
}
public MyTextView1(Context context, @Nullable AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
}
@Override
protected void onDraw(Canvas canvas) {
super.onDraw(canvas);
String testString = "测试居中";
Paint mPaint = new Paint();
mPaint.setTextSize(60);
mPaint.setColor(Color.RED);
mPaint.setTextAlign(Paint.Align.LEFT);
Rect mBound = new Rect();
mPaint.getTextBounds(testString, 0, testString.length(), mBound);
// X=控件宽度/2 - 文本宽度/2;
int x = getWidth() / 2 - mBound.width() / 2;
Log.e("111", "x1===" + x);//
// Y=控件高度/2 + 文本宽度/2;
baseline = getHeight() / 2 + mBound.height() / 2;
Log.e("111", "baseline1===" + baseline);//
// 绘制文字
canvas.drawText(testString, x, baseline, mPaint);
// 绘制基线
mPaint.setColor(Color.YELLOW);
mPaint.setStrokeWidth(5);
canvas.drawLine(0, baseline, getWidth(), baseline, mPaint);
canvas.drawLine(x, 0, x, getHeight(), mPaint);
// 绘制中线,做对比
mPaint.setColor(Color.BLACK);
mPaint.setStrokeWidth(5);
canvas.drawLine(0, getHeight() / 2, getWidth(), getHeight() / 2, mPaint);
canvas.drawLine(getWidth() / 2, 0, getWidth() / 2, getHeight(), mPaint);
}
}
文字宽度计算改进,这是比较精确的测量文字宽度的方式
int value = (int)mPaint.measureText(mText);//准确的文本宽度
修正计算 Y 值的计算公式:
FontMetricsInt fm = mPaint.getFontMetricsInt();
//方式一:见MyTextView (推荐)
int startY = getHeight() / 2 - fm.descent + (fm.bottom - fm.top) / 2;
//方式二:见MyTextView2 : descent+ascent(忽略了音标)
int startY = getHeight() / 2 - fm.descent + (fm.descent - fm.ascent)/ 2
= getHeight() / 2 - (fm.descent + fm.ascent)/ 2;
MyTextView (推荐方式):
public class MyTextView extends View {
private int baseline;
public MyTextView(Context context) {
this(context, null);
}
public MyTextView(Context context, @Nullable AttributeSet attrs) {
this(context, attrs, 0);
}
public MyTextView(Context context, @Nullable AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
}
@Override
protected void onDraw(Canvas canvas) {
super.onDraw(canvas);
String testString = "测试居中";
Paint mPaint = new Paint();
mPaint.setTextSize(60);
mPaint.setColor(Color.RED);
Rect mBound = new Rect();
mPaint.getTextBounds(testString, 0, testString.length(), mBound);
mPaint.setTextAlign(Paint.Align.LEFT);
int x = getWidth() / 2 - (int) mPaint.measureText(testString) / 2;
Log.e("111", "x===" + x);//
Paint.FontMetricsInt fontMetrics = mPaint.getFontMetricsInt();
baseline = getHeight() / 2 - fontMetrics.descent +
(fontMetrics.bottom - fontMetrics.top) / 2;
Log.e("111", "baseline===" + baseline);//
// 绘制文字
canvas.drawText(testString, x, baseline, mPaint);
// 绘制起点、基线
mPaint.setColor(Color.YELLOW);
mPaint.setStrokeWidth(5);
canvas.drawLine(0, baseline, getWidth(), baseline, mPaint);
canvas.drawLine(x, 0, x, getHeight(), mPaint);
// 绘制View的中线,做对比
mPaint.setColor(Color.BLACK);
mPaint.setStrokeWidth(5);
canvas.drawLine(0, getHeight() / 2, getWidth(), getHeight() / 2, mPaint);
canvas.drawLine(getWidth() / 2, 0, getWidth() / 2, getHeight(), mPaint);
}
}
MyTextView2
public class MyTextView2 extends View {
private int baseline;
public MyTextView2(Context context) {
this(context, null);
}
public MyTextView2(Context context, @Nullable AttributeSet attrs) {
this(context, attrs, 0);
}
public MyTextView2(Context context, @Nullable AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
}
@Override
protected void onDraw(Canvas canvas) {
super.onDraw(canvas);
String testString = "测试居中";
Paint mPaint = new Paint();
mPaint.setTextSize(60);
mPaint.setColor(Color.RED);
Rect mBound = new Rect();
mPaint.getTextBounds(testString, 0, testString.length(), mBound);
mPaint.setTextAlign(Paint.Align.LEFT);
int x = getWidth() / 2 - (int) mPaint.measureText(testString) / 2;
Log.e("111", "x2===" + x);//
Paint.FontMetricsInt fontMetrics = mPaint.getFontMetricsInt();
baseline = getHeight() / 2 - fontMetrics.descent +
(fontMetrics.descent - fontMetrics.ascent)/ 2;
Log.e("111", "baseline2===" + baseline);//
// 绘制文字
canvas.drawText(testString, x, baseline, mPaint);
// 绘制起点、基线
mPaint.setColor(Color.YELLOW);
mPaint.setStrokeWidth(5);
canvas.drawLine(0, baseline, getWidth(), baseline, mPaint);
canvas.drawLine(x, 0, x, getHeight(), mPaint);
// 绘制View的中线,做对比
mPaint.setColor(Color.BLACK);
mPaint.setStrokeWidth(5);
canvas.drawLine(0, getHeight() / 2, getWidth(), getHeight() / 2, mPaint);
canvas.drawLine(getWidth() / 2, 0, getWidth() / 2, getHeight(), mPaint);
}
}
作参考的TextView:CutomTextView
@SuppressLint("AppCompatCustomView")
public class CutomTextView extends TextView{
public CutomTextView(Context context) {
super(context);
}
public CutomTextView(Context context, @Nullable AttributeSet attrs) {
super(context, attrs);
}
public CutomTextView(Context context, @Nullable AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
}
@Override
protected void onDraw(Canvas canvas) {
super.onDraw(canvas);
// 绘制View的中线,做对比
Paint mPaint = new Paint();
mPaint.setColor(Color.BLACK);
mPaint.setStrokeWidth(5);
canvas.drawLine(0, getHeight() / 2, getWidth(), getHeight() / 2, mPaint);
canvas.drawLine(getWidth() / 2, 0, getWidth() / 2, getHeight(), mPaint);
}
}
布局一:
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:id="@+id/container"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:background="#ff4081"
android:orientation="vertical">
<com.android.imooc.MyTextView
android:layout_width="match_parent"
android:layout_height="60dp"
android:background="#ffffff" />
<com.android.imooc.MyTextView2
android:layout_width="match_parent"
android:layout_height="60dp"
android:background="#00ff00" />
<com.android.imooc.CutomTextView
android:layout_width="match_parent"
android:layout_height="60dp"
android:background="#e9e9e9"
android:gravity="center"
android:text="测试文本居中"
android:textColor="#ff0000"
android:textSize="100px" />
<com.android.imooc.MyTextView1
android:layout_width="match_parent"
android:layout_height="60dp"
android:background="#00ff00" />
</LinearLayout>
效果图一:
布局二:
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:id="@+id/container"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:background="#e6e6e6"
android:orientation="horizontal">
<com.android.imooc.MyTextView
android:layout_width="0dp"
android:layout_height="100dp"
android:layout_weight="1"
android:background="#ffffff" />
<com.android.imooc.MyTextView2
android:layout_width="0dp"
android:layout_height="100dp"
android:layout_weight="1"
android:background="#00ff00" />
<com.android.imooc.CutomTextView
android:layout_width="0dp"
android:layout_height="100dp"
android:layout_weight="1"
android:gravity="center"
android:text="测试居中"
android:textColor="#ff0000"
android:textSize="60px" />
<com.android.imooc.MyTextView1
android:layout_width="0dp"
android:layout_height="100dp"
android:layout_weight="1"
android:background="#00ff00" />
</LinearLayout>
效果图二:
通过对比可见:
1(MyTextView)与 3(CutomTextView即:TextView)最接近,
即以下绘制文字居中和原生TextView最接近,适合做居中:
Paint mPaint = new Paint();
int x = getWidth() / 2 - (int) mPaint.measureText(testString) / 2;
Paint.FontMetricsInt fontMetrics = mPaint.getFontMetricsInt();
baseline = getHeight() / 2 - fontMetrics.descent + (fontMetrics.bottom - fontMetrics.top) / 2;
// 绘制文字
canvas.drawText(testString, x, baseline, mPaint);
或者
// 计算Baseline绘制的起点X轴坐标 ,计算方式:画布宽度的一半 - 文字宽度的一半
int baseX = (int) (canvas.getWidth() / 2 - mTextPaint.measureText(TEXT) / 2);
// 计算Baseline绘制的Y坐标 ,计算方式:画布高度的一半 - 文字总高度的一半
int baseY = (int) ((canvas.getHeight() / 2) - ((mTextPaint.descent() + mTextPaint.ascent()) / 2));
注意:
mTextPaint.ascent()为负数
mTextPaint.descent()为正数
ascent 和 descent 这两个值还可以通过 Paint.ascent() 和 Paint.descent() 来快捷获取。