上篇导航:自定义View三大流程理论篇
TextView是我们常用的View之一,下面我们来自定义一个TextView,实现基本的TextView功能。以下不涉及基本的理论概念,概念篇参见上一篇文章。
这里我们初始化了一些要用到的数据,其中有字体的颜色和大小以及文字内容的画笔。
public class MyTextView extends View {
private float textSize;//文字大小
private String text = "Hello!";//文字内容
private int textColor;//字体颜色
private Paint textPaint;//字体画笔
private int gravity = Gravity.RIGHT;//文字显示位置
public MyTextView(Context context) {
super(context);
init();
}
public MyTextView(Context context, AttributeSet attrs) {
super(context, attrs);
init();
}
private void init() {
this.textSize = TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_SP, 15, getContext().getResources().getDisplayMetrics());//初始化文字大小
this.textColor = Color.BLACK;
this.textPaint = new Paint();
this.textPaint.setColor(this.textColor);
}
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
super.onMeasure(widthMeasureSpec, heightMeasureSpec);
}
@Override
protected void onLayout(boolean changed, int left, int top, int right, int bottom) {
super.onLayout(changed, left, top, right, bottom);
}
@Override
protected void onDraw(Canvas canvas) {
super.onDraw(canvas);
}
}
这里我们一步步来看下面的代码:
首先是通过MeasureSpec获取特定的尺寸信息。
第二步就是要计算出内容所占的大小,也就是文本所占的大小,因为我们的模式可能是AT_MOST和UNSPECIFIED。
第三步是要根据模式来确定当前View的尺寸,我们在上一篇的内容了解到了AT_MOST对应的是wrap_content
所以我们就将尺寸改为内容的尺寸,UNSPECIFIED是在当前View作为ScrollView子控件时的模式,我们也要设置当前View为内容所占的尺寸。
第四部也是最后一步,就是设置当前View的尺寸。
/**
* 获取Text占用的大小
* @return
*/
private Rect measureTextSize() {
Paint paint = new Paint();
Rect textSize = new Rect();
paint.setTextSize(this.textSize);//注意别忘了设置文字的大小
paint.getTextBounds(this.text, 0, this.text.length(), textSize);
return textSize;
}
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
//【1】获取尺寸和模式
int wMode = MeasureSpec.getMode(widthMeasureSpec);
int w = MeasureSpec.getSize(widthMeasureSpec);
int hMode = MeasureSpec.getMode(heightMeasureSpec);
int h = MeasureSpec.getSize(heightMeasureSpec);
//【2】计算内容尺寸(这里的内容指的是文字的大小)
int contentWidth = 0;
int contentHeight = 0;
if (text != null && text.length() > 0) {
//测量并计算Text的大小
Rect textSize = measureTextSize();
contentWidth = textSize.width();
contentHeight = textSize.height();
}
//【3】根据模式设置最终尺寸(这里要处理AT_MOST和UNSPECIFIED)
if (wMode == MeasureSpec.AT_MOST || wMode == MeasureSpec.UNSPECIFIED) {
w = contentWidth;
}
if (hMode == MeasureSpec.AT_MOST || hMode == MeasureSpec.UNSPECIFIED) {
h = contentHeight;
}
//【4】设置最终的尺寸
setMeasuredDimension(w, h);
}
这里我们可以不重写onLayout函数,因为我们并没有子view需要来布局。
@Override
protected void onLayout(boolean changed, int left, int top, int right, int bottom) {
super.onLayout(changed, left, top, right, bottom);
}
我们这里使用drawText将文本绘制到View中,实现了三种Gravity,具体计算这里不再详述。
@Override
protected void onDraw(Canvas canvas) {
super.onDraw(canvas);
if (this.text == null || this.text.isEmpty()) {
return;
}
//计算坐标
Rect rect = measureTextSize();
int textWidth = rect.width();
int textHeight = rect.height();
int x = 0;
int y = 0;
if (gravity == Gravity.CENTER) {
x = getMeasuredWidth() / 2 - textWidth / 2;
y = (int) (getMeasuredHeight() / 2F + textHeight / 2F);
} else if (gravity == Gravity.LEFT) {
y = textHeight;
} else if (gravity == Gravity.RIGHT) {
x = getMeasuredWidth() - textWidth;
y = textHeight;
}
//绘制文字
Paint paint = new Paint();
paint.setTextSize(this.textSize);
int descent = paint.getFontMetricsInt().descent;
canvas.drawText(this.text, x, y - descent, this.textPaint);
}
在上边我们的属性是定死的,我们没办法直接通过xml来设置,下面我们就来说一下如何通过xml来设置自定义的属性。
Step1:
在values目录下新建一个Values Resource File
文件,名字可以随便起。右击values–>New–>Values Resource File。
Step2:
在刚才建立好的xml文件中写需要的属性。建立好xml后默认是有一个根节点resources
,我们在根节点下建立一个declare-styleable
并添加一个属性为name,这个name就是我们View的名字。然后再这个节点下添加我们的attr
节点,也就是属性节点,需要名称和类型,常用类型有dimension、int、string、color、enum,使用enum需要在其节点下声明枚举值。
<resources>
<declare-styleable name="MyTextView">
<attr name="textSize" format="dimension">attr>
<attr name="text" format="string">attr>
<attr name="gravity" format="enum">
<enum name="CENTER" value="1">enum>
<enum name="LEFT" value="2">enum>
<enum name="RIGHT" value="3">enum>
attr>
<attr name="textColor" format="color">attr>
declare-styleable>
resources>
Step3:
第三部就是在构造函数中获取定义的属性值了。
这里我们将原来init函数中的添加默认值的代码给注释掉了(因为我们要从xml属性中获取),还添加了三个内部的方法,从下看第一个就是将sp单位转换px单位的方法,第二个是根据我们定义的枚举值转换成Gravity对象对应的值,getAttrs就是获取我们在xml中定义的属性值了。这个getAttrs函数我们在带有AttributeSet的构造函数中进行调用。这个AttributeSet里边就包含了所有我们在xml中设置的属性。
public MyTextView(Context context, AttributeSet attrs) {
super(context, attrs);
getAttrs(attrs);
init();
}
private void getAttrs(AttributeSet attributeSet) {
TypedArray typedArray = getContext().obtainStyledAttributes(attributeSet, R.styleable.MyTextView);//第二个参数是xml中定义的名称
//以下使用的R.styleable.MyTextView_xxxx是自动生成的,是declare-styleable的name+下划线+attr的name
this.textSize = typedArray.getDimensionPixelSize(R.styleable.MyTextView_textSize, sp2px(12));//解析dimension类型
this.text = typedArray.getString(R.styleable.MyTextView_text);//解析String类型
this.textColor = typedArray.getColor(R.styleable.MyTextView_textColor, Color.BLACK);//解析Color类型
this.gravity = parseGravity(typedArray.getInt(R.styleable.MyTextView_gravity, 1));//解析枚举类型
typedArray.recycle();
}
/**
* 将枚举值解析为Gravity
*
* @param val
* @return
*/
private int parseGravity(int val) {
if (val == 1) {
return Gravity.CENTER;
} else if (val == 2) {
return Gravity.LEFT;
}
return Gravity.RIGHT;
}
/**
* 将sp单位转换为像素单位
*
* @param spValue
* @return
*/
private int sp2px(int spValue) {
return (int) TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_SP, spValue, getContext().getResources().getDisplayMetrics());
}
private void init() {
// this.textSize = TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_SP, 12, getContext().getResources().getDisplayMetrics());//初始化文字大小
// this.textColor = Color.BLACK;
this.textPaint = new Paint();
this.textPaint.setColor(this.textColor);
this.textPaint.setTextSize(this.textSize);
}
Step4:
在xml中使用。
<com.example.customview.MyTextView
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:background="#FF00"
app:text="blog.lost520.cn"
app:textColor="#FFFF"
app:textSize="30sp" />
效果图:
public class MyTextView extends View {
private float textSize;//文字大小
private String text = "Hello";//文字内容
private int textColor;//字体颜色
private Paint textPaint;//字体画笔
private int gravity = Gravity.CENTER;//文字显示位置
public MyTextView(Context context) {
super(context);
init();
}
public MyTextView(Context context, AttributeSet attrs) {
super(context, attrs);
getAttrs(attrs);
init();
}
private void getAttrs(AttributeSet attributeSet) {
TypedArray typedArray = getContext().obtainStyledAttributes(attributeSet, R.styleable.MyTextView);//第二个参数是xml中定义的名称
//以下使用的R.styleable.MyTextView_xxxx是自动生成的,是declare-styleable的name+下划线+attr的name
this.textSize = typedArray.getDimensionPixelSize(R.styleable.MyTextView_textSize, sp2px(12));//解析dimension类型
this.text = typedArray.getString(R.styleable.MyTextView_text);//解析String类型
this.textColor = typedArray.getColor(R.styleable.MyTextView_textColor, Color.BLACK);//解析Color类型
this.gravity = parseGravity(typedArray.getInt(R.styleable.MyTextView_gravity, 1));//解析枚举类型
typedArray.recycle();
}
/**
* 将枚举值解析为Gravity
*
* @param val
* @return
*/
private int parseGravity(int val) {
if (val == 1) {
return Gravity.CENTER;
} else if (val == 2) {
return Gravity.LEFT;
}
return Gravity.RIGHT;
}
/**
* 将sp单位转换为像素单位
*
* @param spValue
* @return
*/
private int sp2px(int spValue) {
return (int) TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_SP, spValue, getContext().getResources().getDisplayMetrics());
}
private void init() {
// this.textSize = TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_SP, 12, getContext().getResources().getDisplayMetrics());//初始化文字大小
// this.textColor = Color.BLACK;
this.textPaint = new Paint();
this.textPaint.setColor(this.textColor);
this.textPaint.setTextSize(this.textSize);
}
/**
* 获取Text占用的大小
*
* @return
*/
private Rect measureTextSize() {
Paint paint = new Paint();
Rect textSize = new Rect();
paint.setTextSize(this.textSize);
paint.getTextBounds(this.text, 0, this.text.length(), textSize);
return textSize;
}
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
//【1】获取尺寸和模式
int wMode = MeasureSpec.getMode(widthMeasureSpec);
int w = MeasureSpec.getSize(widthMeasureSpec);
int hMode = MeasureSpec.getMode(heightMeasureSpec);
int h = MeasureSpec.getSize(heightMeasureSpec);
//【2】计算内容尺寸(这里的内容指的是文字的大小)
int contentWidth = 0;
int contentHeight = 0;
if (text != null && text.length() > 0) {
//测量并计算Text的大小
Rect textSize = measureTextSize();
contentWidth = textSize.width();
contentHeight = textSize.height();
}
//【3】根据模式设置最终尺寸(这里要处理AT_MOST和UNSPECIFIED)
if (wMode == MeasureSpec.AT_MOST || wMode == MeasureSpec.UNSPECIFIED) {
w = contentWidth;
}
if (hMode == MeasureSpec.AT_MOST || hMode == MeasureSpec.UNSPECIFIED) {
h = contentHeight;
}
//【4】设置最终的尺寸
setMeasuredDimension(w, h);
}
@Override
protected void onLayout(boolean changed, int left, int top, int right, int bottom) {
super.onLayout(changed, left, top, right, bottom);
}
@Override
protected void onDraw(Canvas canvas) {
super.onDraw(canvas);
if (this.text == null || this.text.isEmpty()) {
return;
}
//计算坐标
Rect rect = measureTextSize();
int textWidth = rect.width();
int textHeight = rect.height();
int x = 0;
int y = 0;
if (gravity == Gravity.CENTER) {
x = getMeasuredWidth() / 2 - textWidth / 2;
y = (int) (getMeasuredHeight() / 2F + textHeight / 2F);
} else if (gravity == Gravity.LEFT) {
y = textHeight;
} else if (gravity == Gravity.RIGHT) {
x = getMeasuredWidth() - textWidth;
y = textHeight;
}
//绘制文字
Paint paint = new Paint();
paint.setTextSize(this.textSize);
int descent = paint.getFontMetricsInt().descent;
canvas.drawText(this.text, x, y - descent, this.textPaint);
}
}
以上的属性只能通过xml来定义,如果想要通过代码来设置,直接为变量添加get和set方法即可。
下一章导航:自定义View实现LinearLayout