自定义View/ViewGroup基本步骤
- 选择和设置构造方法;
- 重写onMeasure()方法;
- 重写onDraw()方法;
- 重写onLayout()方法;
- 自定义属性;
- 重写其他事件的方法(滑动监听等)。
下面我们就来进行自定义View的绘制流程,在进行绘制的开始先看一下View常用的方法:
一、自定义View
1. 构造器
新建一个MyView类继承自View类,选择构造器,这里有四个构造器,区别是参数的不同,一般用前三个构造器,参数不一样分别对应不同的创建方式:
只有一个Context参数的构造方法
public MyView(Context context) {
super(context);
}
通常是通过代码初始化控件时使用,即当我们在JAVA代码中直接通过new关键在创建这个控件时,就会调用这个方法.
两个参数(Context上下文和AttributeSet属性集)的构造方法
public MyView(Context context, @Nullable AttributeSet attrs) {
super(context, attrs);
init();
}
两个参数的构造方法通常对应布局文件中控件被映射成对象时调用(需要解析属性),即当我们需要在自定义控件中获取属性时,就默认调用这个构造方法。AttributeSet对象就是这个控件中定义的所有属性。可以通过AttributeSet对象的getAttributeCount()方法获取属性的个数,通过getAttributeName()方法获取到某条属性的名称,通过getAttributeValue()方法获取到某条属性的值。
注意:不管有没有使用自定义属性,都会默认调用这个构造方法
三个参数(Context上下文、AttributeSet属性集和defStyleAttr自定义属性)的构造方法
public MyView(Context context, @Nullable AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
init();
}
这个构造方法不会默认调用,必须要手动调用,这个构造方法和两个参数的构造方法的唯一区别就是这个构造方法给我们默认传入了一个默认属性集。defStyleAttr指向的是自定义属性的<declare-styleable>标签中定义的自定义属性集,我们在创建TypedArray对象时需要用到defStyleAttr。
一般情况下,我们会将这三个构造方法串联起来层层调用,让最终的业务处理都集中在三个参数的构造方法。我们让一参的构造方法引用两参的构造方法,两参的构造方法引用三参的构造方法。示例代码如下:
public MyView(Context context) {
this(context,null);
}
public MyView(Context context, @Nullable AttributeSet attrs) {
this(context, attrs,0);
}
public MyView(Context context, @Nullable AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
init();
//业务代码
}
2. 重写三个方法
@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);
}
2.1 OnMeasurea()方法
onMeasure()方法主要负责测量,决定控件本身或其子控件所占的宽高。我们可以通过onMeasure()方法提供的参数widthMeasureSpec和heightMeasureSpec来分别获取控件宽度和高度的测量模式和测量值(测量 = 测量模式 + 测量值)。
其中widthMeasureSpec和heightMeasureSpec虽然只是int类型的值,但它们是通过MeasureSpec类进行了编码处理的,其中封装了测量模式和测量值,因此我们可以分别通过MeasureSpec.getMode(xMeasureSpec)和MeasureSpec. getSize(xMeasureSpec)来获取到控件或其子View的测量模式和测量值。
测量模式分为以下三种情况:
- EXACTLY:当宽高值设置为具体值时使用,如100dip、match_parent等,此时取出的size是精确的尺寸;
- AT_MOST:当宽高值设置为wrap_content时使用,此时取出的size是控件最大可获得的空间;
- UNSPECIFIED:当没有指定宽高值时使用(很少见)。
也就是说如果我们的自定义控件在布局文件中,只需要设置指定的具体宽高,或者MATCH_PARENT 的情况,我们可以不用重写onMeasure方法。但如果自定义控件需要设置包裹内容WRAP_CONTENT ,我们需要重写onMeasure方法,为控件设置需要的尺寸;默认情况下WRAP_CONTENT 的处理也将填充整个父控件。
onMeasure方法最后需要调用setMeasuredDimension方法来保存测量的宽高值。
onMeasure()方法中常用的方法:
- getChildCount():获取子View的数量;
- getChildAt(i):获取第i个子控件;
- subView.getLayoutParams().width/height:设置或获取子控件的宽或高;
- measureChild(child, widthMeasureSpec, heightMeasureSpec):测量子View的宽高;
- child.getMeasuredHeight/width():执行完measureChild()方法后就可以通过这种方式获取子View的宽高值;
- getPaddingLeft/Right/Top/Bottom():获取控件的四周内边距;
- setMeasuredDimension(width, height):重新设置控件的宽高。如果写了这句代码,就需要删除“super. onMeasure(widthMeasureSpec, heightMeasureSpec);”这行代码。
注意:onMeasure()方法可能被调用多次,这是因为控件中的内容或子View可能对分配给自己的空间“不满意”,因此向父空间申请重新分配空间。
2.2 OnLayout()方法
onLayout()方法负责布局,大多数情况是在自定义ViewGroup中才会重写,主要用来确定子View在这个布局空间中的摆放位置。 onLayout(boolean changed, int left, int top, int right, int bottom)方法有5个参数,其中changed表示这个控件是否有了新的尺寸或位置;left、top、right、bottom分别表示这个View相对于父布局的左/上/右/下方的位置。
以下是onLayout()方法中常用的方法:
- getChildCount():获取子View的数量;
- getChildAt(i):获取第i个子View
- getWidth/Height():获取onMeasure()中返回的宽度和高度的测量值;
- child.getLayoutParams():获取到子View的LayoutParams对象;
- child.getMeasuredWidth/Height():获取onMeasure()方法中测量的子View的宽度和高度值;
- getPaddingLeft/Right/Top/Bottom():获取控件的四周内边距;
- child.layout(l, t, r, b):设置子View布局的上下左右边的坐标。
2.3 OnDraw()方法
onDraw()方法负责绘制,即如果我们希望得到的效果在Android原生控件中没有现成的支持,那么我们就需要自己绘制我们的自定义控件的显示效果。要学习onDraw()方法,我们就需要学习在onDraw()方法中使用最多的两个类:Paint和Canvas。
注意:每次自定义View/ViewGroup时都会调用onDraw()方法。
Paint类
Paint画笔对象,这个类中包含了如何绘制几何图形、文字和位图的样式和颜色信息,指定了如何绘制文本和图形。画笔对象有很多设置方法,大体上可以分为两类:图形绘制和文本绘制。
Paint类中有如下方法:
1、图形绘制:
1) setArgb(int a, int r, int g, int b):设置绘制的颜色,a表示透明度,r、g、b表示颜色值;
2) setAlpha(int a):设置绘制的图形的透明度;
3) setColor(int color):设置绘制的颜色;
4) setAntiAlias(boolean a):设置是否使用抗锯齿功能,抗锯齿功能会消耗较大资源,绘制图形的速度会减慢;
5) setDither(boolean b):设置是否使用图像抖动处理,会使图像颜色更加平滑饱满,更加清晰;
6) setFileterBitmap(Boolean b):设置是否在动画中滤掉Bitmap的优化,可以加快显示速度;
7) setMaskFilter(MaskFilter mf):设置MaskFilter来实现滤镜的效果;
8) setColorFilter(ColorFilter cf):设置颜色过滤器,可以在绘制颜色时实现不同颜色的变换效果;
9) setPathEffect(PathEffect pe):设置绘制的路径的效果;
10) setShader(Shader s):设置Shader绘制各种渐变效果;
11) setShadowLayer(float r, int x, int y, int c):在图形下面设置阴影层,r为阴影角度,x和y为阴影在x轴和y轴上的距离,c为阴影的颜色;
12) setStyle(Paint.Style s):设置画笔的样式:FILL实心;STROKE空心;FILL_OR_STROKE同时实心与空心;
13) setStrokeCap(Paint.Cap c):当设置画笔样式为STROKE或FILL_OR_STROKE时,设置笔刷的图形样式;
14) setStrokeJoin(Paint.Join j):设置绘制时各图形的结合方式;
15) setStrokeWidth(float w):当画笔样式为STROKE或FILL_OR_STROKE时,设置笔刷的粗细度;
16) setXfermode(Xfermode m):设置图形重叠时的处理方式;</pre>
2、文本绘制:
1) setTextAlign(Path.Align a):设置绘制的文本的对齐方式;
2) setTextScaleX(float s):设置文本在X轴的缩放比例,可以实现文字的拉伸效果;
3) setTextSize(float s):设置字号;
4) setTextSkewX(float s):设置斜体文字,s是文字倾斜度;
5) setTypeFace(TypeFace tf):设置字体风格,包括粗体、斜体等;
6) setUnderlineText(boolean b):设置绘制的文本是否带有下划线效果;
7) setStrikeThruText(boolean b):设置绘制的文本是否带有删除线效果;
8) setFakeBoldText(boolean b):模拟实现粗体文字,如果设置在小字体上效果会非常差;
9) setSubpixelText(boolean b):如果设置为true则有助于文本在LCD屏幕上显示效果;</pre>
Canvas类
Canvas即画布,其上可以使用Paint画笔对象绘制很多东西。
Canvas对象中可以绘制:
1) drawArc():绘制圆弧;
2) drawBitmap():绘制Bitmap图像;
3) drawCircle():绘制圆圈;
4) drawLine():绘制线条;
5) drawOval():绘制椭圆;
6) drawPath():绘制Path路径;
7) drawPicture():绘制Picture图片;
8) drawRect():绘制矩形;
9) drawRoundRect():绘制圆角矩形;
10) drawText():绘制文本;
11) drawVertices():绘制顶点。
Canvas对象的其他方法:
1) canvas.save():把当前绘制的图像保存起来,让后续的操作相当于是在一个新图层上绘制;
2) canvas.restore():把当前画布调整到上一个save()之前的状态;
3) canvas.translate(dx, dy):把当前画布的原点移到(dx, dy)点,后续操作都以(dx, dy)点作为参照;
4) canvas.scale(x, y):将当前画布在水平方向上缩放x倍,竖直方向上缩放y倍;
5) canvas.rotate(angle):将当前画布顺时针旋转angle度.
2.4 其他方法
generateLayoutParams()
generateLayoutParams()方法用在自定义ViewGroup中,用来指明子控件之间的关系,即与当前的ViewGroup对应的LayoutParams。我们只需要在方法中返回一个我们想要使用的LayoutParams类型的对象即可。在generateLayoutParams()方法中需要传入一个AttributeSet对象作为参数,这个对象是这个ViewGroup的属性集,系统根据这个ViewGroup的属性集来定义子View的布局规则,供子View使用。
例如,在自定义流式布局中,我们只需要关心子控件之间的间隔关系,因此我们需要在generateLayoutParams()方法中返回一个new MarginLayoutParams()即可。
onTouchEvent()
onTouchEvent()方法用来监测用户手指操作。我们通过方法中MotionEvent参数对象的getAction()方法来实时获取用户的手势,有UP、DOWN和MOVE三个枚举值,分别表示用于手指抬起、按下和滑动的动作。每当用户有操作时,就会回掉onTouchEvent()方法。
onScrollChanged()
如果我们的自定义View / ViewGroup是继承自ScrollView / HorizontalScrollView等可以滚动的控件,就可以通过重写onScrollChanged()方法来监听控件的滚动事件。这个方法中有四个参数:l和t分别表示当前滑动到的点在水平和竖直方向上的坐标;oldl和oldt分别表示上次滑动到的点在水平和竖直方向上的坐标。我们可以通过这四个值对滑动进行处理,如添加属性动画等。
invalidate()
invalidate()方法的作用是请求View树进行重绘,即draw()方法,如果视图的大小发生了变化,还会调用layout()方法。
一般会引起invalidate()操作的函数如下:
1) 直接调用invalidate()方法,请求重新draw(),但只会绘制调用者本身;
2) 调用setSelection()方法,请求重新draw(),但只会绘制调用者本身;
3) 调用setVisibility()方法,会间接调用invalidate()方法,继而绘制该View;
4) 调用setEnabled()方法,请求重新draw(),但不会重新绘制任何视图,包括调用者本身。
postInvalidate()
功能与invalidate()方法相同,只是postInvalidate()方法是异步请求重绘视图。
postInvalidate()内部调用的是postInvalidateDelayed(),可以用于控件的定时刷新。
requestLayout()
requestLayout()方法只是对View树进行重新布局layout过程(包括measure()过程和layout()过程),不会调用draw()过程,即不会重新绘制任何视图,包括该调用者本身。
requestFocus()
请求View树的draw()过程,但只会绘制需要重绘的视图,即哪个View或ViewGroup调用了这个方法,就重绘哪个视图。
自定义View / ViewGroup时调用的各种函数的顺序,如下图所示:
在这些方法中:
onMeasure()会在初始化之后调用一到多次来测量控件或其中的子控件的宽高;
onLayout()会在onMeasure()方法之后被调用一次,将控件或其子控件进行布局;
onDraw()会在onLayout()方法之后调用一次,也会在用户手指触摸屏幕时被调用多次,来绘制控件。
二、自定义属性
1. 为什么要自定义属性
我们在使用系统控件的时候,利用控件或布局的系统属性进行设置得到我们需要的显示效果,在使用自定义View的时候,就要使用自定义属性来进行设置。我们的自定义View是继承自View的,查看系统的自定义属性文件attrs.xml,看一下常用的控件是怎么定义的。系统定义的所有属性我们可以在\sdk\platforms\Android-xx\data\res\values目录下找到。
<declare-styleable name="View">
<attr name="id" format="reference" />
<attr name="background" format="reference|color" />
<attr name="padding" format="dimension" />
...
<attr name="focusable" format="boolean" />
...
</declare-styleable>
<declare-styleable name="TextView">
<attr name="text" format="string" localization="suggested" />
<attr name="hint" format="string" />
<attr name="textColor" />
<attr name="textColorHighlight" />
<attr name="textColorHint" />
...
</declare-styleable>
<declare-styleable name="ViewGroup_Layout">
<attr name="layout_width" format="dimension">
<enum name="fill_parent" value="-1" />
<enum name="match_parent" value="-1" />
<enum name="wrap_content" value="-2" />
</attr>
<attr name="layout_height" format="dimension">
<enum name="fill_parent" value="-1" />
<enum name="match_parent" value="-1" />
<enum name="wrap_content" value="-2" />
</attr>
</declare-styleable>
<declare-styleable name="LinearLayout_Layout">
<attr name="layout_width" />
<attr name="layout_height" />
<attr name="layout_weight" format="float" />
<attr name="layout_gravity" />
</declare-styleable>
<declare-styleable name="RelativeLayout_Layout">
<attr name="layout_centerInParent" format="boolean" />
<attr name="layout_centerHorizontal" format="boolean" />
<attr name="layout_centerVertical" format="boolean" />
...
</declare-styleable>
上面的标签,都有<declare-styleable name = "xxxx"> ,里面还有一堆的子标签,子标签就表示这是这个XXX类的属性,但是并不是每个控件都能使用所有属性,比如LinearLayout可以使用layout_weight属性,但是在RelativeLayout中就不能使用。可以看出自定义属性可以为我们自定义的View添加一些我们需要的属性,达到我们需要的效果。
2. 怎么自定义属性
根据上面的源码可以看出,这两种的区别就是attr标签后面带不带format属性,如果带format的就是在定义属性,如果不带format的就是在使用已有的属性,name的值就是属性的名字,format是限定当前定义的属性能接受什么值。而系统定义的属性一般引用都用android:XXX引用。
如果我们要定义一个text属性,所以我们可以有两种定义我们的方式
常规方式:
<resources>
<declare-styleable name="MyTextView">
<attr name=“text" format="string" />
</declare-styleable>
</resources>
引用系统已经定义好的:
<resources>
<declare-styleable name="MyTextView">
<attr name=“android:text"/>
</declare-styleable>
</resources>
为什么还要引自系统属性呢,因为自定义的MyTextView是继承的View,而android:text是TextView的特殊属性即自己定义的属性,所以这里必须要引用一下。其中declare-stylable标签只是为了给自定义属性分类。一个项目中可能又多个自定义控件,但只能又一个attr.xml文件,因此我们需要对不同自定义控件中的自定义属性进行分类,这也是为什么declare-stylable标签中的name属性往往定义成自定义控件的名称。
3. format的属性值
在自定义属性的时候用format来规定属性的类型,format一共支持以下11种类型
- string:字符串类型;
- integer:整数类型;
- float:浮点型;
- dimension:尺寸,后面必须跟dp、dip、px、sp等单位;
- Boolean:布尔值;
- reference:引用类型,传入的是某一资源的ID,必须以“@”符号开头;
- color:颜色,必须是“#”符号开头;
- fraction:百分比,必须是“%”符号结尾;
- enum:枚举类
- flag:位或运算
- 混合类型:属性定义时可以指定多种类型值,用 “|” 隔开
三、 自定义View实践之绘制时钟
public class ClockView extends View{
private Calendar time;
private float centerX,centerY,radius;
private float width,height;
/**
* 绘制表盘及时钟指针的画笔
*/
private Paint paintPoint;
private Paint paintCircle;
private Paint paintHour;
private Paint paintMinute;
private Paint paintSecond;
public ClockView(Context context) {
super(context,null);
initView();
}
public ClockView(Context context, @Nullable AttributeSet attrs) {
super(context, attrs,0);
initView();
}
public ClockView(Context context, @Nullable AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
initView();
}
@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);
width = getMeasuredWidth();
height = getMeasuredHeight();
centerX = width/2;
centerY = height/2;
radius = Math.min(width/2,height/2);
}
@Override
protected void onDraw(Canvas canvas) {
super.onDraw(canvas);
//绘制外圆
drawCircle(centerX,centerY,radius,canvas);
//绘制小时点
int points = 0;
int pointCount = 12;
while (points++ < pointCount){
drawPoints(centerX,centerY/6,8,canvas);
canvas.rotate(30f,centerX,centerY);
}
//绘制圆心
canvas.drawCircle(centerX,centerY,10,paintPoint);
canvas.save ();
drawHand (canvas);
postInvalidateDelayed (1000);
}
public void initView(){
/**
* 绘制外圆表盘
*/
paintCircle = new Paint();
paintCircle.setStyle(Paint.Style.FILL);
paintCircle.setAntiAlias(true);
paintCircle.setColor(Color.WHITE);
/**
* 绘制小时圆点,共十二个
*/
paintPoint = new Paint();
paintPoint.setStyle(Paint.Style.FILL);
paintPoint.setAntiAlias(true);
paintPoint.setColor(Color.GRAY);
paintHour = new Paint();
paintHour.setAntiAlias(true);
paintHour.setStrokeWidth(10f);
paintHour.setStyle(Paint.Style.FILL);
paintHour.setColor(Color.BLACK);
paintMinute = new Paint();
paintMinute.setAntiAlias(true);
paintMinute.setStrokeWidth(8f);
paintMinute.setStyle(Paint.Style.FILL);
paintMinute.setColor(Color.BLACK);
paintSecond = new Paint();
paintSecond.setAntiAlias(true);
paintSecond.setStrokeWidth(2f);
paintSecond.setStyle(Paint.Style.FILL);
paintSecond.setColor(Color.RED);
}
public void drawCircle(float x, float y, float radius, Canvas canvas){
canvas.drawCircle(x,y,radius,paintCircle);
}
public void drawPoints(float x, float y, float radius, Canvas canvas){
canvas.drawCircle(x,y,radius,paintPoint);
}
public void drawHand(Canvas canvas){
time = Calendar.getInstance();
float hour = time.get(Calendar.HOUR);
float minute = time.get(Calendar.MINUTE);
float second = time.get(Calendar.SECOND);
float angleHour = hour * (360 / 12)+minute/60*30;
//时针转过的角度
float angleMinute = minute*6;
//分针转过的角度
float angleSecond = second*6;
//秒针转过的角度
//绘制时针
canvas.save ();
canvas.rotate (angleHour,centerX,centerY);
canvas.drawLine (centerX,centerY,centerX,centerY/2,paintHour);
canvas.restore ();
canvas.save ();
canvas.rotate (angleMinute,centerX,centerY);
canvas.drawLine (centerX,centerY,centerX,centerY/3,paintMinute);
canvas.restore ();
canvas.save ();
canvas.rotate (angleSecond,centerX,centerY);
canvas.drawLine (centerX,centerY,centerX,centerY/4,paintSecond);
canvas.restore ();
}
}