写在前面
首先我们要明白,为什么要自定义View?主要是Android系统内置的View无法实现我们的需求,我们需要针对我们的业务需求定制我们想要的View。
开发俩年,多多少少写了些自定义控件和自定义View,这次还是好好整理下,面的有些东西老是遗漏。不过自己在公司前辈面前算是菜鸟了,所以水平有限,记录一些比较基础的和网上参考的,仅作个人总结和分享。
一、自定义View常用场景和分类
需求永远是在变更的,花样多的不行,很多时候系统自带的控件和布局无法完成的时候就可以使用自定义View来实现,或者,在某些多处复用的布局,我们可以将其封装成一个组合控件,用到的地方直接复用,减少了代码量也更加便于维护。
类型 | 定义 |
1.继承View | 不复用系统控件逻辑,继承View进行功能定义 |
2.自定义组合控件 | 多个控件组合成为一个新的控件,方便多处复用 |
3.继承系统View控件 | 继承自TextView等系统控件,在系统控件的基础功能上进行扩展 |
4.继承系统ViewGroup | 继承自LinearLayout等系统控件,在系统控件的基础功能上进行扩展 |
5.继承ViewViewGroup | 不复用系统控件逻辑,继承ViewGroup进行功能定义 |
后面会单独的每个类型都写个Demo
二、基本属性
2.1 构造函数
构造函数 ,是一种特殊的方法。主要用来在创建对象时初始化对象, 即为对象成员变量赋初始值,总与new运算符一起使用在创建对象的语句中。特别的一个类可以有多个构造函数 ,可根据其参数个数的不同或参数类型的不同来区分它们 即构造函数的重载。
无论是继承系统控件还是直接继承View,我们都需要写出他的构造函数,例如一个TestView
public TestView(Context context) {
super(context);
}
public TestView(Context context, AttributeSet attrs) {
super(context, attrs);
}
来个kotlin的
class TestView: View {
constructor(context: Context): super(context){
}
constructor(context: Context, attributeSet: AttributeSet): super(context, attributeSet){
}
constructor(context: Context, attributeSet: AttributeSet, defStyleAttr: Int): super(context, attributeSet, defStyleAttr){
}
init {
}
}
kotlin还可以简化,Kotlin中@JvmOverloads的作用
class TestView @JvmOverloads constructor(context: Context, attrs: AttributeSet? = null, defStyleAttr: Int = 0)
: View(context, attrs, defStyleAttr) {
init{
}
}
2.2 View自身的坐标
View的坐标就相当于在屏幕上有个X,Y轴,根据坐标位置可以进行很多自定义操作,默认坐标原点为左上角。当然可以通过path.moveto等方法更改操作的坐标位置
通过如下方法可以获取View到其父控件的距离。
- getTop();获取View到其父布局顶边的距离。
- getLeft();获取View到其父布局左边的距离。
- getBottom();获取View到其父布局顶边的距离。
- getRight();获取View到其父布局左边的距离。
由此可计算宽高:
- width = getRight() - getLeft();
- height = getBottom() - getTop();
View的源码当中提供了getWidth()和getHeight()方法用来获取View的宽度和高度,其内部方法和上文所示是相同的,我们可以直接调用来获取View的宽高。
2.3 View的自定义属性 AttributeSet
每个控件都会有属性,既然是自定义控件怎么能少了自定义属性呢!
操作步骤:
Android自定义属性可分为以下几步:
- 自定义一个View
- 编写values/attrs.xml,在其中编写styleable和item等标签元素
- 在布局文件中View使用自定义的属性
- 在View的构造方法中通过TypedArray获取
例如:
1.attrs.xml
<?xml version="1.0" encoding="utf-8"?>
<resources>
<!-- 这里会写什么呢!,当然是我们的自定义属性 -->
<declare-styleable name="MyViewStyle" >
<declare-styleable name="MyViewStyle" >
<attr name="content" format="string" />
<attr name="isShow" format="boolean"/>
<attr name="background" format="color"/>
</declare-styleable>
</resources>
关于里面的format:即支持的自定义属性
- reference:引用资源 传入资源ID
- string:字符串
- Color:颜色
- boolean:布尔值
- dimension:尺寸值
- float:浮点型
- integer:整型
- fraction:百分数
- enum:枚举类型
- flag:位或运算
2.View代码
private String mContent;
private Boolean mIsShow;
private int mBackground;
private int mSelect;
public MyView(Context context, AttributeSet attrs) {
super(context, attrs);
TypedArray typedArray = context.obtainStyledAttributes(attrs,R.styleable.MyViewStyle);
if(typedArray != null){
//这里要注意,String类型是没有默认值的,所以必须定义好,不然又是空指针大法
mContent = typedArray.getString(R.styleable.MyViewStyle_content);
mIsShow = typedArray.getBoolean(R.styleable.MyViewStyle_is_show, true);
mBackground = typedArray.getColor(R.styleable.MyViewStyle_view_background, Color.RED);
mSelect = typedArray.getInt(R.styleable.MyViewStyle_select, 0);
}
}
3.当用于XML文件上时:记住哦,前面是app:
<com.xlhgo.viewdemo.MyView
app:content="这是内容"
app:is_show="false"
app:view_background="@color/colorAccent"
app:select="s1"
android:layout_width="wrap_content"
android:layout_height="wrap_content" />
三、onMeasure函数
自定义View我们大部分时候只需重写两个函数(不包括构造函数):onMeasure()、onDraw()。onMeasure负责对当前View的尺寸进行测量,onDraw负责把当前这个View绘制出来。
需求:
xml布局文件中,我们的layout_width和layout_height参数可以不用写具体的尺寸,而是wrap_content或者是match_parent。其意思我们都知道,就是将尺寸设置为“包住内容”和“填充父布局给我们的所有空间”。这两个设置并没有指定真正的大小,可是我们绘制到屏幕上的View必须是要有具体的宽高的,正是因为这个原因,我们必须自己去处理和设置尺寸。当然了,View类给了默认的处理,但是如果View类的默认处理不满足我们的要求,我们就得重写onMeasure函数。
函数:
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec)
{
super.onMeasure(widthMeasureSpec, heightMeasureSpec);
}
测量模式:
重写之前先了解MeasureSpec的specMode,一共三种类型:
EXACTLY:一般是设置了明确的值或者是MATCH_PARENT,当前的尺寸就是当前View应该取的尺寸
AT_MOST:表示子布局限制在一个最大值内,一般为WARP_CONTENT,当前尺寸是当前View能取的最大尺寸。
UNSPECIFIED:表示子布局想要多大就多大,父容器没有对当前View有任何限制,当前View可以任意取尺寸。
举例:假设我们要实现这样一个效果:将当前的View以正方形的形式显示,即要宽高相等,并且默认的宽高值为100像素。
private int getMySize(int defaultSize, int measureSpec) {
int mySize = defaultSize;
int mode = MeasureSpec.getMode(measureSpec);
int size = MeasureSpec.getSize(measureSpec);
switch (mode) {
case MeasureSpec.UNSPECIFIED: {//如果没有指定大小,就设置为默认大小
mySize = defaultSize;
break;
}
case MeasureSpec.AT_MOST: {//如果测量模式是最大取值为size
//我们将大小取最大值,你也可以取其他值
mySize = size;
break;
}
case MeasureSpec.EXACTLY: {//如果是固定的大小,那就不要去改变它
mySize = size;
break;
}
}
return mySize;
}
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
super.onMeasure(widthMeasureSpec, heightMeasureSpec);
int width = getMySize(100, widthMeasureSpec);
int height = getMySize(100, heightMeasureSpec);
if (width < height) {
height = width;
} else {
width = height;
}
setMeasuredDimension(width, height);
}
四、重写onDraw()
当我们可以去自定义尺寸后,我们就可以去通过onDraw方法绘制出来了。
画个圆:
其中的 Paint在自定义View时经常会用到,可以理解为是一个画笔,粗细,颜色,填充/描边等都可以通过它来设置。
其中的 Canvas是画布,我们通过自己的笔画在上面。当一个View较为复杂时,可以在这个View里定义多个画布和画笔。
@Override
protected void onDraw(Canvas canvas) {
//调用父View的onDraw函数,因为View这个类帮我们实现了一些
// 基本的而绘制功能,比如绘制背景颜色、背景图片等
super.onDraw(canvas);
int r = 40;//也可以是getMeasuredHeight()/2,本例中我们已经将宽高设置相等了
//圆心的横坐标为当前的View的左边起始位置+半径
int centerX = getLeft() + r;
//圆心的纵坐标为当前的View的顶部起始位置+半径
int centerY = getTop() + r;
//矩形左边离父布局 248 离顶部218,右边400,底部452
new rectF = new RectF(248f,218f,400f,452f)
Paint paint = new Paint();
paint.setColor(Color.GREEN);
//开始绘制
canvas.drawCircle(centerX, centerY, r, paint);
// 画椭圆 canvas?.drawOval(rectF, paint)
// 画直线 canvas?.drawLine(400f,455f,400f,700f,piant)
}
Android Canvashttps://blog.csdn.net/iispring/article/details/49770651?ops_request_misc=%257B%2522request%255Fid%2522%253A%2522163825853716780261958851%2522%252C%2522scm%2522%253A%252220140713.130102334..%2522%257D&request_id=163825853716780261958851&biz_id=0&utm_medium=distribute.pc_search_result.none-task-blog-2~all~sobaiduend~default-1-49770651.pc_search_result_control_group&utm_term=android+canvas&spm=1018.2226.3001.4187Android Painthttps://blog.csdn.net/abcdef314159/article/details/51720686?ops_request_misc=%257B%2522request%255Fid%2522%253A%2522163825841716780261931477%2522%252C%2522scm%2522%253A%252220140713.130102334..%2522%257D&request_id=163825841716780261931477&biz_id=0&utm_medium=distribute.pc_search_result.none-task-blog-2~all~baidu_landing_v2~default-4-51720686.pc_search_result_control_group&utm_term=android+paint&spm=1018.2226.3001.4187