自定义View
为什么要自定义View?
---既然goole已经为我们提供了很多原生的view,我们为什么还要自定义view呢?主要是Android系统内置的View无法实现我们的需求,我们需要针对我们的业务需求定制我们想要的View。同时自定义View对于一个Android开发者来说是必须掌握的知识点,也是Android开发进阶的必经之路。
如何实现view的自定义?
自定义View的最基本的三个方法分别是: onMeasure()、onLayout()、onDraw();
View在Activity中显示出来,要经历测量、布局和绘制三个步骤,分别对应三个动作:measure、layout和draw。
测量:onMeasure()决定View的大小;
布局:onLayout()决定View在ViewGroup中的位置;
绘制:onDraw()决定绘制这个View。
自定义控件又分为自定义View和自定义ViewGroup,自定义View只需要重写onMeasure()和onDraw()即可,而自定义ViewGroup则只需要重写onMeasure()和onLayout()。
自定义View原理是Android开发者必须了解的基础,基础掌握了我们才能进行下一步的学习
基础的学习思路:
View的分类
类别解释特点
单一视图即一个View,如TextView不包含子View
视图组即多个View组成的ViewGroup,如LinearLayout包含子View
View类简介:
View类是Android中各种组件的基类,如View是ViewGroup基类。
Android中的UI组件都由View、ViewGroup组成。
View的构造函数:共有4个,具体如下:
--------------自定义view必须重写至少一个构造
// 如果View是在Java代码里面new的,则调用第一个构造函数
publicCarsonView(Contextcontext) {
super(context);
}
// 如果View是在.xml里声明的,则调用第二个构造函数
// 自定义属性是从AttributeSet参数传进来的
publicCarsonView(Contextcontext,AttributeSetattrs) {
super(context,attrs);
}
// 不会自动调用
// 一般是在第二个构造函数里主动调用
// 如View有style属性时
publicCarsonView(Contextcontext,AttributeSetattrs,intdefStyleAttr) {
super(context,attrs,defStyleAttr);
}
//API21之后才使用
// 不会自动调用
// 一般是在第二个构造函数里主动调用
// 如View有style属性时
publicCarsonView(Contextcontext,AttributeSetattrs,intdefStyleAttr,intdefStyleRes) {
super(context,attrs,defStyleAttr,defStyleRes);
}
3.View视图结构
对于多View的视图,结构是树形结构:最顶层是ViewGroup,ViewGroup下可能有多个ViewGroup或View,如下图:
一定要记住:无论是measure过程、layout过程还是draw过程,永远都是从View树的根节点开始测量或计算(即从树的顶端开始),一层一层、一个分支一个分支地进行(即树形递归),最终计算整个View树中各个View,最终确定整个View树的相关属性。
Android坐标系
Android的坐标系定义为:
屏幕的左上角为坐标原点
向右为x轴增大方向
向下为y轴增大方向
View位置(坐标)描述
View的位置由4个顶点决定的(如下A、B、C、D):
注意:View的位置是相对于父控件而言的
Top:子View上边界到父view上边界的距离
Left:子View左边界到父view左边界的距离
Bottom:子View下边距到父View上边界的距离
Right:子View右边界到父view左边界的距离
位置获取方式
View的位置是通过view.getxxx()函数进行获取:(以Top为例)
// 获取Top位置
public final int getTop() {
return mTop;
}
// 其余如下:
getLeft(); //获取子View左上角距父View左侧的距离
getBottom(); //获取子View右下角距父View顶部的距离
getRight(); //获取子View右下角距父View左侧的距离
Android的角度(angle)与弧度(radian)
自定义View实际上是将一些简单的形状通过计算,从而组合到一起形成的效果。
这会涉及到画布的相关操作(旋转)、正余弦函数计算等,即会涉及到角度(angle)与弧度(radian)的相关知识
角度和弧度都是描述角的一种度量单位,区别如下图:
在默认的屏幕坐标系中角度增大方向为顺时针。
注:在常见的数学坐标系中角度增大方向为逆时针
Android中颜色相关内容:
Android中的颜色相关内容包括颜色模式,创建颜色的方式,以及颜色的混合模式等。
Android支持的颜色模式:
以ARGB8888为例介绍颜色定义:
在java中定义颜色:
//java中使用Color类定义颜色
int color = Color.GRAY; //灰色
//Color类是使用ARGB值进行表示
int color = Color.argb(127, 255, 0, 0); //半透明红色
int color = 0xaaff0000; //带有透明度的红色
在xml文件中定义颜色:
//定义了红色(没有alpha(透明)通道)
#ff0000 //定义了蓝色(没有alpha(透明)通道)
#00ff00
在java文件中引用xml中定义的颜色:
//方法1
int color = getResources().getColor(R.color.mycolor);
//方法2(API 23及以上)
int color = getColor(R.color.myColor);
在xml文件(layout或style)中引用或者创建颜色:
- @color/red
android:background="@color/red"
android:background="#ff0000"
有了以上的基础,接下来我们就可以来学习自定义view的具体过程了;
首先是Measure过程
OnMeasure()方法是自定义控件中非常重要的一个方法,下面我们来系统的学习,由浅至深来Measure的过程
Measure的作用作用:
测量View的宽/高:
1.在某些情况下,需要多次测量(measure)才能确定View最终的宽/高;
2.在这种情况下measure过程后得到的宽/高可能是不准确的;
3.建议在layout过程中onLayout()去获取最终的宽/高
准备的基础
在了解measure 过程前,我们需要先了解measure过程中传递尺寸(宽 / 高测量值)的两个类:
ViewGroup.LayoutParams (View 自身的布局参数)
MeasureSpecs 类(父视图对子视图的测量要求)
2.1 ViewGroup.LayoutParams:
这个类我们很常见,用来指定视图的高度(height)和宽度(width)等布局参数。可通过以下参数进行指定:
fill_parent : 即一个View,如TextView
match_parent: 与fill_parent相同,用于Android 2.3及之后版本
wrap_content : 自适应大小,强制性地使视图扩展以便显示其全部内容(含 padding )
具体的应用如下图:
android:layout_weight="wrap_content" //自适应大小
android:layout_weight="match_parent" //与父视图等高
android:layout_weight="fill_parent" //与父视图等高
android:layout_weight="100dip" //精确设置高度值为 100dip
ViewGroup 的子类有其对应的 ViewGroup.LayoutParams 子类
1.ViewGroup 的子类包括RelativeLayout、LinearLayout等;
2.如 RelativeLayout的 ViewGroup.LayoutParams 的子类是RelativeLayoutParams。
构造函数
构造函数是View的入口,可以用于初始化一些的内容,和获取自定义属性。
// View的构造函数有四种重载
public DIY_View(Context context){
super(context);
}
public DIY_View(Context context,AttributeSet attrs){
super(context, attrs);
}
public DIY_View(Context context,AttributeSet attrs,int defStyleAttr ){
super(context, attrs,defStyleAttr);
// 第三个参数:默认Style
// 默认Style:指在当前Application或Activity所用的Theme中的默认Style
// 且只有在明确调用的时候
2.2 MeasureSpec
2.21 定义:测量规格也可理解为是测量View的依据
MeasureSpec的类型分为两种:
即每个MeasureSpec代表了一组宽度和高度的测量规格
2.22 作用:决定了一个view的大小(宽/高)
2.23 组成:如下图
其中,Mode模式共分为三类:
UNSPECIFIED模式():unspecified (未指明的;未详细说明的)
EXACTLY模式:exactly (恰好地;正是;精确地;正确地)
AT_MOST模式:at most:至多
具体说明如下图:
2.2.4 MeasureSpec类的使用
MeasureSpec 、Mode 和Size都封装在View类中的一个内部类里 - MeasureSpec类。
MeasureSpec类通过使用二进制,将mode和size打包成一个int值来减少对象内存分配,用一个变量携带两个数据(size,mode),并提供了打包和解包的方法
2.2.6 MeasureSpec值的确定:
子View的MeasureSpec值是根据子View的布局参数(LayoutParams)和父容器的MeasureSpec值计算得来的,具体计算逻辑封装在getChildMeasureSpec()里。
如下图:
关于getChildMeasureSpec()里对于子View的测量模式和大小的判断逻辑有点复杂;
别担心,我已经帮大家总结好。具体子View的测量模式和大小请看下表:
规律总结:(以子View为标准,横向观察)
当子View采用具体数值(dp / px)时无论父容器的测量模式是什么,子View的测量模式都是EXACTLY且大小等于设置的具体数值;
当子View采用match_parent时子View的测量模式与父容器的测量模式一致若测量模式为EXACTLY,则子View的大小为父容器的剩余空间;若测量模式为AT_MOST,则子View的大小不超过父容器的剩余空间
当子View采用wrap_parent时无论父容器的测量模式是什么,子View的测量模式都是AT_MOST且大小不超过父容器的剩余空间。
UNSPECIFIED模式:由于适用于系统内部多次measure情况,很少用到,故此处不讨论
注:区别于顶级View(即DecorView)的计算逻辑
onMeasure()方法中常用的方法:
1.getChildCount():获取子View的数量;
2.getChildAt(i):获取第i个子控件;
3.subView.getLayoutParams().width/height:设置或获取子控件的宽或高;
4.measureChild(child, widthMeasureSpec, heightMeasureSpec):测量子View的宽高;
5.child.getMeasuredHeight/width():执行完measureChild()方法后就可以通过这种方式获取子View的宽高值;
6.getPaddingLeft/Right/Top/Bottom():获取控件的四周内边距;
7.setMeasuredDimension(width, height):重新设置控件的宽高。如果写了这句代码,就需要删除“super. onMeasure(widthMeasureSpec, heightMeasureSpec);”这行代码。
注意:onMeasure()方法可能被调用多次,这是因为控件中的内容或子View可能对分配给自己的空间“不满意”,因此向父空间申请重新分配空间。
接下来是layout过程:
主要说onLayout()这个方法:
onLayout()方法负责布局,大多数情况是在自定义ViewGroup中才会重写,主要用来确定子View在这个布局空间中的摆放位置。 onLayout(boolean changed, int left, int top, int right, int bottom)方法有5个参数,其中changed表示这个控件是否有了新的尺寸或位置;left、top、right、bottom分别表示这个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布局的上下左右边的坐标。
Draw过程:
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):设置图形重叠时的处理方式;
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屏幕上显示效果;
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度.
小结:绘制什么,由Canvas处理。 怎么去绘制,由Paint处理。
最后我们需要注意的是绘制顺序
Android 里面的绘制都是按顺序的,先绘制的内容会被后绘制的盖住。比如你在重叠的位置先画圆再画方,和先画方再画圆所呈现出来的结果肯定是不同的:
到底放在 super.onDraw() 上面还是下面?通常如果我们继承的是 View 的话,super.onDraw() 只是一个空实现,所以它的位置放在哪儿都没事,甚至直接不要也没事,但反正加上也没啥影响,尽量还是加上吧。由于 Android 的绘制顺序性,当你继承至已经有绘制的其他 View(比如 TextView)的时候,放在 super.onDraw() 上面就意味着绘制代码会被控件的原内容盖住。
dispatchDraw():绘制子 View 的方法
自定义绘制其实不止 onDraw() 一个方法。onDraw() 只是负责自身主体内容绘制的。而有的时候,你想要的遮盖关系无法通过 onDraw() 来实现,而是需要通过别的绘制方法。
例如,你继承了一个 LinearLayout,重写了它的 onDraw() 方法,在 super.onDraw() 中插入了你自己的绘制代码,使它能够在内部绘制一些斑点作为点缀:
看起来确实没有问题,但是你会发现,当你添加了子 View 之后,你的斑点不见了:
造成这种情况的原因是 Android 的绘制顺序:在绘制过程中,每一个 ViewGroup 会先调用自己的 onDraw() 来绘制完自己的主体之后再去绘制它的子 View。对于上面这个例子来说,就是你的 LinearLayout 会在绘制完斑点后再去绘制它的子 View。那么在子 View 绘制完成之后,先前绘制的斑点就被子 View 盖住了。具体来讲,这里说的「绘制子 View」是通过另一个绘制方法的调用来发生的,这个绘制方法叫做:dispatchDraw()。也就是说,在绘制过程中,每个 View 和 ViewGroup 都会先调用 onDraw() 方法来绘制主体,再调用 dispatchDraw() 方法来绘制子 View。
怎样才能让 LinearLayout 的绘制内容盖住子 View 呢?只要让它的绘制代码在子 View 的绘制之后再执行就好了。所以直接执行在 super.dispatchDraw() 的下面即可。
下面是绘制的流程图: