自定义View
一、自定义View的分类
-
继承View或者ViewGroup类,重写onDraw方法进行绘制,调用invalidate方法重新绘制,常见的有绘制钟表,绘制圆形进度条
-
自定义组合控件,即将几种控件组合起来形成一个新的控件,这个新的组合控件就会整合了原来每一个控件的功能,常用的有自定义页面的ToolBar,自定义输入框
-
继承某一个控件,在该控件的基础之上添加新的功能。listview上下滚动定位功能
二、构造函数
public class MyCustomView extends View {
/**
* 第一个构造函数
*/
public MyCustomView(Context context) {
super(context, null);
}
/**
* 第二个构造函数
*/
public MyCustomView(Context context, AttributeSet attrs) {
super(context, attrs, 0);
// TODO:获取自定义属性
}
/**
* 第三个构造函数
*/
public MyCustomView(Context context, AttributeSet attrs, int defStyle) {
super(context, attrs, defStyle);
// TODO:获取自定义属性
}
/**
* 第四个构造函数
*/
public MyCustomView(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) {
super(context, attrs, defStyleAttr, defStyleRes);
}
自定义View通常有四个构造函数,需要重写哪个函数,具体看如下:
-
第一个构造函数:是在java文件中通过new对象,创建视图的时候调用,如果从中填充,则不会调用这个构造函数,参数需要一个上下文对象
-
第二个构造函数:用于XML文件中通过ayout文件实例化使用,会把xml中的参数通过attrs带入,方法的参数除了上下文对象,还需要一个attrs(主要是获取布局文件中自定义的属性)
-
第三个构造函数:也适用于XML文件中通过ayout文件实例化使用,与第二个构造函数的区别在于有第三个参数defStyle,它是Theme中的默认样式;
-
第四个构造函数:只有当第三个参数为0或者没有定义defStyleAttr时,第四个参数才起作用,它是style的引用,高版本才支持,所以一般不会用到。R.style中系统为view定义了很多默认主题Theme,主题中有对某些属性的默认赋值。
在实际使用中,系统自己调用的构造函数只有一个参数和两个参数的构造函数, 三个,四个参数的构造函数,通常由我们自己去主动调用。
一般定义view时,View的四个构造函数时相互调用的,调用其中的一个内部会调用相应的其他构造函数,例如Button,Button内部指定了默认的com.android.internal.R.attr.buttonStyle
三、自定义属性
添加自定义属性主要是通过declare-styleable标签为其配置自定义属性。
具体做法是: 在res/values/目录下增加一个resources xml文件,示例如下(res/values/attrs_my_custom_view.xml):
<resources>
<declare-styleable name="MyCustomView">
<attr name="TextColor" format="color" />
<attr name="Count" format="integer" />
<attr name="bgColor" format="color" />
<attr name="custom_attr" format="string" />
<attr name="custom_attr1" format="string1" />
<attr name="custom_attr2" format="string2" />
</declare-styleable>
<attr name="custom_attr5" format="string" />
</resources
所有resources文件中声明的属性都会在R.attr类中生成对应的成员变量:
public final class R {
public static final class attr {
public static final int custom_attr1=0x7f012538;
public static final int custom_attr2=0x7f016539;
public static final int custom_attr3=0x7f26303a;
}
}
但是声明在标签中的属性,系统还会在R.styleable类中生成相关的成员变量:
public static final class styleable {
public static final int[] MyCustomView = {
0x7f012538, 0x7f016539, 0x7f26303a
};
public static final int MyCustomView_custom_attr1 = 0;
public static final int MyCustomView_custom_attr2 = 1;
public static final int MyCustomView_custom_attr3 = 2;
}
可以看出,R.styleable.MyCustomView是一个数组,其中的元素值恰好就是R.attr.custom_attr1~R.attr.custom_attr4的值.而下面的MyCustomView_custom_attr1~MyCustomView_custom_attr4正好就是其对应的索引。
四、获取自定义属性
对于在上面定义的自定义属性,我们一般在第二个构造函数或第三个构造函数中获取,下面以三个参数的构造函数为例:
public MyCustomView(Context context, AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
TypedArray ta = context.obtainStyledAttributes(attrs, R.styleable.MyCustomView);
Color textColor= ta.getColor(R.styleable.MyCustomView_TextColor,Color.parseColor("#fff"));
int count= ta.getInteger(R.styleable.MyCustomView_TextColor,0);
Color bgColor = ta.getColor(R.styleable.MyCustomView_bgColor,Color.parseColor("#000"));
String attr1 = ta.getString(R.styleable.MyCustomView_custom_attr1);
String attr2 = ta.getString(R.styleable.MyCustomView_custom_attr2);
String attr3 = ta.getString(R.styleable.MyCustomView_custom_attr3);
}
在上面的代码中,用到了context.obtainStyledAttributes()这个函数,可能有些人对这个函数不太了解
下面让我们去看看它的源码:
/**
* Retrieve styled attribute information in this Context's theme. See
* {@link android.content.res.Resources.Theme#obtainStyledAttributes(AttributeSet, int[], int, int)}
* for more information.
*
* @see android.content.res.Resources.Theme#obtainStyledAttributes(AttributeSet, int[], int, int)
*/
public final TypedArray obtainStyledAttributes(
AttributeSet set, @StyleableRes int[] attrs) {
return getTheme().obtainStyledAttributes(set, attrs, 0, 0);
}
注释的翻译:检索上下文主题中的样式化属性信息。看到 {@link android.content.res.Resources。主题#obtainStyledAttributes(AttributeSet, int[], int, int)}了解更多信息。@see android.content.res.Resources。主题#obtainStyledAttributes(AttributeSet, int[], int, int)
通过对源码的追踪,我们发现context的两个参数的obtainStyledAttributes方法最终是调用了Theme的4个参数的obtainStyledAttributes方法。让我们来看一下这个函数的源码实现:
public TypedArray obtainStyledAttributes(AttributeSet set,
@StyleableRes int[] attrs, @AttrRes int defStyleAttr, @StyleRes int defStyleRes) {
return mThemeImpl.obtainStyledAttributes(this, set, attrs, defStyleAttr, defStyleRes);
}
TypedArray obtainStyledAttributes( Resources.Theme wrapper,
AttributeSet set, int[] attrs, int defStyleAttr, int defStyleRes) {
synchronized (mKey) {
final int len = attrs.length;
final TypedArray array = TypedArray.obtain(wrapper.getResources(), len);
// XXX note that for now we only work with compiled XML files.
// To support generic XML files we will need to manually parse
// out the attributes from the XML file (applying type information
// contained in the resources and such).
final XmlBlock.Parser parser = (XmlBlock.Parser) set;
mAssets.applyStyle(mTheme, defStyleAttr, defStyleRes, parser, attrs,
array.mDataAddress, array.mIndicesAddress);
array.mTheme = wrapper;
array.mXml = parser;
return array;
}
}
注释太长,就不给大家往出粘贴了,有兴趣的可以去看一看,
这里就不做过多的源码讲解,而是把这四个参数的含义解释给大家:
-
AttributeSet set: 属性值的集合。
-
int[] attrs: 我们自定义属性集合在R类中生成的int型数组.这个数组中包含了自定义属性的资源ID。
-
int defStyleAttr: 这是当前Theme中的包含的一个指向style的引用.当我们没有给自定义View设置declare-styleable资源集合时,默认从这个集合里面查找布局文件中配置属性值.传入0表示不向该defStyleAttr中查找默认值。
-
int defStyleRes: 这个也是一个指向Style的资源ID,但是仅在defStyleAttr为0或者defStyleAttr不为0但Theme中没有为defStyleAttr属性赋值时起作用。
由于一个属性可以在很多地方对其进行赋值,包括: XML布局文件中、decalare-styleable、theme中等,它们之间是有优先级次序的,按照优先级从高到低排序如下:
属性赋值优先级次序表:
在布局xml中直接定义 > 在布局xml中通过style定义 > 自定义View所在的Activity的Theme中指定style引用 > 构造函数中defStyleRes指定的默认值
五、主要方法
(一)、onMeasure():用于测量子控件的宽高
MeasureSpec在很大程度上决定了一个View的尺寸规格,大小
1、模式:exactly, at_most, unspecified:
- Exactly :是写出具体的dp值
- At_most :一般对应wrap_content,最大值不能超过父控件宽高
- Unspecified :一般在scrollView或者listview中,要多大就多大
2、常见的三个方法:
-
makeMeasureSpec(int size ,int mode) :makeMeasureSpec()方法的作用将size 和 mode 打包成一个32位的int值,之所以这样做就是为了减少内存的分配。返回值为打包成的int类型值measureSpec 。
-
getMode(int measureSpec) :getMode是根据传入的int 类型值,解析获得模式
-
getSize(int measureSpec) : getSize 是根据传入的int 类型值,解包成为size
注意只处理AT_MOST情况也就是wrap_content,其他情况则沿用系统的测量值即可。
setMeasuredDimension会设置View宽高的测量值,只有setMeasuredDimension调用之后,才能使用getMeasuredWidth()和getMeasuredHeight()来获取视图测量出的宽高,以此之前调用这两个方法得到的值都会是0。
如果我们不处理AT_MOST情况,那么即使设置了wrap_content,最终的效果也和match_parent一样,这是因为这种情况下,view的SpecSize就是父容器测量出来可用的大小。
(二)、onLayout():用于摆放子控件在父控件中的位置
只有ViewGroup才能让子控件显示在自己的什么位置,也就是说,这个方法是ViewGroup和ViewGroup的子类才会有的方法:
1、getWidth()方法和getMeasureWidth()的值基本相同。
2、但getMeasureWidth()方法在measure()过程结束后就可以获取到了,而getWidth()方法要在layout()过程结束后才能获取到。
3、另外,getMeasureWidth()方法中的值是通过setMeasuredDimension()方法来进行设置的,而getWidth()方法中的值则是通过视图右边的坐标减去左边的坐标计算出来的。
4、我们在ViewGroup中重写onLayout的目的:
就是设置当前View与其所有的子View,在ViewGroup(或其继承ViewGroup的Layout)父布局当中的位置。
5、childView.getMeasuredWidth();//在onMeasure()方法之后取得View的实际宽、高
childView.getMeasuredHeight();
(三)、onDraw(): 用于绘制需要的图形
1、ondraw函数有一个参数,为canvas,是一个绘制工具 ,canvas常用的方法有:
-
drawPoint(float x, float y, Paint paint) //画点,参数一水平x轴,参数二垂直y轴,第三个参数为Paint对象。
-
drawLine(float startX, float startY, float stopX, float stopY, Paintpaint) //画线,参数一起始点的x轴位置,参数二起始点的y轴位置,参数三终点的x轴水平位置,参数四y轴垂直位置,最后一个参数为Paint 画笔对象。
-
drawRect(RectF rect, Paint paint) //绘制区域,参数一为RectF一个区域
-
drawPath(Path path, Paint paint) //绘制一个路径,参数一为Path路径对象
-
drawBitmap(Bitmap bitmap, Rect src, Rect dst, Paint paint) //贴图,参数一就是我们常规的Bitmap对象,参数二是源区域(这里是bitmap),参数三是目标区域(应该在canvas的位置和大小),参数四是Paint画刷对象,因为用到了缩放和拉伸的可能,当原始Rect不等于目标Rect时性能将会有大幅损失。
-
drawOval(RectF oval, Paint paint)//画椭圆,参数一是扫描区域,参数二为paint对象;
-
drawCircle(float cx, float cy, float radius,Paint paint)// 绘制圆,参数一是中心点的x轴,参数二是中心点的y轴,参数三是半径,参数四是paint对象;
-
drawArc(RectF oval, float startAngle, float sweepAngle, boolean useCenter, Paint paint)//画弧,参数一是RectF对象,一个矩形区域椭圆形的界限用于定义在形状、大小、电弧,参数二是起始角(度)在电弧的开始,参数三扫描角(度)开始顺时针测量的,参数四是如果这是真的话,包括椭圆中心的电弧,并关闭它,如果它是假这将是一个弧线,参数五是Paint对象;
-
drawText(String text, float x, floaty, Paint paint) //渲染文本,Canvas类除了上面的还可以描绘文字,参数一是String类型的文本,参数二x轴,参数三y轴,参数四是Paint对象。
2、Paint 画笔,paint 常用方法如下:
- setStyle(Style style) :设置绘制模式
- setColor(int color) :设置颜色
- setStrokeWidth(float width) :设置线条宽度,画笔样式为空心时,设置空心画笔的宽度
- setTextSize(float textSize) :设置文字大小
- setAntiAlias(boolean aa) :设置抗锯齿开关
- setAlpha(int a) :设置画笔的透明度[0-255],0是完全透明,255是完全不透明
- setColorFilter(ColorFilter filter) :设置图形重叠时的显示方式,下面来演示一下
- setARGB(int a, int r, int g, int b) :设置画笔颜色,argb形式alpha,red,green,blue每个范围都是[0-255]
- setTextScaleX(float scaleX) :设置字体的水平方向的缩放因子,默认值为1,大于1时会沿X轴水平放大,小于1时会沿X轴水平缩小
- setTypeface(Typeface typeface) :设置字体样式
- setFakeBoldText(boolean fakeBoldText) :设置文本粗体
- setStrikeThruText(boolean strikeThruText) :设置文本的删除线
- setUnderlineText(boolean underlineText) :设置文本的下划线
- reset() : 重置Paint
- setFlags(int flags) :设置一些标志,比如抗锯齿,下划线等等
- setLetterSpacing(float letterSpacing) :设置行的间距,默认值是0,负值行间距会收缩
- setStrokeMiter(float miter) :当style为Stroke或StrokeAndFill时设置连接处的倾斜度,这个值必须大于0,看一下演示结果
- setDither(boolean dither) :设置是否抖动,如果不设置感觉就会有一些僵硬的线条,如果设置图像就会看的更柔和一些
- setStrokeCap(Paint.Cap cap) :设置线冒样式,取值有Cap.ROUND(圆形线冒)、Cap.SQUARE(方形线冒)、Paint.Cap.BUTT(无线冒)
- setStrokeJoin(Paint.Join join) :设置线段连接处样式,取值有:Join.MITER(结合处为锐角)、Join.Round(结合处为圆弧)、Join.BEVEL(结合处为直线)
推荐文章:
HenCoder Android 开发进阶: 自定义 View 绘制基础
HenCoder Android 开发进阶: 自定义 View Paint 详解