前言
- 自定义View是Android开发者必须了解的基础
- 今天,我将手把手教你写一个自定义View,并理清自定义View所有应该的注意点
阅读本文前,请先阅读我写的一系列自定义View文章
自定义View基础 - 最易懂的自定义View原理系列(1)
自定义View Measure过程 - 最易懂的自定义View原理系列(2)
自定义View Layout过程 - 最易懂的自定义View原理系列(3)
自定义View Draw过程- 最易懂的自定义View原理系列(4)
目录
![目录](https://i-blog.csdnimg.cn/blog_migrate/1cee8020a83fa890c4bcaeb3eb133d33.webp?x-image-process=image/format,png)
1. 自定义View的分类
自定义View一共分为两大类,具体如下图:
![分类](https://i-blog.csdnimg.cn/blog_migrate/02b07276a027cd4fd688490441003956.webp?x-image-process=image/format,png)
2. 具体介绍 & 使用场景
对于自定义View的类型介绍及使用场景如下图:
![具体介绍 & 使用场景](https://i-blog.csdnimg.cn/blog_migrate/e2e5de46f1ce9ed7c8635cf375cb7a84.webp?x-image-process=image/format,png)
3. 使用注意点
在使用自定义View时有很多注意点(坑),希望大家要非常留意:
![使用注意点](https://i-blog.csdnimg.cn/blog_migrate/8e6eb9e640686968cd8eb97ad201899b.webp?x-image-process=image/format,png)
3.1 支持特殊属性
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的介绍
![Paste_Image.png](https://i-blog.csdnimg.cn/blog_migrate/fffe1ae6ca989d68a7d621cc2c49ae8c.webp?x-image-process=image/format,png)
在下面的例子中,我将讲解:
- 如何实现一个基本的自定义View(继承VIew)
- 如何自身支持wrap_content & padding属性
-
如何为自定义View提供自定义属性(如颜色等等)
-
实例说明:画一个实心圆
4.2 具体步骤
- 创建自定义View类(继承View类)
- 布局文件添加自定义View组件
- 注意点设置(支持wrap_content & padding属性自定义属性等等)
下面我将逐个步骤进行说明:
步骤1:创建自定义View类(继承View类)
CircleView.java
public class CircleView extends View {
Paint mPaint1;
public CircleView(Context context){
super(context);
init();
}
public CircleView(Context context,AttributeSet attrs){
super(context, attrs);
init();
}
public CircleView(Context context,AttributeSet attrs,int defStyleAttr ){
super(context, attrs,defStyleAttr);
init();
}
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);
mPaint1.setStrokeWidth(5f);
mPaint1.setStyle(Paint.Style.FILL);
}
@Override
protected void onDraw(Canvas canvas) {
super.onDraw(canvas);
int width = getWidth();
int height = getHeight();
int r = Math.min(width, height)/2;
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">
<scut.carson_ho.diy_view.CircleView
android:layout_width="match_parent"
android:layout_height="150dp"
android:background="#000000"
</RelativeLayout>
步骤3:在MainActivity类设置显示
MainActivity.java
public class MainActivity extends AppCompatActivity {
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
}
}
![效果图](https://i-blog.csdnimg.cn/blog_migrate/2fea37c9e153e8cee5c04c0464ee26ce.webp?x-image-process=image/format,png)
好了,至此,一个基本的自定义View已经实现了。接下来继续看自定义View所有应该注意的点:
- 如何手动支持wrap_content属性
- 如何手动支持padding属性
- 如何为自定义View提供自定义属性(如颜色等等)
a. 手动支持wrap_content属性
先来看wrap_content & match_parent属性的区别
android:layout_width="wrap_content"
android:layout_width="match_parent"
如果不手动设置支持wrap_content
属性,那么wrap_content
属性是不会生效(显示效果同match_parent
)
具体原因 & 解决方案请看我写的文章:为什么你的自定义View wrap_content不起作用?
b. 支持padding属性
padding
属性:用于设置控件内容相对控件边缘的边距;
区别与margin属性(同样称为:边距):控件边缘相对父控件的边距(父控件控制),具体区别如下:
![Paste_Image.png](https://i-blog.csdnimg.cn/blog_migrate/ce0c56b901868f66aa889c4a1f2f6aa2.webp?x-image-process=image/format,png)
如果不手动设置支持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>
解决方案
绘制时考虑传入的padding属性值(四个方向)。
在自定义View类的复写onDraw()进行设置
CircleView.java
@Override
protected void onDraw(Canvas canvas) {
super.onDraw(canvas);
final int paddingLeft = getPaddingLeft();
final int paddingRight = getPaddingRight();
final int paddingTop = getPaddingTop();
final int paddingBottom = getPaddingBottom();
int width = getWidth() - paddingLeft - paddingRight ;
int height = getHeight() - paddingTop - paddingBottom ;
int r = Math.min(width, height)/2;
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
![效果图](https://i-blog.csdnimg.cn/blog_migrate/12088fa93333b02d489d26b6a10f300c.webp?x-image-process=image/format,png)
c. 提供自定义属性
系统自带属性,如
android:layout_width="match_parent"
android:layout_height="match_parent"
android:background="#000000"
android:padding="30dp"
- 但有些时候需要一些系统所没有的属性,称为自定义属性
- 使用步骤有如下:
- 在values目录下创建自定义属性的xml文件
- 在自定义View的构造方法中解析自定义属性的值
- 在布局文件中使用自定义属性
下面我将对每个步骤进行具体介绍
步骤1:在values目录下创建自定义属性的xml文件
attrs_circle_view.xml
<?xml version="1.0" encoding="utf-8"?>
<resources>
<declare-styleable name="CircleView">
<attr name="circle_color" format="color"/>
</declare-styleable>
</resources>
对于自定义属性类型 & 格式如下:
<-- 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);
init();
public CircleView(Context context, AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
TypedArray a = context.obtainStyledAttributes(attrs,R.styleable.CircleView);
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"
>
<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
![Paste_Image.png](https://i-blog.csdnimg.cn/blog_migrate/14c29587f32fe6f2f7bf6b3ff4c64c95.webp?x-image-process=image/format,png)
至此,一个较为规范的自定义View已经完成了。
完整代码下载
Carson_Ho的github:自定义View的具体应用
5. 总结
请帮顶或评论点赞!因为你们的赞同/鼓励是我写作的最大动力!