自定义View
一、重要概念介绍
Canvas (画布)类
可以用来实现各种图形的绘制工作,如绘制直线,矩形,圆等等
- 绘制直线:canvas.drawLine();
- 绘制矩形:canvas.drawRect();
- 绘制圆形:canvas.drawCircle();
- 绘制字符:canvas.drawText();
- 绘制图形:canvas.drawBitmap();
Paint(画笔)类
要绘制图形,首先得需要画笔,按照自己的开发需要设置画笔的相关属性
- setAntiAlias() :设置画笔的锯齿效果
- setColor() :设置画笔的颜色
- setARGB() :设置画笔的A,R,G,B值
- setAlpha() :设置画笔的Alpha值
- setTextSize() :设置字体的大小
- setStyle() :设置画笔的风格(空心或者实心)
- setStrokeWidth() :设置空心边框的宽度
- getColor() :获取画笔的颜色
Rect(尺寸)类
- 设置画布大小 new Rect(int left, int top ,int right, int bottom)
二、自定义View的步骤及实现
自定义View的步骤
- 创建自定义类继承View
- 自定义属性
- 选择和设置构造方法
- 重写onMeasure() 方法
- 重写onDonDraw() 方法
- 重写onLayout() 方法
- 重写其他事件的方法(滑动监听等)
实现
一、创建自定义类继承View
所有的控件都继承View,自定义View也同样继承View
public class CustomTitleView extends View{
//实现构造方法
}
二、自定义属性
Android 自定义属性主要有定义、使用和获取三个步骤。
通常我们将自定义属性定义在 /values/attrs.xml文件中(attrs.xml 需要自己创建)
<?xml version="1.0" encoding="UTF-8"?>
<resources>
<attr name="titleText" format="string" />
<attr name="titleTextColor" format="color" />
<attr name="titleTextSize" format="dimension" />
<declare-styleable name="CustomTitleView">
<attr name="titleText" />
<attr name="titleTextColor" />
<attr name="titleTextSize" />
</declare-styleable>
</resources>
上段代码,自定义了三个属性 titleText ,titleTextColor, titleTextSize,然后在CustomTitleView 中引用这些属性。
declare-stylable 标签只是为了给自定义属性分类,一个项目中可能有多个自定义View,而只能有一个attrs.xml 文件,因此我们需要给不同定义控件的自定义属性进行分类,这个就是为什么declare-stylable 标签中的name 属性往往是自定义控件的名称。
format 定义了属性的类型,常见的属性有:
1)string:字符串类型;
2)integer:整数类型
3)float:浮点数
4)dimension:尺寸,后面必须跟dp、dip、px、sp等单位
5)Boolean:布尔值
6)reference:引用类型,传入的是某一个资源的ID,必须以"@"符号开头
7)color:颜色,必须是"#"符号开头
8)fraction:百分比,必须是“%”结尾
9)enum:枚举类型
format 中可以写多个类型,中间使用 “|” 符号隔开,表示这几种类型都可以传入这个属性。
接下来是如何去使用自定义属性,自定义VIew都需要在布局文件中使用。
使用前,需要先定义一个namespace。Android默认的namespace是android,因此我们通常可以使用 android:XXX 的格式去设置一个控件的某个属性,android这个namespace的定义是在XML文件的头标签中定义的,通常是这样的:
xmlns:android="http://schemas.android.com/apk/res/android"
我们自定义的属性不在这个命名控件下,因此我们需要添加一个命名空间。
自定义属性的命名控件如下:
xmlns:custom="http://schemas.android.com/apk/res/com.example.customview01"
可以看出,除了将命名空间的名称从android 改成 custom之外,就是讲最后的 /res/android 改成 /res/com.example.customview01 。
注意:自定义namespace 的名称可以自己定义,不一定非要是 custom
然后在XML布局文件中添加自定义View
<com.example.customview01.CustomTitleView
android:layout_width="300dp"
android:layout_height="100dp"
custom:titleText="3712"
custom:titleTextColor="#ff0000"
custom:titleTextSize="40sp" />
接下来,就是在自定义View的三参数的构造函数中通过 TypedArray 获取到自定义属性:
public CustomTitleView(Context context, AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
// TODO Auto-generated constructor stub
TypedArray a = context.getTheme().obtainStyledAttributes(attrs,R.styleable.CustomTitleView,defStyleAttr,0);
int n = a.getIndexCount();
for(int i = 0;i<n;i++){
int attr = a.getIndex(i);
switch(attr){
case R.styleable.CustomTitleView_titleText:
mTitleText = a.getString(attr);
break;
case R.styleable.CustomTitleView_titleTextColor:
//默认颜色是黑色
mTitleTextColor = a.getInt(attr, Color.BLACK);
break;
case R.styleable.CustomTitleView_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);
}
三、选择和设置构造方法
当自定义类继承View时,提示我们需要重写构造方法,View中一共有四个构造方法,它们分别有1,2,3,4个参数。
一个参数的构造方法:
public CustomTitleView(Context context) {
this(context, null);
}
这个构造方法只有一个参数Context上下文。当我们在JAVA代码中直接通过new关键在创建这个控件时,就会调用这个方法。
两个参数的构造方法
public CustomTitleView(Context context, AttributeSet attrs) {
this(context, attrs, 0);
}
这个构造方法有两个参数:Context上下文和AttributeSet属性集。当我们需要在自定义控件中获取属性时,就默认调用这个构造方法。AttributeSet对象就是这个控件中定义的所有属性。
我们可以通过AttributeSet对象的getAttributeCount()方法获取属性的个数,通过getAttributeName()方法获取到某条属性的名称,通过getAttributeValue()方法获取到某条属性的值。
注意:不管有没有使用自定义属性,都会默认调用这个构造方法,“使用了自定义属性就会默认调用三个参数的构造方法”的说法是错误的。
三个参数的构造方法
public CustomTitleView(Context context, AttributeSet attrs, int defStyleAttr){...}
这个构造方法中有三个参数:Context上下文、AttributeSet属性集和defStyleAttr自定义属性的引用。这个构造方法不会默认调用,必须要手动调用,这个构造方法和两个参数的构造方法的唯一区别就是这个构造方法给我们默认传入了一个默认属性集。
defStyleAttr指向的是自定义属性的**<declare-styleable标签中定义的自定义属性集,我们在创建TypedArray对象时需要用到defStyleAttr**。
三个构造方法的整合
一般情况下,我们会将这三个构造方法串联起来,即层层调用,让最终的业务处理都集中在三个参数的构造方法。我们让一参的构造方法引用两参的构造方法,两参的构造方法引用三参的构造方法。示例代码如下:
public CustomTitleView(Context context) {
this(context, null);
}
public CustomTitleView(Context context, AttributeSet attrs) {
this(context, attrs, 0);
}
public CustomTitleView(Context context, AttributeSet attrs, int defStyleAttr) {
//处理
}
这样一来,就可以保证无论使用什么方式创建这个控件,最终都会到三个参数的构造方法中处理,减少了重复代码。
四、重写onMeasure()方法
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:当没有指定宽高值时使用(很少见)。
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可能对分配给自己的空间“不满意”,因此向父空间申请重新分配空间。
示例代码:
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
// super.onMeasure(widthMeasureSpec,heightMeasureSpec);
int width;
int height;
int WIDTH_MODE = MeasureSpec.getMode(widthMeasureSpec);
int WIDTH_SIZE = MeasureSpec.getSize(widthMeasureSpec);
int HEIGHT_MODE = MeasureSpec.getMode(heightMeasureSpec);
int HEIGHT_SIZE = MeasureSpec.getSize(heightMeasureSpec);
if(WIDTH_MODE == MeasureSpec.EXACTLY) {
width = WIDTH_SIZE;
}else if(WIDTH_MODE == MeasureSpec.AT_MOST){
width = 300;
}
if(HEIGHT_MODE == MeasureSpec.EXACTLY) {
height = HEIGHT_SIZE;
}else if(HEIGHT_MODE == MeasureSpec.AT_MOST) {
height = 150;
}
setMeasuredDimension(width, height);
}
5、onDraw()
onDraw() 方法负责绘制,即如果我们希望的效果在Android原生中没有支持,那么我们就需要绘制自己的控件显示效果。
注意: 每次触摸了自定义View时都会出发onDraw() 方法。
Paint 类 (笔)
Paint 画笔对象,这个类中包含了如何绘制几何图形、文字和位图的形式和颜色信息,指定了如何绘制文本和图形。画笔对象有很多设置方法,可以分为两类:一类与图形绘制有关,一类与文本绘制有关。
图形绘制:
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):设置图形重叠时的处理方式;
文本绘制:
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屏幕上显示效果;
其他方法:
1) getTextBounds(String t, int s, int e, Rect b):将页面中t文本从s下标开始到e下标结束的所有字符所占的区域宽高封装到b这个矩形中;
2) clearShadowLayer():清除阴影层;
3) measureText(String t, int s, int e):返回t文本中从s下标开始到e下标结束的所有字符所占的宽度;
4) reset():重置画笔为默认值。
这里需要就几个方法进行解释一下:
1、setPathEffect(pathEffect pe):设置绘制的路径效果 常见的有以下几种可选方案:
1) CornerPathEffect:可以用圆角来代替尖锐的角;
2) DathPathEffect:虚线,由短线和点组成;
3) DiscretePathEffect:荆棘状的线条;
4) PathDashPathEffect:定义一种新的形状并将其作为原始路径的轮廓标记;
5) SumPathEffect:在一条路径中顺序添加参数中的效果;
6) ComposePathEffect:将两种效果组合起来,先使用第一种效果,在此基础上应用第二种效果。
2、setXfermode (xfermode m):设置图形重叠时的处理方法
关于Xfermode 的多种效果,可以参考下图
在使用的时候,我们需要通过paint.setXfermode(new PorterDuffXfermode(PorterDuff.Mode.XXX)) 来设置,XXX 是图上某种模式对应的常量参数,
这16中情况的具体解释如下:
1.PorterDuff.Mode.CLEAR:所绘制不会提交到画布上。
2.PorterDuff.Mode.SRC:显示上层绘制图片
3.PorterDuff.Mode.DST:显示下层绘制图片
4.PorterDuff.Mode.SRC_OVER:正常绘制显示,上下层绘制叠盖。
5.PorterDuff.Mode.DST_OVER:上下层都显示。下层居上显示。
6.PorterDuff.Mode.SRC_IN:取两层绘制交集。显示上层。
7.PorterDuff.Mode.DST_IN:取两层绘制交集。显示下层。
8.PorterDuff.Mode.SRC_OUT:上层绘制非交集部分。
9.PorterDuff.Mode.DST_OUT:取下层绘制非交集部分。
10.PorterDuff.Mode.SRC_ATOP:取下层非交集部分与上层交集部分
11.PorterDuff.Mode.DST_ATOP:取上层非交集部分与下层交集部分
12.PorterDuff.Mode.XOR:异或:去除两图层交集部分
13.PorterDuff.Mode.DARKEN:取两图层全部区域,交集部分颜色加深
14.PorterDuff.Mode.LIGHTEN:取两图层全部,点亮交集部分颜色
15.PorterDuff.Mode.MULTIPLY:取两图层交集部分叠加后颜色
16.PorterDuff.Mode.SCREEN:取两图层全部区域,交集部分变为透明色
Canvas (画布)类
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度。
代码示例:
protected void onDraw(Canvas canvas) {
// TODO Auto-generated method stub
super.onDraw(canvas);
mPaint.setColor(Color.YELLOW);
//画矩形
canvas.drawRect(0,0,getMeasuredWidth(),getMeasuredWidth(),mPaint);
mPaint.setColor(mTitleTextColor);
//画文本
canvas.drawText(mTitleText, getWidth()/2-mBound.width()/2, getHeight()/2+mBound.height()/2, mPaint);
}
6、onLayout()
onLayout()方法负责布局,大多数情况是在自定义ViewGroup中才会重写,主要用来确定子View在这个布局空间中的摆放位置。
onLayout(boolean changed, int l, int t, int r, int b)方法有5个参数,其中changed表示这个控件是否有了新的尺寸或位置;l、t、r、b分别表示这个View相对于父布局的左/上/右/下方的位置。
以下是onLayout()方法中常用的方法:
1) getChildCount():获取子View的数量;
2) getChildAt(i):获取第i个子View
3) getWidth/Height():获取onMeasure()中返回的宽度和高度的测量值;
4) child.getLayoutParams():获取到子View的LayoutParams对象;
5) child.getMeasuredWidth/Height():获取onMeasure()方法中测量的子View的宽度和高度值;
6) getPaddingLeft/Right/Top/Bottom():获取控件的四周内边距;
7) child.layout(l, t, r, b):设置子View布局的上下左右边的坐标。
7、其他方法
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()方法是异步请求重绘视图。
requestLayout()
requestLayout()方法只是对View树进行重新布局layout过程(包括measure()过程和layout()过程),不会调用draw()过程,即不会重新绘制任何视图,包括该调用者本身。
requestFocus()
请求View树的draw()过程,但只会绘制需要重绘的视图,即哪个View或ViewGroup调用了这个方法,就重绘哪个视图。
8、总结
最后,让我们总览以下自定义view时调用各个函数的顺序,如图:
在这些方法中:
- onMeasure()会在初始化之后调用一到多次来测量控件或其中的子控件的宽高;
- onLayout()会在onMeasure()方法之后被调用一次,将控件或其子控件进行布局;
- onDraw()会在onLayout()方法之后调用一次,也会在用户手指触摸屏幕时被调用多次,来绘制控件。