手把手教你写一个完整的自定义View
2017-03-14 10:11
295 查看
前言
自定义View是Android开发者必须了解的基础今天,我将手把手教你写一个自定义View,并理清自定义View所有应该的注意点
阅读本文前,请先阅读我写的一系列自定义View文章
自定义View基础 - 最易懂的自定义View原理系列(1)
自定义View Measure过程 - 最易懂的自定义View原理系列(2)
自定义View Layout过程 - 最易懂的自定义View原理系列(3)
自定义View Draw过程- 最易懂的自定义View原理系列(4)
目录
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. 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>
步骤3:在MainActivity类设置显示
MainActivity.java
public class MainActivity extends AppCompatActivity { @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_main); } }
好了,至此,一个基本的自定义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"
如果不手动设置支持
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>
解决方案
绘制时考虑传入的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); }
c. 提供自定义属性
系统自带属性,如// 基本是以android开头 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> <!--自定义属性集合:CircleView--> <!--在该集合下,设置不同的自定义属性--> <declare-styleable name="CircleView"> <!--在attr标签下设置需要的自定义属性--> <!--此处定义了一个设置图形的颜色:circle_color属性,格式是color,代表颜色--> <!--格式有很多种,如资源id(reference)等等--> <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" />
步骤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();
步骤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>
至此,一个较为规范的自定义View已经完成了。
完整代码下载
Carson_Ho的github:自定义View的具体应用5. 总结
本文对自定义View的具体应用和注意点进行了全面分析如果希望继续了解自定义View的原理,请参考我写的文章:
自定义View基础 - 最易懂的自定义View原理系列(1)
自定义View Measure过程 - 最易懂的自定义View原理系列(2)
自定义View Layout过程 - 最易懂的自定义View原理系列(3)
自定义View Draw过程- 最易懂的自定义View原理系列(4)
接下来,我将继续对自定义View的应用进行分析,有兴趣的可以继续关注Carson_Ho的安卓开发笔记
请帮顶或评论点赞!因为你们的赞同/鼓励是我写作的最大动力!
相关文章推荐
- 手把手教你写一个完整的自定义View
- 手把手带你画一个漂亮蜂窝view Android自定义view
- 手把手带你做一个超炫酷loading成功动画view Android自定义view
- 手把手带你做一个超炫酷loading成功动画view Android自定义view
- 手把手带你画一个动态错误提示 Android自定义view
- 恭喜发财! -- 手把手教你仿造一个qq下拉抢红包 Android自定义view
- 手把手教你写一个完整的自定义View
- 手把手带你画一个 时尚仪表盘 Android 自定义View
- 恭喜发财! -- 手把手教你仿造一个qq下拉抢红包 Android自定义view
- 手把手带你画一个漂亮蜂窝view Android自定义view
- 手把手带你画一个 时尚仪表盘 Android 自定义View
- 一个炫字都不够??!!!手把手带你打造3D自定义view
- 手把手带你画一个 时尚仪表盘 Android 自定义View
- 手把手带你画一个动态错误提示 Android自定义view
- 手把手带你做一个超炫酷loading成功动画view Android自定义view
- 手把手带你画一个 时尚仪表盘 Android 自定义View
- 一个炫字都不够??!!!手把手带你打造3D自定义view
- 自定义view详解,手把手带你画一个漂亮蜂窝view Android自定义view
- 手把手带你画一个漂亮蜂窝view Android自定义view
- 手把手带你做一个超炫酷loading成功动画view Android自定义view