自定义View
学的是啥?无非就两种:绘制文字和绘制图像
通过上篇的学习,了解到Paint
类中有很多方法关于属性设置的方法。本篇就记录我学习绘制文字的过程。
学习资料:
- 爱哥写得非常非常好的系列:自定义控件其实很简单1/4
- Paint的源码api
1.简单效果
代码:
public class DrawTextView extends View {
private Paint mPaint ;
private String text = "英勇青铜5+abcdefg";
public DrawTextView(Context context, AttributeSet attrs) {
super(context, attrs);
initPaint();
}
/**
* 初始化画笔设置
*/
private void initPaint() {
mPaint = new Paint(Paint.ANTI_ALIAS_FLAG);
mPaint.setColor(Color.parseColor("#FF4081"));
mPaint.setTextSize(90f);
}
/**
* 绘制
* @param canvas
*/
@Override
protected void onDraw(Canvas canvas) {
super.onDraw(canvas);
canvas.drawText(text, 0 ,getHeight()/2 ,mPaint);
}
/**
* 测量
* @param widthMeasureSpec
* @param heightMeasureSpec
*/
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
super.onMeasure(widthMeasureSpec, heightMeasureSpec);
int wSpecMode = MeasureSpec.getMode(widthMeasureSpec);
int wSpecSize = MeasureSpec.getSize(widthMeasureSpec);
int hSpecMode = MeasureSpec.getMode(heightMeasureSpec);
int hSpecSize = MeasureSpec.getSize(heightMeasureSpec);
if (wSpecMode == MeasureSpec.AT_MOST && hSpecMode == MeasureSpec.AT_MOST){
setMeasuredDimension(300,300);
}else if (wSpecMode == MeasureSpec.AT_MOST){
setMeasuredDimension(300,hSpecSize);
}else if (hSpecMode == MeasureSpec.AT_MOST) {
setMeasuredDimension(wSpecSize,300);
}
}
}
代码比较简单。
再看效果图,此时文字的y
坐标虽然设置了getHeight()/2
,但很明显,文字所处的y
轴的位置不是控件的高的一半。很简单,文字本身也有高度,在绘制的时候,计算坐标并没有考虑文字本身的宽高。
现在首先解决的需求,就是让文字在这个自定义的DrawTextView
控件中居中
2.在X轴居中
@Override
protected void onDraw(Canvas canvas) {
super.onDraw(canvas);
//拿到字符串的宽度
float stringWidth = mPaint.measureText(text);
float x =(getWidth()-stringWidth)/2;
canvas.drawText(text, x ,getHeight()/2 ,mPaint);
}
利用measureText(String text)
这个方法,很容易拿到要绘制文字的宽度,再根据(getWidth()-stringWidth)/2
简单计算,就可以得到在X
轴起始绘制坐标
源码中,measureText(String text)
调用了measureText(text, 0, text.length())
measureText(String text, int start, int end)
text The text to measure. Cannot be null.
start The index of the first character to start measuring
end 1 beyond the index of the last character to measure
方法中传入字符串,并指定开始测量角标和结束角标,返回结果为float
型的值
2.在Y轴居中
想要在Y
轴居中,就要确定出绘制文字baseline
时的所在Y
轴的坐标。
在Android
中,和文字高度相关的信息都存在FontMetrics
对象中。。
2.1 FontMetrics 字体度量
FontMetrics
是Paint
的一个静态内部类
/**
* Class that describes the various metrics for a font at a given text size.
* Remember, Y values increase going down, so those values will be positive,
* and values that measure distances going up will be negative. This class
* is returned by getFontMetrics().
*/
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;
}
在FontMetrics
有五个float
类型值:
leading
留给文字音标符号的距离ascent
从baseline
线到最高的字母顶点到距离,负值top
从baseline
线到字母最高点的距离加上ascent
descent
从baseline
线到字母最低点到距离bottom
和top
类似,系统为一些极少数符号留下的空间。top
和bottom
总会比ascent
和descent
大一点的就是这些少到忽略的特殊符号
baseline
上为负,下为正。可以理解为文字坐标系中的x
轴,实际的Log打印的值
文字的绘制是从baseline
开始的
2.2确定文字Y
轴的坐标
由于文字绘制是从baseline
开始,所以想要文字的正中心和DrawTextView
的中心重合,baseline
就不能和getHeight()/2
重合,而且baseline
要在getHeight()/2
下方。
但要在下方多少?就是2号线和3号线之间的距离。
|ascent|
=descent
+ 2 * ( 2号线和3号线之间的距离 )
@Override
protected void onDraw(Canvas canvas) {
super.onDraw(canvas);
//文字的x轴坐标
float stringWidth = mPaint.measureText(text);
float x = (getWidth() - stringWidth) / 2;
//文字的y轴坐标
Paint.FontMetrics fontMetrics = mPaint.getFontMetrics();
float y = getHeight() / 2 + (Math.abs(fontMetrics.ascent) - fontMetrics.descent) / 2;
canvas.drawText(text, x, y, mPaint);
}
这里只需要自己在画图板自己画一次,就差不多可以理解。
文字在中心需求已经完成。
3.其他的方法
3.1 setTextAlign(Align align)
设置对齐方式
-
Paint.Align.LEFT
左对齐 -
Paint.Align.CENTER
中心对齐,绘制从 -
Paint.Align.RIGHT
右对齐
这个方法影响的是两端的绘制起始。LEFT
就是从左端开始,所以使用这三个属性时,在drawText(test,x,y,paint);
要注意x
坐标,否则,绘制会出现错乱
LEFT
对应0
,CENTER
对应getWidth()/2
, RIGHT
对应getWidth()
3.2 setTypeface(Typeface typeface)
设置字体
系统提供了五种字体:DEFAULT
,DEFAULT_BOLD
,SANS_SERIF
,SERIF
,MONOSPACE
,除了粗体,没看出有太大区别
这个对象可以用来加载自定义的字体
Typeface createFromAsset(AssetManager mgr, String path)
从assets
资源中加载字体Typeface createFromFile(String path)
通过路径加载字体文件Typeface createFromFile(File file)
通过指定文件加载字体
也可以通过Typefacecreate(Typeface family, int style)
方法拿到字体样式
Typeface font = Typeface.create(Typeface.SANS_SERIF, Typeface.BOLD);
textPaint.setTypeface( font );
-
Typeface.NORMAL
默认 -
Typeface.BOLD
粗体 -
Typeface.ITALIC
斜体 -
Typeface.BOLD_ITALIC
粗斜体
3.3 setStyle
设置画笔样式
-
Paint.Style.FILL
实心充满
-
Paint.Style.STROKE
Paint.Style.FILL_AND_STROKE
这个暂时没发现和FILL
有啥不同
3.4 setFlags(int flags)
设置画笔的flag
-
ANTI_ALIAS_FLAG
抗锯齿 -
DITHER_FLAG
防抖动
其他还有一堆,试了试,没看出太大区别。常见的就是抗锯齿,遇到特殊的需求,再来深入了解
也可以直接在Paint
的构造方法中指定
3.5PathEffect setPathEffect(PathEffect effect)
设置路径效果
<p>
这个方法感觉不应该放在本篇,应该算作图像。不过,代码写好了,也就放在这了。
public class DrawTextView extends View {
private Paint textPaint;
private Paint pathPaint;
private Path mPath;
private String[] pathEffectName = {
"默认", "CornerPathEffect", "DashPathEffect", "PathDashPathEffect",
"SumPathEffect", "DiscretePathEffect", "ComposePathEffect"
};
private PathEffect[] mPathEffect;
public DrawTextView(Context context, AttributeSet attrs) {
super(context, attrs);
initPaint();
}
/**
* 初始化画笔设置
*/
private void initPaint() {
//文字画笔
textPaint = new Paint(Paint.ANTI_ALIAS_FLAG);
textPaint.setColor(Color.WHITE);
textPaint.setTextSize(40f);
//路径画笔
pathPaint = new Paint(Paint.ANTI_ALIAS_FLAG);
pathPaint.setColor(Color.parseColor("#FF4081"));
pathPaint.setStrokeWidth(5F);
pathPaint.setStyle(Paint.Style.STROKE);
mPath = new Path();
//设置起点
mPath.moveTo(0, 0);
//路径连接的点
for (int i = 1; i < 37; i++) {
mPath.lineTo(i * 30, (float) (Math.random() * 100));
}
//初始化PathEffect
mPathEffect = new PathEffect[7];
mPathEffect[0] = new PathEffect();//默认
mPathEffect[1] = new CornerPathEffect(10f);
mPathEffect[2] = new DashPathEffect(new float[]{10f, 5f, 20f, 15f},10);
mPathEffect[3] = new PathDashPathEffect(new Path(), 10, 10f, PathDashPathEffect.Style.ROTATE);
mPathEffect[4] = new SumPathEffect(mPathEffect[1], mPathEffect[2]);
mPathEffect[5] = new DiscretePathEffect(5f, 10f);
mPathEffect[6] = new ComposePathEffect(mPathEffect[3], mPathEffect[5]);
}
/**
* 绘制路径
*
* @param canvas
*/
@Override
protected void onDraw(Canvas canvas) {
super.onDraw(canvas);
for (int i = 0; i < mPathEffect.length; i++) {
pathPaint.setPathEffect(mPathEffect[i]);
canvas.drawPath(mPath,pathPaint);
canvas.drawText(pathEffectName[i], 0, 130, textPaint);//每个画布的最上面,就是y轴的0点
// 每绘制一条将画布向下平移180个像素
canvas.translate(0, 180);//控件的高度要足够大才能平移
}
invalidate();//绘制刷新
}
/**
* 测量
*
* @param widthMeasureSpec
* @param heightMeasureSpec
*/
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
super.onMeasure(widthMeasureSpec, heightMeasureSpec);
int wSpecMode = MeasureSpec.getMode(widthMeasureSpec);
int wSpecSize = MeasureSpec.getSize(widthMeasureSpec);
int hSpecMode = MeasureSpec.getMode(heightMeasureSpec);
int hSpecSize = MeasureSpec.getSize(heightMeasureSpec);
if (wSpecMode == MeasureSpec.AT_MOST && hSpecMode == MeasureSpec.AT_MOST) {
setMeasuredDimension(300, 300);
} else if (wSpecMode == MeasureSpec.AT_MOST) {
setMeasuredDimension(300, hSpecSize);
} else if (hSpecMode == MeasureSpec.AT_MOST) {
setMeasuredDimension(wSpecSize, 300);
}
}
}
这里借鉴了爱哥的思路,把7种效果都画了出来,还加上了名字。
需要注意的是,绘制路径时,pathPaint.setStyle(Paint.Style.STROKE)
画笔的风格要空心,否则,后果画出的不是线,而是一个不规则的区域。
这7种路径效果,暂时还不能区分,先暂时知道有这么7种效果,等到实现具体需求了再深入了解
9月8号补充
-
CornerPathEffect
拐角处变圆滑 -
DashPathEffect
可以用来绘制虚线,用一个数组来设置各个点之间的间隔,phase
控制绘制时数组的偏移量 -
PathDashPathEffect
和DashPathEffect
类似 ,可以设置显示的点的图形,例如圆形的点 -
DisCreatePathEffect
线段上会有许多杂点 -
ComposePathEffect
组合两个PathEffect
,将两个组合成一个效果
从Android群英传摘抄
3.6breakText(CharSequence text, int start, int end,boolean measureForwards,float maxWidth, float[] measuredWidth)
Measure the text, stopping early if the measured width exceeds maxWidth.
Return the number of chars that were measured, and if measuredWidth is
not null, return in it the actual width measured.
@param text The text to measure. Cannot be null.
@param start The offset into text to begin measuring at
@param end The end of the text slice to measure.
@param measureForwards If true, measure forwards, starting at start.Otherwise, measure backwards, starting with end.
@param maxWidth The maximum width to accumulate.
@param measuredWidth Optional. If not null, returns the actual width measured.
@return The number of chars that were measured. Will always be <= abs(end - start).
这个方法,暂时没有测试出啥效果。
先引用爱哥的描述:
这个方法让我们设置一个最大宽度在不超过这个宽度的范围内返回实际测量值否则停止测量,参数很多但是都很好理解,text表示我们的字符串,start表示从第几个字符串开始测量,end表示从测量到第几个字符串为止,measureForwards表示向前还是向后测量,maxWidth表示一个给定的最大宽度在这个宽度内能测量出几个字符,measuredWidth为一个可选项,可以为空,不为空时返回真实的测量值。同样的方法还有breakText (String text, boolean measureForwards, float maxWidth, float[] measuredWidth)和breakText (char[] text, int index, int count, float maxWidth, float[] measuredWidth)。这些方法在一些结合文本处理的应用里比较常用,比如文本阅读器的翻页效果,我们需要在翻页的时候动态折断或生成一行字符串,这就派上用场了~~~
3.7 其余
剩下的方法,试一下就晓得效果了
-
setTextScaleX(float f)
设置缩放,0f到1f为缩小,大于1f为放大 -
setUnderlineText(booelan b)
设置下划线 -
setStrikeThruText (boolean strikeThruText)
设置文本删除线 -
setTextSize(float f)
设置文字字体大小 -
getFontSpacing()
得到行间距 -
descent()
得到descent
的值 -
ascent()
得到asccent
的值 -
getLetterSpacing()
字母间距
关于字体的常用的方法差不多就这些了。漏掉的,用到了再补充。
4.最后
重点在于FontMetrics
的学习。先用画图板画出来,个人感觉比较有助于记忆。然后,尝试用代码在程序中把各条线画出来。
下篇学习Paint
类中关于画图像的属性方法。