接着上一篇《Android 自定义View基础(一)》,这一篇我们真正的来实现一个文字自定义控件:
自定义控件的步骤:
自定义属性
在自定义View的构造函数获取属性
onMesure()
onDraw()
在res/values/下创建一个attrs.xml文件,然后在创建需要的属性
代码如下:
<resources>
<attr name="titleText" format="string">attr>
<attr name="titleTextColor" format="color">attr>
<attr name="titleTextSize" format="dimension">attr>
<declare-styleable name="CustomView01" >
<attr name="titleText"/>
<attr name="titleTextColor"/>
<attr name="titleTextSize"/>
declare-styleable>
resources>
上面的属性中,我们创建了文字,文字颜色,文字大小三个属性
<declare-styleable name="CustomView01" >
名字也可以是你随便起,但是建议一般与你创建的自定义类名相同。方便查看和管理。
先创建一个CustomView01继承自View
public class CustomView01 extends View {
public CustomView01(Context context) {
super(context);
}
public CustomView01(Context context, AttributeSet attrs) {
super(context, attrs);
}
}
上面的两个构造方法中,一个是我们动态创建控件时使用,一个是从xml布局文件中加载时调用。
在activity_main.xml中加入我们创建的属性和控件
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
xmlns:custom="http://schemas.android.com/apk/res-auto"
android:id="@+id/main_layout"
android:layout_width="match_parent"
android:layout_height="match_parent"
tools:context="com.aofei.myview.MainActivity">
<com.aofei.myview.CustomView01
android:layout_width="200dp"
android:layout_height="100dp"
custom:titleText="这是测试文字"
custom:titleTextColor="#ff0000"
custom:titleTextSize="40sp"
/>
RelativeLayout>
此行代码就是将我们创建的自定义属性导入进当前布局,该代码不必记忆,只要输入app然后就会出现一个appNs的提示,确认后就是
xmlns:app="http://schemas.android.com/apk/res-auto"
然后可以更改名字,为custom,我们的属性就是以custom 开头,
xmlns:custom="http://schemas.android.com/apk/res-auto"
到这里我们运行,后发现,界面上并没有我们的创建的控件,为什么呢?这是因为我们的自定Custom中,并没有饮用我们的属性和调用onDraw()进行绘画。
然后我们实现我们调用属性和绘画
public class CustomView01 extends View {
private String mTitleText;//文本
private int mTitleTextColor;//文本的颜色
private int mTitleTextSize;//文本的大小
private Rect mBound;//绘制时控制文本绘制的范围
private Paint mPaint; //绘制文本时的画笔
public CustomView01(Context context) {
this(context, null);
}
public CustomView01(Context context, AttributeSet attrs) {
this(context, attrs, 0);
}
/**
* 获取我们的自定义样式
*
* @param context
* @param attrs
* @param defStyleAttr
*/
public CustomView01(Context context, AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
//获取我们所定义的自定义样式属性
TypedArray a = context.getTheme().obtainStyledAttributes(attrs, R.styleable.CustomView01, defStyleAttr, 0);
int n = a.getIndexCount();//获取多少个属性
//遍历所有属性
for (int i = 0; i < n; i++) {
int attr = a.getIndex(i);
//根据从xml对应的值对属性赋值
switch (attr) {
case R.styleable.CustomView01_titleText:
mTitleText = a.getString(attr);
break;
case R.styleable.CustomView01_titleTextColor:
mTitleTextColor = a.getColor(attr, Color.BLACK);//默认颜色为黑色
break;
case R.styleable.CustomView01_titleTextSize:
//給文字尺寸赋值,默认设置为16sp,TypeValue也可以把sp转化为px
mTitleTextSize = a.getDimensionPixelSize(attr,
(int) TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_SP, 16,
getResources().getDisplayMetrics()));
break;
}
}
a.recycle();//释放资源,这个一定不要忘记!
//获取绘制文本的宽和高
mPaint=new Paint();
mPaint.setTextSize(mTitleTextSize);
mBound= new Rect();
mPaint.getTextBounds(mTitleText,0,mTitleText.length(),mBound);
}
}
我们重写了3个构造方法,默认的布局文件调用的是两个参数的构造方法,所以记得让所有的构造调用我们的三个参数的构造,我们在三个参数的构造中获得自定义属性。
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
super.onMeasure(widthMeasureSpec, heightMeasureSpec);
}
@Override
protected void onDraw(Canvas canvas) {
// super.onDraw(canvas); //我们要重写,不必调用继承的View的方法
mPaint.setColor(Color.YELLOW);
//背景矩形
canvas.drawRect(0,0,getMeasuredWidth(),getMeasuredHeight(),mPaint);
mPaint.setColor(mTitleTextColor);
//画文字
canvas.drawText(mTitleText,getWidth()/2-mBound.width()/2,getHeight()/2+mBound.height()/2,mPaint);
}
运行后的结果如下:
到这里,我们就实现一个类似于TextView的控件
我们在onDraw()中打印一下几个测量的值
@Override
protected void onDraw(Canvas canvas) {
// super.onDraw(canvas); //我们要重写,不必调用继承的View的方法
mPaint.setColor(Color.YELLOW);
canvas.drawRect(0,0,getMeasuredWidth(),getMeasuredHeight(),mPaint);
mPaint.setColor(mTitleTextColor);
mPaint.setAntiAlias(true);//抗锯齿
canvas.drawText(mTitleText,getWidth()/2-mBound.width()/2,getHeight()/2+mBound.height()/2,mPaint);
Log.e(TAG,"getMeasuredWidth()="+getMeasuredWidth()+", getMeasuredHeight()="+getMeasuredHeight());
Log.e(TAG,"getWidth()="+getWidth()+" ,getHeight="+getHeight());
Log.e(TAG,"mBound.width()="+mBound.width()+" ,mBound.height()="+mBound.height());
}
03-09 14:37:47.172 22448-22448/com.aofei.myview E/CustomView01: getMeasuredWidth()=600, getMeasuredHeight()=300
03-09 14:37:47.172 22448-22448/com.aofei.myview E/CustomView01: getWidth()=600 ,getHeight=300
03-09 14:37:47.172 22448-22448/com.aofei.myview E/CustomView01: mBound.width()=354 ,mBound.height()=58
这个就是我们控件的大小。
如果我们将我们的布局文件中的属性更改为wrap_content,
<com.aofei.myview.CustomView01
android:layout_width="wrap_content"
android:layout_height="wrap_content"
custom:titleText="这是测试文字"
custom:titleTextColor="#ff0000"
custom:titleTextSize="20sp"
/>
运行后的结果如下:
03-09 14:44:32.548 22448-22448/com.aofei.myview E/CustomView01: getMeasuredWidth()=1080, getMeasuredHeight()=1536
03-09 14:44:32.548 22448-22448/com.aofei.myview E/CustomView01: getWidth()=1080 ,getHeight=1536
03-09 14:44:32.548 22448-22448/com.aofei.myview E/CustomView01: mBound.width()=354 ,mBound.height()=58
此时的宽高发生了变化,这是系统帮我们测量的尺寸,都是match_parent,是手机的屏幕尺寸了,getWidth()=1080 ,getHeight=1536
getMeasuredWidth()=1080, getMeasuredHeight()=1536,显然,这不是我们想要的效果。所以当我们设置了wrap_content时,我们需要自己进行测量,即重写onMeasure.
EXACTLY:一般是设置了明确的值或者是MATCH_PARENT
AT_MOST:表示子布局限制在一个最大值内,一般为wrap_content
UNSPECIFIED:表示子布局想要多大就多大,很少使用
下面是我们重写onMeasure代码:
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
// super.onMeasure(widthMeasureSpec, heightMeasureSpec);
//重写onMeasure
int widthMode=MeasureSpec.getMode(widthMeasureSpec);
int widthSize=MeasureSpec.getSize(widthMeasureSpec);
int heightMode=MeasureSpec.getMode(heightMeasureSpec);
int heightSize=MeasureSpec.getSize(heightMeasureSpec);
int width;
int height;
if (widthMode==MeasureSpec.EXACTLY){
width=widthSize;
}else {
//如果不是给出精确的值,我们通过计算文字的长度和距离控件左右的值来确定,我们画多大
mPaint.setTextSize(mTitleTextSize);
mPaint.getTextBounds(mTitleText,0,mTitleText.length(),mBound);
float textWidth=mBound.width();
int desired = (int) (getPaddingLeft()+textWidth+getPaddingRight());
width=desired;
}
if (heightMode==MeasureSpec.EXACTLY){
height=heightSize;
}else {
mPaint.setTextSize(mTitleTextSize);
mPaint.getTextBounds(mTitleText,0,mTitleText.length(),mBound);
float textHeight=mBound.height();
int desired = (int) (getPaddingTop()+textHeight+getPaddingBottom());
height=desired;
}
setMeasuredDimension(width,height);//设置计算后的宽高
}
我们修改一下布局文件,如下
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:custom="http://schemas.android.com/apk/res-auto"
android:id="@+id/main_layout"
android:layout_width="match_parent"
android:layout_height="match_parent"
tools:context="com.aofei.myview.MainActivity">
<com.aofei.myview.CustomView01
android:layout_width="wrap_content"
android:layout_height="wrap_content"
custom:titleText="3388"
custom:titleTextColor="#ff0000"
custom:titleTextSize="20sp"
android:padding="10dp"
/>
RelativeLayout>
运行后的结果是:
这样的效果,就是我们想要的,我们可以对高度,宽度随便设置,基本可以满足我们的需求。
接下来我们稍微扩展一下,給这个View添加一个点击事件。在onDraw()最后面添加一个点击事件。
//再画完后我们給这个控件添加一个点击事件
this.setOnClickListener(new OnClickListener() {
@Override
public void onClick(View v) {
mTitleText=randomText();
postInvalidate();//重写绘制
}
});
/**
* 获取一个随机的四个数字的字符串
* @return
*/
private String randomText(){
Random random = new Random();
Set set=new HashSet<>();
while (set.size()<4){
int randomInt=random.nextInt(10);
set.add(randomInt);
}
StringBuffer sb=new StringBuffer();
for (Integer i:set) {
sb.append(""+i);
}
return sb.toString();
}
运行效果如下:
,这是不是很像我们登陆一些网站的时候验证码的更换。写到这里,就简单实现了一个类似于TextView的自定义控件,不管看博客还是视频,都需要自己动手,永远不要认为自己看懂了,不用写了。