您的位置:首页 > 移动开发 > Android开发

Android深入浅出自定义控件(一)

2016-05-20 15:57 477 查看

Android自定义主要有3种,自定义View、自定义ViewGroup、继承重写系统控件

本文主要讲解Android中如何自定义View

Android打造自定义控件,大体的思路主要有以下五点:

1.创建自定义属性,在res/values目录下创建attrs.xml文件,声明自定义控件的属性

2.创建自定义View类,继承于View类,重写View的三个构造方法

3.通过TypeArray获得各个自定义属性,并将Paint设置为这些属性的内容

4.重写onMeasure方法,设置好视图在界面上所显示的大小

5.重写onDraw方法,通过paint和canvas渲染出自定义控件

1.创建自定义属性,在res/values目录下创建attrs.xml文件,声明自定义控件的属性

<?xml version="1.0" encoding="utf-8"?>
<resources>

<declare-styleable name="myTextView">
<attr name="mytextContent" format="string" type="string"></attr>
<attr name="mytextColor" format="color" type="color"></attr>
<attr name="mytextSize" format="dimension" type="dimension"></attr>
</declare-styleable>

</resources>


声明自定义属性有两个作用,一方面可以让我们在布局文件中直接使用这些属性,另一方面,在xml中声明属性之后会在R类中生成对应的资源ID,方便到时候TypeArray的使用(关于TypeArray见下文)

每个attr标签表示声明一个属性,name是属性的名字,format是属性的格式,比如string表示这个属性必须为字符串,color表示这个属性必须为颜色类型,dimension表示这个属性必须为像素即dp、sp之类的

2.创建自定义View类,继承于View类,重写View的三个构造方法

public class MyTextView extends View implements View.OnClickListener{

private String mytextContent;

private int mytextColor;

private int mytextSize;
//画笔,用于绘制图形时使用
private Paint paint;
//矩形对象,用于计算文字位置时使用
private Rect rect;

public MyTextView(Context context) {
// TODO Auto-generated constructor stub
this(context,null);
}

public MyTextView(Context context, AttributeSet attrs) {
// TODO Auto-generated constructor stub
this(context,attrs,0);

}

public MyTextView(Context context, AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
// TODO Auto-generated constructor stub
}

}


可以看到三个构造方法的区别在于参数

public MyTextView(Context context) 【通过传入上下文对象来创建view】

public MyTextView(Context context, AttributeSet attrs) 【AttributeSet类型是用来获得我们声明的各个属性,当我们在xml布局文件中声明自定义View时,就会调用此构造方法】

public MyTextView(Context context, AttributeSet attrs, int defStyleAttr) 【多了一个defStyleAttr属性,当我们需要为view设定style时才会用到】

另外,我们对前两个构造方法都调用了this(),学过java的都知道,this表示调用本类的构造方法,在第一个构造方法中,我们调用了 this(context,null);其实是调用了第二个构造方法,在第二个构造方法中调用了this(context,attrs,0);其实是调用了第三个构造方法,所以这样写的好处是无论我们使用哪个构造方法,最终都会进到第三个构造方法,所以接下来我们要做的便是在第三个构造方法中获得我们的属性

3.通过TypeArray获得各个自定义属性,并将Paint设置为这些属性的内容

public MyTextView(Context context, AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
// TODO Auto-generated constructor stub
TypedArray array = getContext().getTheme().obtainStyledAttributes(attrs, R.styleable.myTextView, defStyleAttr, 0);
//获得属性,并设置默认值
mytextContent = array.getString(R.styleable.myTextView_mytextContent);
mytextColor = array.getColor(R.styleable.myTextView_mytextColor,Color.WHITE);
mytextSize = array.getDimensionPixelSize(R.styleable.myTextView_mytextSize, 30);

array.recycle();

paint = new Paint();
//将画笔的文字大小设置为我们定义的大小
paint.setTextSize(mytextSize);
rect = new Rect();
/**
* 此方法可以获得文字所在的矩形区域,并赋给rect
* 参数1:传入文字的内容
* 参数2:传入文字起始的长度,一般为0
* 参数3:传入文字结束的长度,一般为text.length
* 参数4:传入一个Rect矩形对象
*/
paint.getTextBounds(mytextContent, 0, mytextContent.length(), rect);
}


刚才在上文中已说过,此构造方法的AttributeSet参数可以得到我们在attr中声明的属性,那为什么此处还要通过TypeArray来获得呢?TypeArray有什么用?其实如果我们是直接通过AttributeSet获得属性的话,还需要解析才能获得我们需要的各个属性的值以及格式,而TypeArray帮我们把这些操作都封装好了,所以通过TypeArray可以很方便的获取到我们的属性。

4.重写onMeasure方法,设置好视图在界面上所显示的大小

@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
// TODO Auto-generated method stub
super.onMeasure(widthMeasureSpec, heightMeasureSpec);
//setMeasuredDimension(widthMeasureSpec, heightMeasureSpec);
}


对这个方法的使用见下文

5.重写onDraw方法,通过paint和canvas渲染出自定义控件

