您的位置:首页 > 其它

手把手教你写一个完整的自定义View

2018-03-31 10:52 459 查看
参考链接:https://blog.csdn.net/carson_ho/article/details/62037696
自定义View是Android开发者必须了解的基础

今天,我将手把手教你写一个自定义View,并理清自定义View所有应该的注意点 

目录



1. 自定义View的分类

自定义View一共分为两大类,具体如下图: 


2. 具体介绍 & 使用场景

对于自定义View的类型介绍及使用场景如下图: 


3. 使用注意点

在使用自定义View时有很多注意点(坑),希望大家要非常留意: 


3.1 支持特殊属性

支持wrap_content 
如果不在
onMeasure()
中对
wrap_content
作特殊处理,那么
wrap_content
属性将失效
具体原因请看文章:为什么你的自定义View wrap_content不起作用?
支持padding & margin 
如果不支持,那么
padding
margin
(ViewGroup情况)的属性将失效
对于继承View的控件,padding是在draw()中处理
对于继承ViewGroup的控件,padding和margin会直接影响measure和layout过程

3.2 多线程应直接使用post方式

View的内部本身提供了post系列的方法,完全可以替代Handler的作用,使用起来更加方便、直接。

3.3 避免内存泄露

主要针对View中含有线程或动画的情况:当View退出或不可见时,记得及时停止该View包含的线程和动画,否则会造成内存泄露问题。启动或停止线程/ 动画的方式: 
1. 启动线程/ 动画:使用
view.onAttachedToWindow()
,因为该方法调用的时机是当包含View的Activity启动的时刻 
2. 停止线程/ 动画:使用
view.onDetachedFromWindow()
,因为该方法调用的时机是当包含View的Activity退出或当前View被remove的时刻

3.4 处理好滑动冲突

当View带有滑动嵌套情况时,必须要处理好滑动冲突,否则会严重影响View的显示效果。

4. 具体实例

接下来,我将用自定义View中最常用的继承View来说明自定义View的具体应用和需要注意的点

4.1 继承VIew的介绍


在下面的例子中,我将讲解:如何实现一个基本的自定义View(继承VIew)
如何自身支持wrap_content & padding属性
如何为自定义View提供自定义属性(如颜色等等)

实例说明:画一个实心圆

4.2 具体步骤

创建自定义View类(继承View类)
布局文件添加自定义View组件
注意点设置(支持wrap_content & padding属性自定义属性等等)
下面我将逐个步骤进行说明: 
步骤1:创建自定义View类(继承View类)CircleView.java
// 用于绘制自定义View的具体内容
// 具体绘制是在复写的onDraw()内实现

public class CircleView extends View {

// 设置画笔变量
Paint mPaint1;

// 自定义View有四个构造函数
// 如果View是在Java代码里面new的,则调用第一个构造函数
public CircleView(Context context){
super(context);

// 在构造函数里初始化画笔的操作
init();
}

// 如果View是在.xml里声明的,则调用第二个构造函数
// 自定义属性是从AttributeSet参数传进来的
public CircleView(Context context,AttributeSet attrs){
super(context, attrs);
init();

}

// 不会自动调用
// 一般是在第二个构造函数里主动调用
// 如View有style属性时
public CircleView(Context context,AttributeSet attrs,int defStyleAttr ){
super(context, attrs,defStyleAttr);
init();
}

//API21之后才使用
// 不会自动调用
// 一般是在第二个构造函数里主动调用
// 如View有style属性时
public  CircleView(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) {
super(context, attrs, defStyleAttr, defStyleRes);
}

// 画笔初始化
private void init() {

// 创建画笔
mPaint1 = new Paint ();
// 设置画笔颜色为蓝色
mPaint1.setColor(Color.BLUE);
// 设置画笔宽度为10px
mPaint1.setStrokeWidth(5f);
//设置画笔模式为填充
mPaint1.setStyle(Paint.Style.FILL);

}

// 复写onDraw()进行绘制
@Override
protected void onDraw(Canvas canvas) {

super.onDraw(canvas);

// 获取控件的高度和宽度
int width = getWidth();
int height = getHeight();

// 设置圆的半径 = 宽,高最小值的2分之1
int r = Math.min(width, height)/2;

// 画出圆(蓝色)
// 圆心 = 控件的中央,半径 = 宽,高最小值的2分之1
canvas.drawCircle(width/2,height/2,r,mPaint1);

}

}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
特别注意: 
1. View的构造函数一共有4个,具体使用请看:深入理解View的构造函数和 
理解View的构造函数 
2. 对于绘制内容为何在复写onDraw()里实现,具体请看我写的文章:自定义View Draw过程- 最易懂的自定义View原理系列(4)步骤2:在布局文件中添加自定义View类的组件activity_main.xml
<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
tools:context="scut.carson_ho.diy_view.MainActivity">

