LabelView是Android API DEMO中的一个例子,演示了如何写一个简单的自定义View。
先看一下效果
布局文件是这样的:
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res/com.example.labelview"
android:orientation="vertical"
android:layout_width="match_parent"
android:layout_height="wrap_content">
<com.example.labelview.LabelView
android:background="@android:color/holo_blue_light"
android:layout_width="match_parent"
android:layout_height="match_parent"
app:text="Red"/>
<com.example.labelview.LabelView
android:background="@android:color/holo_red_light"
android:layout_width="match_parent"
android:layout_height="wrap_content"
app:text="Blue"
app:textSize="20dp"/>
<com.example.labelview.LabelView
android:background="@android:color/holo_purple"
android:layout_width="match_parent"
android:layout_height="wrap_content"
app:text="Green"
app:textColor="#ffffffff" />
</LinearLayout>
xml元素名com.example.labelview.LabelView,对于自定义的控件,要写上它完整的包路径
LabelVIew的路径如图所示:
注意到标签中的app:text,app:textColor,app:textSize属性都是控件的自定义属性,app:前缀是labelview专属的,定义它的步骤如下:
1.在布局文件中声明命名空间
LinearLayout元素下的xmlns:app="http://schemas.android.com/apk/res/com.example.labelview" 属性,声明了app:这个自定义View属性,格式为:
xmlns:命名空间 /res/应用程序包名。
2,在attrs.xml中定义属性类型
<?xml version="1.0" encoding="utf-8"?>
<resources>
<declare-styleable name="LabelView">
<attr name="text" format="string" />
<attr name="textColor" format="color" />
<attr name="textSize" format="dimension" />
</declare-styleable>
</resources>
3.在布局文件xml中使用这个属性
app:text="Red"
app:textSize="20dp"
app:textColor="#ffffffff"
4.在代码中实现初始化。
LabelView的代码:
public class LabelView extends View {
private Paint mTextPaint;
private String mText;
private int mAscent;
/**
* Constructor. This version is only needed if you will be instantiating
* the object manually (not from a layout XML file).
* @param context
*/
public LabelView(Context context) {
super(context);
initLabelView();
}
/**
* Construct object, initializing with any attributes we understand from a
* layout file. These attributes are defined in
* SDK/assets/res/any/classes.xml.
*
* @see android.view.View#View(android.content.Context, android.util.AttributeSet)
*/
public LabelView(Context context, AttributeSet attrs) {
super(context, attrs);
initLabelView();
TypedArray a = context.obtainStyledAttributes(attrs,
R.styleable.LabelView);
CharSequence s = a.getString(R.styleable.LabelView_text);
if (s != null) {
setText(s.toString());
}
// Retrieve the color(s) to be used for this view and apply them.
// Note, if you only care about supporting a single color, that you
// can instead call a.getColor() and pass that to setTextColor().
setTextColor(a.getColor(R.styleable.LabelView_textColor, 0xFF000000));
int textSize = a.getDimensionPixelOffset(R.styleable.LabelView_textSize, 0);
if (textSize > 0) {
setTextSize(textSize);
}
a.recycle();
}
private final void initLabelView() {
mTextPaint = new Paint();
mTextPaint.setAntiAlias(true);
// Must manually scale the desired text size to match screen density
mTextPaint.setTextSize(16 * getResources().getDisplayMetrics().density);
mTextPaint.setColor(0xFF000000);
setPadding(20, 3, 3, 3);
}
/**
* Sets the text to display in this label
* @param text The text to display. This will be drawn as one line.
*/
public void setText(String text) {
mText = text;
requestLayout();
invalidate();
}
/**
* Sets the text size for this label
* @param size Font size
*/
public void setTextSize(int size) {
// This text size has been pre-scaled by the getDimensionPixelOffset method
mTextPaint.setTextSize(size);
requestLayout();
invalidate();
}
/**
* Sets the text color for this label.
* @param color ARGB value for the text
*/
public void setTextColor(int color) {
mTextPaint.setColor(color);
invalidate();
}
/**
* @see android.view.View#measure(int, int)
*/
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
Log.v("test", "onMeasure");
setMeasuredDimension(measureWidth(widthMeasureSpec),
measureHeight(heightMeasureSpec));
}
/**
* Determines the width of this view
* @param measureSpec A measureSpec packed into an int
* @return The width of the view, honoring constraints from measureSpec
*/
private int measureWidth(int measureSpec) {
int result = 0;
int specMode = MeasureSpec.getMode(measureSpec);
int specSize = MeasureSpec.getSize(measureSpec);
if (specMode == MeasureSpec.EXACTLY) {
// We were told how big to be
result = specSize;
} else {
// Measure the text
result = (int) mTextPaint.measureText(mText) + getPaddingLeft()
+ getPaddingRight();
if (specMode == MeasureSpec.AT_MOST) {
// Respect AT_MOST value if that was what is called for by measureSpec
result = Math.min(result, specSize);
}
}
return result;
}
/**
* Determines the height of this view
* @param measureSpec A measureSpec packed into an int
* @return The height of the view, honoring constraints from measureSpec
*/
private int measureHeight(int measureSpec) {
int result = 0;
int specMode = MeasureSpec.getMode(measureSpec);
int specSize = MeasureSpec.getSize(measureSpec);
mAscent = (int) mTextPaint.ascent();
if (specMode == MeasureSpec.EXACTLY) {
// We were told how big to be
result = specSize;
} else {
// Measure the text (beware: ascent is a negative number)
//-asent+desent = 字符的高度,再加上山下的padding等于这个view的总高度
result = (int) (-mAscent + mTextPaint.descent()) + getPaddingTop()
+ getPaddingBottom();
if (specMode == MeasureSpec.AT_MOST) {
// Respect AT_MOST value if that was what is called for by measureSpec
result = Math.min(result, specSize);
}
}
return result;
}
/**
* Render the text
*
* @see android.view.View#onDraw(android.graphics.Canvas)
*/
@Override
protected void onDraw(Canvas canvas) {
super.onDraw(canvas);
Log.v("test", "onDraw");
//y是指定这个字符baseline在屏幕上的位置
canvas.drawText(mText, getPaddingLeft(),
((int) (-mAscent + getPaddingTop())),
mTextPaint);
}
}
先看构造方法
public LabelView(Context context, AttributeSet attrs) {
super(context, attrs);
initLabelView();
TypedArray a = context.obtainStyledAttributes(attrs,
R.styleable.LabelView);
CharSequence s = a.getString(R.styleable.LabelView_text);
if (s != null) {
setText(s.toString());
}
// Retrieve the color(s) to be used for this view and apply them.
// Note, if you only care about supporting a single color, that you
// can instead call a.getColor() and pass that to setTextColor().
setTextColor(a.getColor(R.styleable.LabelView_textColor, 0xFF000000));
int textSize = a.getDimensionPixelOffset(R.styleable.LabelView_textSize, 0);
if (textSize > 0) {
setTextSize(textSize);
}
a.recycle();
}
intiLavbelVIew方法设置了Paint对象绘制时的一些初始值。
TypedArray a = context.obtainStyledAttributes(attrs,
R.styleable.LabelView);
获得了attrs.xml中定义的属性类型,然后用TypedArray对象中的方法获得具体的值,因为attr文件中,text是string类型的,所以用getString,textSize和textColor分别对应getColor和getDimensionPixelOffset。最后要调用a.recycle();回收TypedArray对象。
注意到 setTextSize方法:
public void setTextSize(int size) {
// This text size has been pre-scaled by the getDimensionPixelOffset method
mTextPaint.setTextSize(size);
requestLayout();
invalidate();
}
requestLayout的调用会触发View的onMeasure过程,invalidate的调用会触发View的onDraw过程。
View的绘制流程中,onMeasure用来测量控件的尺寸,onDraw用来绘制图形
其他2个设置text的方法,
public void setText(String text) {
mText = text;
requestLayout();
invalidate();
}
public void setTextColor(int color) {
mTextPaint.setColor(color);
invalidate();
}
可以看到setTextColor并没有调用requestLayout方法,因为设置字体的颜色并不需要变化字体的大小形状,所以不用测量,就不需要走onMeasure的流程,自然不需调用requestLayout方法。
LabelView重写了onMeasure方法
setMeasuredDimension(measureWidth(widthMeasureSpec),
measureHeight(heightMeasureSpec));
用来设置测量完毕的尺寸,宽度和高的测量是通过measureWidth和measureHeight进行的。
measureWidth:
private int measureWidth(int measureSpec) {
int result = 0;
int specMode = MeasureSpec.getMode(measureSpec);
int specSize = MeasureSpec.getSize(measureSpec);
if (specMode == MeasureSpec.EXACTLY) {
// We were told how big to be
result = specSize;
} else {
// Measure the text
result = (int) mTextPaint.measureText(mText) + getPaddingLeft()
+ getPaddingRight();
if (specMode == MeasureSpec.AT_MOST) {
// Respect AT_MOST value if that was what is called for by measureSpec
result = Math.min(result, specSize);
}
}
return result;
}
onMeasure传入的widthMeasureSpec和heightMeasureSpec不是一般的尺寸数值,而是将模式和尺寸组合在一起的数值,需要用静态方法getMode和getSize分别取出
MeasureSpec.getMode获取MeasureSpec的Mode,MeasureSpec的Mode有三种类型:
MeasureSpec.AT_MOST
MeasureSpec.EXACTLY
MeasureSpec.UNSPECIFIED
MeasureSpec.UNSPECIFIED
调试代码时,发现MeasureSpec.getSize会返回屏幕的真实宽度,mode和布局xml中宽度属性值的对应关系为:
xx dp:specMode == MeasureSpec.EXACTLY
wrap_content:specMode == MeasureSpec.AT_MOST
match_parent:specMode ==MeasureSpec.EXACTLY
所以,如果布局中是具体的dp的话,result返回dp换算后的实际px。而如果是match_parent的话,会返回MeasureSpec.getSize的值,即屏幕的实际宽度。如果是wrap_content的话,会先调用Paint的measureText方法算出text的具体宽度然后返回。
再来看measureHeight方法:
private int measureHeight(int measureSpec) {
int result = 0;
int specMode = MeasureSpec.getMode(measureSpec);
int specSize = MeasureSpec.getSize(measureSpec);
mAscent = (int) mTextPaint.ascent();
if (specMode == MeasureSpec.EXACTLY) {
// We were told how big to be
result = specSize;
} else {
// Measure the text (beware: ascent is a negative number)
//-asent+desent = 字符的高度,再加上山下的padding等于这个view的总高度
result = (int) (-mAscent + mTextPaint.descent()) + getPaddingTop()
+ getPaddingBottom();
if (specMode == MeasureSpec.AT_MOST) {
// Respect AT_MOST value if that was what is called for by measureSpec
result = Math.min(result, specSize);
}
}
return result;
}
大体上和measureWidth差不多。计算字体高度的方式很有意思,涉及到baseLine的问题,
以下图片来自网络,说明baseLine和各个属性的关系
ascent:是baseline之上至字符最高处的距离
descent:是baseline之下至字符最低处的距离
descent:是baseline之下至字符最低处的距离
所以用如下代码计算字体高度
result = (int) (-mAscent + mTextPaint.descent()) + getPaddingTop()
+ getPaddingBottom();
最后来看onDraw方法,它负责具体的绘制
protected void onDraw(Canvas canvas) {
super.onDraw(canvas);
canvas.drawText(mText, getPaddingLeft(),
((int) (-mAscent + getPaddingTop())),
mTextPaint);
}
使用画布对象canvas绘制字体,参数是:显示的字体内容,x坐标,y坐标,paint对象