内容简介:知识点摘要:只需要会简单的自定义 View、ViewGroup,不必了解 onMeasure、onLayout 和 onDraw 的过程。最后还提供了一个比较复杂的小米时针 View。自定义 View 其实很简单,只需要继承 View,然后重写构造函数、 onMeasure 和 onDraw 方法即可,下面我们就来学习学习他们的用法。在继承 View 之后,编译器提醒我们必须实现构造函数,我们一般实现如下两种即可
知识点摘要:只需要会简单的自定义 View、ViewGroup,不必了解 onMeasure、onLayout 和 onDraw 的过程。最后还提供了一个比较复杂的小米时针 View。
自定义 View
自定义 View 其实很简单,只需要继承 View,然后重写构造函数、 onMeasure 和 onDraw 方法即可,下面我们就来学习学习他们的用法。
重写构造函数
在继承 View 之后,编译器提醒我们必须实现构造函数,我们一般实现如下两种即可
public CustomView(Context context) { super(context); } public CustomView(Context context, AttributeSet attrs) { super(context, attrs); } 复制代码
重写 onMeasure()
onMeasure 顾名思义就是测量当前 View 的大小,你可能会有疑惑,我们不是在布局 xml 中已经指定 View 的 layout_width 和 layout_height,这两个属性不就是 View 的高宽吗?没错,这两个属性就是设置 View 的大小,不过如果你应该使用过 wrap_content 和 match_parent 这样的值。我们知道它们分别代表「包裹内容」和「填充父容器」,我们还知道所有代码最后通过编译器都会编译成机器码,但是 cpu 肯定不可能明白「包裹内容」和「填充父类」是什么意思,所以我们应该将它们转化成具体的数值,如 100 px(100 个像素点,最后在屏幕根据像素点显示)。
啰嗦了半天,我们还是来看代码更为直观,我们如果想画一个正方形,并且这个正方形的宽度需要填满整个父容器,这个时候就需要重写 onMeasure 来设置 View 的具体值。
这是重写 onMeasure 的基础代码,有两个参数 widthMeasureSpec 和 heightMeasureSpec,它们保存了 view 的长宽和「测量模式」信息。
@Override protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { super.onMeasure(widthMeasureSpec, heightMeasureSpec); } 复制代码
长宽我们懂,这个「测量模式」又是什么东西?简单来说「测量模式」包含三种 UNSPECIFIED、EXACTLY 和 AT_MOST。
UNSPECIFIED : 父容器对当前 view 没有任何限制,可以设置任意的尺寸。
EXACTLY : 当前读到的尺寸就是 view 的尺寸。
AT_MOST : 当前读到的尺寸是 view 能够设置的最大尺寸。
三种测量模式与 match_parent 、wrap_content 、固定尺寸之间的关系,可以看到 UNSPECIFIED 模式我们基本上不会触发。
match_parent --> EXACTLY。match_parent 就是要利用父 View 给我们提供的所有剩余空间,而父 View 剩余空间是确定的,也就是这个测量模式的整数里面存放的尺寸。
wrap_content --> AT_MOST。wrap_content 就是我们想要将大小设置为包裹我们的 view 内容,那么尺寸大小就是父 View 给我们作为参考的尺寸,只要不超过这个尺寸就可以啦,具体尺寸就根据我们的需求去设定。
固定尺寸(如100dp)--> EXACTLY。用户自己指定了尺寸大小,我们就不用再去干涉了,当然是以指定的大小为主啦。
我们弄懂了 onMeasure 方法的作用以及参数,接下来直接实现正方形 view 的取值
@Override protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { super.onMeasure(widthMeasureSpec, heightMeasureSpec); int width = getMySize(100, widthMeasureSpec); //从 widthMeasureSpec 得到宽度 int height = getMySize(100, heightMeasureSpec); //从 heightMeasureSpec 得到高度 if (width < height) { // 取最小的那个值 height = width; } else { width = height; } setMeasuredDimension(width, height); //设置 view 具体的尺寸 } 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; } default: break; } return mySize; } 复制代码
重写 onDraw()
我们已经设置好了 view 的尺寸,也就是将画板准备好。接下来需要在画板上绘制图形,我们只需要重写 onDraw 方法。参数 Canvas 是官方为我们提供的画图 工具 箱,我们可以利用它绘制各种各样的图形。
@Override protected void onDraw(Canvas canvas) { //调用父 View 的 onDraw 函数,因为 View 这个类帮我们实现了一些 // 基本的而绘制功能,比如绘制背景颜色、背景图片等 super.onDraw(canvas); int r = getMeasuredHeight() / 2; //圆心的从横坐标 int centerX = r; //圆心的从纵坐标 int centerY = r; Paint p = new Paint(); //画笔 p.setColor(Color.GREEN); //设置画笔的颜色 //开始绘制 canvas.drawCircle(centerX, centerY, r, p); } 复制代码
我们只需要在布局 xml 中加入 CustomView 控件,就能看到效果
<?xml version="1.0" encoding="utf-8"?> <LinearLayout xmlns:android="http://schemas.android.com/apk/res/android" 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=".MainActivity"> <com.wendraw.customviewexample.CustomView android:layout_width="match_parent" android:layout_height="match_parent" android:background="#f00" /> </LinearLayout> 复制代码
自定义布局属性
不知道你在写布局文件的时候,有没有思考过设置 layout_width 属性,相应的 View 对象就会改变,这是怎么实现的呢?我们的 CustomView 可不可以自己定义一个这样的布局属性呢?
我们在重写构造函数时,其实埋下了一个伏笔,为什么我们要实现 public CustomView(Context context, AttributeSet attrs) 方法呢?AttributeSet 参数又有什么作用呢?
我们在使用 view 时会发现,defaultSize 值被我们写死了,如果有别的开发者想使用我们的 CustomView,但是默认大小想设置为 200,就需要去修改源码,这就破坏了代码的封装特性,有的人会说我们可以增加 getDefaultSize、setDefaultSize 方法,这个方法没有问题,但是还不够优雅,其实 Google 已经帮我们优雅的实现了,就本节要讲到的 AttributeSet。
首先我们需要新建一个 res/values/attr.xml 文件,用来存放各种自定义的布局属性:
<?xml version="1.0" encoding="utf-8"?> <resources> <!-- name 为声明的"属性集合"名,可以随便取,但是最好是设置为跟我们的 View 一样的名称--> <declare-styleable name="CustomView"> <!-- 声明我们的属性,名称为 default_size,取值类型为尺寸类型(dp,px等)--> <attr name="default_size" format="dimension" /> </declare-styleable> </resources> 复制代码
接下来就能在布局文件中使用这个属性了
<?xml version="1.0" encoding="utf-8"?> <LinearLayout xmlns:android="http://schemas.android.com/apk/res/android" 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=".MainActivity"> <com.wendraw.customviewexample.CustomView android:layout_width="match_parent" android:layout_height="match_parent" android:background="#f00" app:default_size="100dp" /> </LinearLayout> 复制代码
注意:需要在根标签(LinearLayout)里面设定命名空间,命名空间名称可以随便取,比如 app,命名空间后面取得值是固定的:" schemas.android.com/apk/res-aut… "
我们在布局文件中使用当然还不会产生效果,因为我们没有将它解析到 CustomView 类中,解析的过程也很简单,使用我们前面介绍过带 AttributeSet 参数的构造函数即可:
public CustomView(Context context, AttributeSet attrs) { super(context, attrs); //第二个参数就是我们在styles.xml文件中的<declare-styleable>标签 //即属性集合的标签,在R文件中名称为R.styleable+name TypedArray typedArray = context.obtainStyledAttributes(attrs, R.styleable.CustomView); //第一个参数为属性集合里面的属性,R文件名称:R.styleable+属性集合名称+下划线+属性名称 //第二个参数为,如果没有设置这个属性,则设置的默认的值 mDefaultSize = typedArray.getDimensionPixelSize(R.styleable.CustomView_default_size, 100); //最后将 TypedArray 回收 typedArray.recycle(); } 复制代码
全局变量 mDefaultSize 就等于布局文件中的 default_size 属性中解析来的值。
至此一个简单的自定义 view 就创建成功了,跟我们平时使用的 Buttom 控件是一样的,我们还可以在 activity_main.xml 的 Design 界面的左上角看到我们刚刚创建的控件
自定义 ViewGroup
我们写一个布局文件用到的就是两个元素,控件、布局。控件在上一节已经讲了,这一节我们一起来学习布局 ViewGroup。布局就是一个 View 容器,其作用就是决定控件的摆放位置。
其实官方给我们提供的六个布局已经够用了,我们学习自定义 view 主要是为了在使用布局的时候更好的理解其原理。既然是布局就要满足几个条件:
- 要知道子 view 的大小,根据子 View 才能设置 ViewGroup 的大小。
- 要知道布局功能,也就是子 View 需要怎么摆放,知道了子 View 的尺寸和摆放方式才能确定 ViewGroup 的大小。
- 最后就是将子 View 填到相应的位置。
接下来我们通过一个简单的案例学习一下,自定义一个将子 View 按垂直方向一次摆放的布局。我们先创建一个 CustomViewLayout 类并继承 ViewGroup。
实现 onMeasure,测量子 View 的大小,设置 ViewGroup 的大小
@Override protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { super.onMeasure(widthMeasureSpec, heightMeasureSpec); //将所有的子View进行测量,这会触发每个子View的onMeasure函数 //注意要与measureChild区分,measureChild是对单个view进行测量 measureChildren(widthMeasureSpec, heightMeasureSpec); int widthMode = MeasureSpec.getMode(widthMeasureSpec); int widthSize = MeasureSpec.getSize(widthMeasureSpec); int heightMode = MeasureSpec.getMode(heightMeasureSpec); int heightSize = MeasureSpec.getSize(heightMeasureSpec); int childCount = getChildCount(); if (childCount == 0) { //如果没有子View,当前ViewGroup没有存在的意义,不用占用空间 setMeasuredDimension(0, 0); } else { //如果高宽都是包裹内容 if (widthMode == MeasureSpec.AT_MOST && heightMode == MeasureSpec.AT_MOST) { //我们就将高度设为所有子 View 的高度相加,宽度设为子 View 最大的。 int width = getMaxChildWidth(); int height = getTotalHeight(); setMeasuredDimension(width, height); } else if (widthMode == MeasureSpec.AT_MOST) { //只有宽度是包裹内容 //高度设置为 ViewGroup 的测量值,宽度为子 View 的最大宽度 setMeasuredDimension(getMaxChildWidth(), heightSize); } else if (heightMode == MeasureSpec.AT_MOST) { //只有高度是包裹内容 //高度设置为 ViewGroup 的测量值,宽度为子 View 的最大宽度 setMeasuredDimension(widthSize, getTotalHeight()); } } } /** * 获取子 View 中宽度最大的值 * * @return 子 View 中宽度最大的值 */ private int getMaxChildWidth() { int childCount = getChildCount(); int maxWidth = 0; for (int i = 0; i < childCount; i++) { View childView = getChildAt(i); if (childView.getMeasuredWidth() > maxWidth) { maxWidth = childView.getMeasuredWidth(); } } return maxWidth; } /** * 将所有子 View 的高度相加 * * @return 所有子 View 的高度的总和 */ private int getTotalHeight() { int childCount = getChildCount(); int height = 0; for (int i = 0; i < childCount; i++) { View childView = getChildAt(i); height += childView.getMeasuredHeight(); } return height; } 复制代码
代码已经注释的比较详细了,我就不赘述了。我们解决了 ViewGroup 的大小问题,接下来就是解决子 View 的摆放问题。
实现 onLayout 摆放子 View
@Override protected void onLayout(boolean changed, int l, int t, int r, int b) { int count = getChildCount(); //记录当前的高度位置 int curHeight = t; //将子 View 逐个拜访 for (int i = 0; i < count; i++) { View child = getChildAt(i); int width = child.getMeasuredWidth(); int height = child.getMeasuredHeight(); //摆放子 View,参数分别是子 View 矩形区域的左、上、右、下边 child.layout(l, curHeight, l + width, curHeight + height); curHeight += height; } } 复制代码
代码很简单,用一个循环将子 View 按照顺序一次执行 layout,设置子 View 的摆放位置。
至此一个简单的自定义布局我们也完成了,我们来测试一下:
<?xml version="1.0" encoding="utf-8"?> <com.wendraw.customviewexample.CustomViewLayout xmlns:android="http://schemas.android.com/apk/res/android" 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=".MainActivity"> <com.wendraw.customviewexample.CustomViewLayout android:layout_width="wrap_content" android:layout_height="wrap_content" android:background="#0f0"> <com.wendraw.customviewexample.CustomView android:layout_width="300dp" android:layout_height="100dp" android:background="#f00" app:default_size="200dp" /> <Button android:layout_width="300dp" android:layout_height="wrap_content" android:text="xxxxxxxxxx" /> <com.wendraw.customviewexample.CustomView android:layout_width="match_parent" android:layout_height="50dp" android:background="#f00" app:default_size="200dp" /> </com.wendraw.customviewexample.CustomViewLayout> <View android:layout_width="match_parent" android:layout_height="100dp" /> <com.wendraw.customviewexample.CustomView android:layout_width="wrap_content" android:layout_height="wrap_content" android:background="#f00" app:default_size="100dp" /> </com.wendraw.customviewexample.CustomViewLayout> 复制代码
可以看到我们创建的自定义的 View 和 ViewGroup 跟使用平常的控件、布局的方式一样,我们组合起来其效果如下:
深入学习自定义 View
通过上面的学习你应该对自定义 View 和 ViewGroup 有一定的认识,甚至觉得还有一点点简单,接下来你就可以学习一下更复杂的 View。比如小米时钟,你可以先尝试自己实现,不会的再参考我的代码。
以上所述就是小编给大家介绍的《Android 入门(三)简单自定义 View》,希望对大家有所帮助,如果大家有任何疑问请给我留言,小编会及时回复大家的。在此也非常感谢大家对 码农网 的支持!
猜你喜欢:- 一起入门gradle自定义插件编写(一)
- Spring自定义注解从入门到精通
- SVG定义路径的path标签入门详细教程
- Android自定义控件(入门级)--数字显示器
- Python 数据科学入门:Matplotlib 基本的自定义
- Azure 入门基础:自定义 Table storage 查询条件
本站部分资源来源于网络,本站转载出于传递更多信息之目的,版权归原作者或者来源机构所有,如转载稿涉及版权问题,请联系我们。