<!-- 注意添加自定义View组件的标签名:包名 + 自定义View类名-->
<!--  控件背景设置为黑色-->
<scut.carson_ho.diy_view.CircleView
android:layout_width="match_parent"
android:layout_height="150dp"
android:background="#000000"

</RelativeLayout>
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
步骤3:在MainActivity类设置显示MainActivity.java
public class MainActivity extends AppCompatActivity {

@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
}
}
1
2
3
4
5
6
7
8


好了,至此,一个基本的自定义View已经实现了。接下来继续看自定义View所有应该注意的点:如何手动支持wrap_content属性
如何手动支持padding属性
如何为自定义View提供自定义属性(如颜色等等)

a. 手动支持wrap_content属性

先来看wrap_content & match_parent属性的区别
// 视图的宽和高被设定成刚好适应视图内容的最小尺寸
android:layout_width="wrap_content"

// 视图的宽和高延伸至充满整个父布局
android:layout_width="match_parent"
// 在Android API 8之前叫作"fill_parent"
1
2
3
4
5
6
如果不手动设置支持
wrap_content
属性,那么
wrap_content
属性是不会生效(显示效果同
match_parent
)具体原因 & 解决方案请看我写的文章:为什么你的自定义View wrap_content不起作用?

b. 支持padding属性

padding
属性:用于设置控件内容相对控件边缘的边距;区别与margin属性(同样称为:边距):控件边缘相对父控件的边距(父控件控制),具体区别如下:

如果不手动设置支持padding属性,那么padding属性在自定义View中是不会生效的。
<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
tools:context="scut.carson_ho.diy_view.MainActivity">

<scut.carson_ho.diy_view.CircleView
android:layout_width="match_parent"
android:layout_height="match_parent"
/**  添加Padding属性,但不会生效 **/
android:padding="20dp"
/>
</RelativeLayout>
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15

解决方案

绘制时考虑传入的padding属性值(四个方向)。在自定义View类的复写onDraw()进行设置CircleView.java
// 仅看复写的onDraw()
@Override
protected void onDraw(Canvas canvas) {

super.onDraw(canvas);

// 获取传入的padding值
final int paddingLeft = getPaddingLeft();
final int paddingRight = getPaddingRight();
final int paddingTop = getPaddingTop();
final int paddingBottom = getPaddingBottom();

// 获取绘制内容的高度和宽度(考虑了四个方向的padding值)
int width = getWidth() - paddingLeft - paddingRight ;
int height = getHeight() - paddingTop - paddingBottom ;

// 设置圆的半径 = 宽,高最小值的2分之1
int r = Math.min(width, height)/2;

// 画出圆(蓝色)
// 圆心 = 控件的中央,半径 = 宽,高最小值的2分之1
canvas.drawCircle(paddingLeft+width/2,paddingTop+height/2,r,mPaint1);

}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25


c. 提供自定义属性

系统自带属性,如
// 基本是以android开头
android:layout_width="match_parent"
android:layout_height="match_parent"
android:background="#000000"
android:padding="30dp"
1
2
3
4
5
6
但有些时候需要一些系统所没有的属性,称为自定义属性
使用步骤有如下: 
在values目录下创建自定义属性的xml文件
在自定义View的构造方法中解析自定义属性的值
在布局文件中使用自定义属性

下面我将对每个步骤进行具体介绍

步骤1:在values目录下创建自定义属性的xml文件

attrs_circle_view.xml
<?xml version="1.0" encoding="utf-8"?>
<resources>
<!--自定义属性集合:CircleView-->
<!--在该集合下,设置不同的自定义属性-->
<declare-styleable name="CircleView">
<!--在attr标签下设置需要的自定义属性-->
<!--此处定义了一个设置图形的颜色:circle_color属性,格式是color,代表颜色-->
<!--格式有很多种,如资源id(reference)等等-->
<attr name="circle_color" format="color"/>

</declare-styleable>
</resources>
1
2
3
4
5
6
7
8
9
10
11
12
对于自定义属性类型 & 格式如下:
<-- 1. reference:使用某一资源ID -->
<declare-styleable name="名称">
<attr name="background" format="reference" />
</declare-styleable>
// 使用格式
<ImageView
android:layout_width="42dip"
android:layout_height="42dip"
android:background="@drawable/图片ID" />

<--  2. color:颜色值 -->
<declare-styleable name="名称">
<attr name="textColor" format="color" />
</declare-styleable>
// 格式使用
<TextView
android:layout_width="42dip"
android:layout_height="42dip"
android:textColor="#00FF00" />

<-- 3. boolean:布尔值 -->
<declare-styleable name="名称">
<attr name="focusable" format="boolean" />
</declare-styleable>
// 格式使用
<Button
android:layout_width="42dip"
android:layout_height="42dip"
android:focusable="true" />

<-- 4. dimension:尺寸值 -->
<declare-styleable name="名称">
<attr name="layout_width" format="dimension" />
</declare-styleable>
// 格式使用:
<Button
android:layout_width="42dip"
android:layout_height="42dip" />