@Override
protected void onDraw(Canvas canvas) {
// TODO Auto-generated method stub
paint.setColor(Color.BLACK);
//canvas.drawRect(0, 0, getMeasuredWidth(), getMeasuredHeight(), paint);
/**
* 参数1:圆心的横坐标
* 参数2:圆心的纵坐标
* 参数3:圆的半径
* 参数4:用来绘制的画笔
*/
canvas.drawCircle(getMeasuredWidth()/2, getMeasuredHeight()/2, getMeasuredWidth()/2, paint);
paint.setColor(Color.RED);
canvas.drawCircle(getMeasuredWidth()/2, getMeasuredHeight()/2, getMeasuredWidth()/3, paint);

paint.setColor(mytextColor);
canvas.drawText(mytextContent, getWidth() / 2 - rect.width() / 2, getHeight() / 2 + rect.height() / 2, paint);
}


我们在这里通过canvas调用了两次drawCircle,即绘制了两个圆形,注意到第二个圆形的半径为getMeasuredWidth()/3,比第一个圆的半径小了,而圆心又与第一个圆一致,所以待会儿绘制出来的效果就是一个小圆叠在一个大圆前面,在每次绘制圆之前都调用了paint设置颜色,是为了两个圆形的颜色不一样。

最后再次设置paint颜色为mytextColor,即我们的文字内容的颜色,然后通过canvas.drawText进行文字的绘制,这里之所以getWidth() / 2 - rect.width() / 2是因为将View的宽度的一半减去文字内容的宽度的一半,得到的就是文字内容的左上角的横坐标(可以自行画图理解),纵坐标也是同理。

至此,我们完成了基本的定义,接下来就是在布局文件中使用它了:

<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
xmlns:mytext="http://schemas.android.com/apk/res/com.example.myview"
android:layout_width="match_parent"
android:layout_height="match_parent"
tools:context="${relativePackage}.${activityClass}" >

<com.example.view.MyTextView
android:layout_width="200dp"
android:layout_height="200dp"
mytext:mytextContent="1"
mytext:mytextColor="#FFF"
mytext:mytextSize="20sp"
/>

</RelativeLayout>


xmlns:mytext="http://schemas.android.com/apk/res/com.example.myview"是命名空间的声明,等下我们声明自定义属性的时候需要用到,这里的路径是http://scheam.....res/+项目包名

运行



运行之后,发现在屏幕成功出现了一个圆形背景中间带有文字内容,这里我们设置的宽和高都是200dp,如果将layout_width和layout_height都设置为wrap_content,会发现它并不像我们平时那样会自动有一个默认大小,而是像fill_parent一样的填充屏幕,很明显这不是我们愿意看到的,这个时候就需要用到onMeasure方法了:

首先为我们的自定义类加上两个私有成员标量:

//wrap_content时默认的宽度
private final int DEFAULT_WIDTH = 200;
//wrap_content时默认的高度
private final int DEFAULT_HEIGHT = 200;


重写onMeasure方法:

@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
// TODO Auto-generated method stub
//super.onMeasure(widthMeasureSpec, heightMeasureSpec);

int width;  //最终的宽度
int height;   //最终的高度

int wspecMode = MeasureSpec.getMode(widthMeasureSpec);
int wspecSize = MeasureSpec.getSize(widthMeasureSpec);
int hspecMode = MeasureSpec.getMode(heightMeasureSpec);
int hspecSize = MeasureSpec.getSize(heightMeasureSpec);

if(wspecMode==MeasureSpec.EXACTLY){
width = wspecSize;
}
else{
width = DEFAULT_WIDTH;
}
if(hspecMode==MeasureSpec.EXACTLY){
height = hspecSize;
}
else{
height = DEFAULT_HEIGHT;
}
setMeasuredDimension(width, height);
}


其中,有两个关键的方法,getModegetSize,getSize是获得用户所设定的view的宽高,getMode是获得这个属性的模式,Mode有三种

MeasureSpec.EXACTLY【设置了明确的值或者是MATCH_PARENT】

MeasureSpec.AT_MOST【表示子布局限制在一个最大值内,一般为WARP_CONTENT】

MeasureSpec.UNSPECIFIED【表示子布局想要多大就多大,很少使用】

对模式进行判断,如果模式是EXACTLY,说明用户在布局文件中对宽高设定了具体数值或者match_parent,如果是具体数值,那么就将这个具体数值作为最终宽高,如果是match_parent,就将其填充父布局。如果是AT_MOST,说明用户在布局文件中设定为了wrap_content,那么就应该将我们的默认宽度或者高度作为最终的宽高

通过以上测量,就能够在设定为wrap_content时依然能够限定在一定的大小中。

另外,如果想为自定义控件加上一些点击事件之类的效果,我们还可以:

1.让自定义类实现OnClickListener



2.在构造方法中注册监听事件



3.实现onClick方法



运行并点击,就会发现文字内容每点击一次就+1

希望本文能够帮助你解决自定义View的困惑,关于ViewGroup和重写系统控件的见解会在以后的博文进行整理
内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息
标签: