上一篇着重就onDraw中的canvas方法进行了一系列绘制,相信对canvas中几个常用的方法应该都有所了解了。今天,就实例做一个自定义的控件,拿最简单的TextView开始,做一个带有背景色的TextView。直接上代码:
1,自定义属性:
<declare-styleable name="MyTextView2"> <attr name="style" format="integer|reference"/> <attr name="colors" format="color|reference"/> <attr name="textSize" format="dimension|reference"/> <attr name="text" format="string|reference"/> </declare-styleable>
这一部分内容,上一篇介绍过,这里不再多说。
2,XML文件:
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android" android:layout_width="match_parent" android:layout_height="match_parent" xmlns:app="http://schemas.android.com/apk/res-auto"> <com.example.administrator.myview1.MyTextView2 android:layout_width="wrap_content" android:layout_height="wrap_content" android:background="#ffff00" app:colors="#ff00" app:textSize="36sp" app:text="Hello World !"/> </RelativeLayout>
很简单,只谢了一个自定义控件而已。
3,接下来,看一下我们在MyTextView2中都做了那些工作:
public class MyTextView2 extends View { private int mTextSize; private int mColor; private String mText; private int mType; private Paint mPaint; private Rect mBound; private float mTextHeight; private float mTextWidth; 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); TypedArray mTypedArray = context.obtainStyledAttributes(attrs,R.styleable.MyTextView2); mType = mTypedArray.getInt(R.styleable.MyTextView2_style,0); mColor = mTypedArray.getColor(R.styleable.MyTextView2_colors, Color.RED); mTextSize = mTypedArray.getDimensionPixelSize(R.styleable.MyTextView2_textSize,20); mText = mTypedArray.getString(R.styleable.MyTextView2_text); mTypedArray.recycle(); initView(); } /** 初始化 */ private void initView(){ mPaint = new Paint(); mPaint.setAntiAlias(true); mPaint.setStyle(Paint.Style.FILL);//填充类型 mPaint.setTextSize(mTextSize); mPaint.setColor(mColor); mBound = new Rect(); mPaint.getTextBounds(mText,0,mText.length(), mBound); mTextHeight = mBound.height();//测量出文字的高度 mTextWidth = mBound.width();//测量出文字的宽度 } @Override protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { super.onMeasure(widthMeasureSpec, heightMeasureSpec); } @Override protected void onDraw(Canvas canvas) { super.onDraw(canvas); canvas.drawText(mText,getWidth()/2 - mBound.width() / 2,getHeight()/2 + mBound.height()/2,mPaint); } } }
先是构造方法中,调用了自定义属性,用于声明,TextView的大小,颜色和文本内容。接着,就是一个初始化的方法,对画笔进行了基础的设置。然后,重写了View的onMeasure方法和onDraw()方法。onMeasure()方法,美俄进行任何操作,先不管他,直接看onDraw()方法,里面就一句代码,很简单的绘制一个文本内容。其中getWidth和getHeight是控件的宽高(和getMeasureWidth和getMeasureHeight结果一样)。结合xml内容,绘制出来的应该是背景色是黄色,内容为Hello World !这样的一个TextView,运行一下,可以看到运行结果如图一:
图一: 图二:
很明显这不是我们想要的结果。在介绍View的生命周期的时候,里面对onMeasure()方法进行的说明,主要就是对控件的尺寸进行测量。而当我们在布局文件中把宽高都设置成wrap_content时,系统帮我们测量的宽高都是match_parent。只有当我们设置了明确的宽高之后测量的才是我们设置的宽高,如果我们把布局文件中的宽高分别设置为 200dp和100dp则结果就是图二。说到这,我们有必要再说一下MeasureSpec的specMode,一共三种类型(也可以参考:自定义View系列(一)):
EXACTLY:一般是设置了明确的值或者是MATCH_PARENT
AT_MOST:表示子布局限制在一个最大值内,一般为WARP_CONTENT
UNSPECIFIED:表示子布局想要多大就多大,很少使用
知道了这些,我们就可以重写onMeasure()方法了,下面是我们重写的onMeasure():
@Override protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { super.onMeasure(widthMeasureSpec, heightMeasureSpec); int widthMode = MeasureSpec.getMode(widthMeasureSpec); int widthSize = MeasureSpec.getSize(widthMeasureSpec); int heightMode = MeasureSpec.getMode(heightMeasureSpec); int heightSize = MeasureSpec.getSize(widthMeasureSpec); int width; int height; if(widthMode == MeasureSpec.EXACTLY){ width = widthSize; }else { width = (int) (getPaddingLeft() + mTextWidth + getPaddingRight()); } if(heightMode == MeasureSpec.EXACTLY){ height = heightSize; }else { height = (int) (getPaddingTop() + mTextHeight + getPaddingBottom()); } setMeasuredDimension(width,height); }
重新运行一下,得到如图三:
图三:
这样子,就达到了预期的效果。但是仔细看一下,就会发现,文本内容最后的叹号,好像只显示了一半,没能完全显示。不用想,肯定是我们的宽度设置的不够啊!上文中,有一行代码测量了文本内容的的宽度为:mTextWidth = mBound.width()。没错这样也确实可以得到文本内容的宽度,只不过不精确,测量出的是粗略值。改进方法为:mTextWidth = mPaint.measureText(mText),这样就得到了文本的精确宽度,再次运行可以看到如图四:
图四:
这样宽度就没问题了!那高度呢,肯定也是粗略的,这样子还不容易看,我们把文本内容加上中文,并且在onDraw()里面添加两行代码:
mPaint.setColor(Color.BLUE); canvas.drawLine(0,getHeight()/2,getWidth(),getHeight()/2,mPaint);
画出控件的中线,运行,看到效果图五:
图五:
看到了吧,高度明显偏小了。高度怎么改进呢,这个有点麻烦,先看一个图:
出自网络
看一下这个图,里面有五个参数,top,ascent,basline,descent,bottom:
Baseline是基线,在Android中,文字的绘制都是从Baseline处开始的,Baseline往上至字符“最高处”的距离我们称之为ascent(上坡度),Baseline往下至字符“最低处”的距离我们称之为descent(下坡度);leading(行间距)则表示上一行字符的descent到该行字符的ascent之间的距离;top和bottom可以理解为文本绘制时,留出的内边距。之所以留出内边距,是因为因为TextView在绘制文本的时候考虑到了类似读音符号,下图中的A上面的符号就是一个拉丁文的类似读音符号的东西:
如上图,Baseline是基线,Baseline以上是负值,以下是正值,因此 ascent,top是负值, descent和bottom是正值。
至此,我们算是了解清楚了,具体怎么修改呢?这里要用到FontMetrics 字体度量,它是Paint的一个今天内部类。这里不做介绍,只解释其用法。原来的高度为:mTextHeight = mBound.height();
修改后为:
Paint.FontMetrics fontMetrics = mPaint.getFontMetrics(); mTextHeight = Math.abs((fontMetrics.bottom - fontMetrics.top));
运行程序,可以看到:
发现文本内容没有居中,我们想让文本居中,然后看效果。
我们知道,中心线到底部的距离为文本高度的一半,也就是(descent - ascent)/2,这个由上面的图可以得到,这是文本的高度;中心线与基线的间距为:(descent - ascent)/2 - descent ;这样只需要在绘制的时候,将y值向上提高中心线与基线的距离就行了,即:
canvas.drawText(mText,getWidth()/2 - mTextWidth / 2, getHeight()/2 - (mFontMetrics.descent - (-mFontMetrics.ascent + mFontMetrics.descent)/2),mPaint);
运行一下,可以看到:
有图可以看出,文本的高度算了出来,并且居中了。由此可以得到文本垂直居中的计算方式方式
Paint.FontMetrics mFontMetrics = mPaint.getFontMetrics();
y: getHeight/2 - mFontMetrics.descent + (mFontMetrics.descent - mFontMetrics.ascent)/ 2;
好了,最终终于达到了想要的结果!今天就说到这吧,下一篇将会说一下,自定义布局。
参考:http://blog.youkuaiyun.com/u014702653/article/details/51985821
最后,奉上最终的代码:
public class MyTextView2 extends View { private int mTextSize; private int mColor; private String mText; private int mType; private Paint mPaint; private Rect mBound; private float mTextHeight; private float mTextWidth; private Paint.FontMetrics mFontMetrics; 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); TypedArray mTypedArray = context.obtainStyledAttributes(attrs,R.styleable.MyTextView2); mType = mTypedArray.getInt(R.styleable.MyTextView2_style,0); mColor = mTypedArray.getColor(R.styleable.MyTextView2_colors, Color.RED); mTextSize = mTypedArray.getDimensionPixelSize(R.styleable.MyTextView2_textSize,20); mText = mTypedArray.getString(R.styleable.MyTextView2_text); mTypedArray.recycle(); initView(); } /** 初始化 */ private void initView(){ mPaint = new Paint(); mPaint.setAntiAlias(true); mPaint.setStyle(Paint.Style.FILL);//填充类型 mPaint.setTextSize(mTextSize); mPaint.setColor(mColor); mBound = new Rect(); mPaint.getTextBounds(mText,0,mText.length(), mBound); mFontMetrics = mPaint.getFontMetrics(); mTextHeight = Math.abs((mFontMetrics.bottom - mFontMetrics.top));//测量出文字的高度 mTextWidth = mPaint.measureText(mText);//测量出文字的宽度 } @Override protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { super.onMeasure(widthMeasureSpec, heightMeasureSpec); int widthMode = MeasureSpec.getMode(widthMeasureSpec); int widthSize = MeasureSpec.getSize(widthMeasureSpec); int heightMode = MeasureSpec.getMode(heightMeasureSpec); int heightSize = MeasureSpec.getSize(widthMeasureSpec); int width; int height; if(widthMode == MeasureSpec.EXACTLY){ width = widthSize; }else { width = (int) (getPaddingLeft() + mTextWidth + getPaddingRight()); } if(heightMode == MeasureSpec.EXACTLY){ height = heightSize; }else { height = (int) (getPaddingTop() + mTextHeight + getPaddingBottom()); } setMeasuredDimension(width,height); } @Override protected void onDraw(Canvas canvas) { super.onDraw(canvas); canvas.drawText(mText,getWidth()/2 - mTextWidth / 2, getHeight()/2 - (mFontMetrics.descent - (-mFontMetrics.ascent + mFontMetrics.descent)/2),mPaint); mPaint.setColor(Color.BLUE); canvas.drawLine(0,getHeight()/2,getWidth(),getHeight()/2,mPaint); } }