<-- 5. float:浮点值 -->
<declare-styleable name="AlphaAnimation">
<attr name="fromAlpha" format="float" />
<attr name="toAlpha" format="float" />
</declare-styleable>
// 格式使用
<alpha
android:fromAlpha="1.0"
android:toAlpha="0.7" />

<-- 6. integer:整型值 -->
<declare-styleable name="AnimatedRotateDrawable">
<attr name="frameDuration" format="integer" />
<attr name="framesCount" format="integer" />
</declare-styleable>
// 格式使用
<animated-rotate
xmlns:android="http://schemas.android.com/apk/res/android"
android:frameDuration="100"
android:framesCount="12"
/>

<-- 7. string:字符串 -->
<declare-styleable name="MapView">
<attr name="apiKey" format="string" />
</declare-styleable>
// 格式使用
<com.google.android.maps.MapView
android:apiKey="0jOkQ80oD1JL9C6HAja99uGXCRiS2CGjKO_bc_g" />

<-- 8. fraction:百分数 -->
<declare-styleable name="RotateDrawable">
<attr name="pivotX" format="fraction" />
<attr name="pivotY" format="fraction" />
</declare-styleable>
// 格式使用
<rotate
xmlns:android="http://schemas.android.com/apk/res/android"
android:pivotX="200%"
android:pivotY="300%"
/>

<-- 9. enum:枚举值 -->
<declare-styleable name="名称">
<attr name="orientation">
<enum name="horizontal" value="0" />
<enum name="vertical" value="1" />
</attr>
</declare-styleable>
// 格式使用
<LinearLayout
android:layout_width="fill_parent"
android:layout_height="fill_parent"
/>

<-- 10. flag:位或运算 -->
<declare-styleable name="名称">
<attr name="windowSoftInputMode">
<flag name="stateUnspecified" value="0" />
<flag name="stateUnchanged" value="1" />
<flag name="stateHidden" value="2" />
<flag name="stateAlwaysHidden" value="3" />
<flag name="stateVisible" value="4" />
<flag name="stateAlwaysVisible" value="5" />
<flag name="adjustUnspecified" value="0x00" />
<flag name="adjustResize" value="0x10" />
<flag name="adjustPan" value="0x20" />
<flag name="adjustNothing" value="0x30" />
</attr>
</declare-styleable>、
// 使用
<activity
android:name=".StyleAndThemeActivity"
android:label="@string/app_name"
android:windowSoftInputMode="stateUnspecified | stateUnchanged | stateHidden" >

<intent-filter>
<action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.LAUNCHER" />
</intent-filter>
</activity>

<-- 特别注意:属性定义时可以指定多种类型值 -->
<declare-styleable name="名称">
<attr name="background" format="reference|color" />
</declare-styleable>
// 使用
<ImageView
android:layout_width="42dip"
android:layout_height="42dip"
android:background="@drawable/图片ID|#00FF00" />
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133

步骤2:在自定义View的构造方法中解析自定义属性的值

此处是需要解析circle_color属性的值
// 该构造函数需要重写
public CircleView(Context context, AttributeSet attrs) {

this(context, attrs,0);
// 原来是:super(context,attrs);
init();

public CircleView(Context context, AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);

// 加载自定义属性集合CircleView
TypedArray a = context.obtainStyledAttributes(attrs,R.styleable.CircleView);

// 解析集合中的属性circle_color属性
// 该属性的id为:R.styleable.CircleView_circle_color
// 将解析的属性传入到画圆的画笔颜色变量当中(本质上是自定义画圆画笔的颜色)
// 第二个参数是默认设置颜色(即无指定circle_color情况下使用)
mColor = a.getColor(R.styleable.CircleView_circle_color,Color.RED);

// 解析后释放资源
a.recycle();

init();
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26

步骤3:在布局文件中使用自定义属性

activity_main.xml
<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
<!--必须添加schemas声明才能使用自定义属性-->
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
tools:context="scut.carson_ho.diy_view.MainActivity"
>

<!-- 注意添加自定义View组件的标签名:包名 + 自定义View类名-->
<!--  控件背景设置为黑色-->
<scut.carson_ho.diy_view.CircleView
android:layout_width="wrap_content"
android:layout_height="wrap_content"

android:background="#000000"
android:padding="30dp"

<!--设置自定义颜色-->
app:circle_color="#FF4081"
/>
</RelativeLayout>
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23


至此,一个较为规范的自定义View已经完成了。

完整代码下载

Carson_Ho的github:自定义View的具体应用

5. 总结

如果希望继续了解自定义View的原理,请参考文章: 

1.自定义View基础 - 最易懂的自定义View原理系列(1)2.自定义View Measure过程 - 最易懂的自定义View原理系列(2)3. 自定义View Layout过程 - 最易懂的自定义View原理系列(3)4.自定义View Draw过程- 最易懂的自定义View原理系列(4)
内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息
标签: