Android---自定义View

当 Android SDK 中提供的系统 UI 控件无法满足业务需求时,需要考虑自己实现 UI 控件。掌握自定义控件,是理解整套 Android 渲染体系的基础。自定义 UI 控件有2种方式:

继承系统提供的成熟控件(比如 LinearLayout、RelativeLayout、ImageView等)

直接继承自系统 View 或者 ViewGroup,并自绘显示内容。

继承现有控件

相对而言,这是一种比较简单的实现方式。因为大部分工作,比如核心控件的测量、控制位置的摆放等相关计算,在系统中都已经实现并封装好了。开发人员只要在其继承上进行扩展,并按照自己的意图显示相关元素。比如下面的代码

CustomToolBar 继承自 RelativeLayout,构造函数中通过 addView() 方式,分别添加两个 ImageView 和 一个 TextView。显示效果如下

自定义属性

想在 XML 布局中使用 CustomToolBar 时,希望能在 XML 文件中直接指定 title 的显示内容,字体颜色,leftImage 和 rightImage 的显示图片等,就需要使用自定义属性。

自定义属性步骤:

步骤1:在 attrs.xml 中声明自定义属性

在 res 目录的 values 目录下的 attrs.xml 文件中(如果没有就新建一个),使用 <declare-styleable> 标签自定义属性。

<declare-styleable> 标签代表定义一个自定义属性集合,一般会与自定义控件结合使用。<attr>标签是某一条具体是属性,name 是属性名称,format 代表属性格式。

在 XML 中使用自定义属性

需要先添加命名空间,然后通过命名空间 app 引入自定义属性。

在 CustomToolBar 中,获取自定义属性的引用值

如上图所示,主要通过 context.obtainStyleAttributes() 方法获取到自定义属性集合。然后从从这个集合中取出相应的自定义属性。

直接继承自 View 或 ViewGroup

使用这种方式可以解决更加复杂的 UI 界面。使用这种实现方式需要解决以下介个问题:

如何根据相应的属性将 UI 元素绘制到界面(onDraw 方法解决)

自定义控件的大小,也就是宽和高分别设置多少(onMeasure 方法实现)

如果是 ViewGroup,如何合理安排其内部子 View 的摆放位置(onLayout)

因此,自定义 View 的重点工作就是复写并合理的实现 onDraw()、onMeasure()、onLayout() 这3个方法。注意:并不是每个自定义 view 都需要同时实现这3个方法。大多数情况下,只需要实现2个或其中一个方法也能满足需求。

onDraw() 方法

onDraw 方法接收一个 Canvas 类型的参数。Canvas 可以理解为一个画布,在这个画布上可以绘制各自类型的 UI 元素。系统提供了一些列 canvas 的操作方法,如下:

从上图中可以看出,每一个操作方法都需要传入一个 Paint 对象 。Paint 为画笔,可以通过设置相关属性,来实现不同的绘制效果,比如绘制图像的颜色、线条的粗细等

实例代码

定义 PieImageView 继承自 View。在 onDraw() 方法中分别使用 canvas 的 drawArs() 和 drawCircle() 方法来绘制弧度和圆形。

在 xml 中直接使用自定义的控件 PieImageView,并设置宽高。如下图所示

也可在 Activity 中设置 PieImageView 的相关内容

java 复制代码
PieImageView pieImageView = findViewById(R.id.pieImageView);
pieImageView.setProgress(45);

setProgress 为 PieImageView 内定义的方法。

运行显示效果,如下

如果在上面代码中的布局文件中,将 PieImageView 的宽高设置为 wrap_content,重新运行则显示效果:

很显然,PieImageView 并没有正常显示。问题的原因就是,PieImageView 并没在 onMeasure() 方法中重新测量,并重新设置宽高。

onMeasure() 方法

首先,我们需要弄明白自定义 View 为什么需要重新测量。正常情况下,我们直接在 XML 文件中定义好 View 宽高,然后让自定义 View 在此区域内显示即可。但是,为了更好的兼容不同尺寸的屏幕,Android 提供了 wrap_content 和 match_parent 属性来规范控件的显示规则。

wrap_content代表自适应大小、match_parent代表填充父视图的大小,但是这两个属性并没有指定具体的大小,因此,需要在 onMeasure() 方法中过滤出这两种情况。真正的测量出自定义 View 应该显示的宽高大小,都是在 onMeasure 方法中完成。方法定义如下

方法传入两个参数 widthMeasureSpec 和 heightMeasureSpec。这两个参数是从父视图传个子 view 的两个参数,看起来很像宽高。但是,它们表示的不仅仅是宽和高,还有一个非常重要的测量模式

3种测量模式:

EXACTLY:表示在 XML 布局文件中宽高使用 match_parent 或者固定大小的宽高。

AT_MOST:表示在 XML 布局文件中宽高使用 wrap_content

UNSPECIFIED:父容器没有对当前 View 有任何限制,当前 View 可以取任意尺寸,比如 ListView 中的 item。

具体值和测量模式都可以通过 Android SDK 中提供的 MeasureSpec 类获取

为什么 widthMeasureSpec/heightMeasureSpec 这种 int 类型数据可以代表 2 种意义呢?

实际上,widthMeasureSpec/heightMeasureSpec 都是使用二进制高2位表示测量模式低30位表示宽高具体大小

重新回到 PieImageView,在 PieImageView 中并没有复写 onMeasure() 方法,因此,默认使用父类(View)中的 onMeasure 方法,代码如下

蓝色框中的 setMeasureDimension 是一个非常重要的方法。这个方法传入的值直接决定了 View 的宽高。也就是说,如果直接调用 setMeasureDimension(100, 200),最终 View 显示的宽100 * 高200 的矩形范围。

getDefaultSize() 返回的是默认大小,默认为父视图的剩余可用空间。这也是 PieImageView 显示异常的原因。虽然我们在 xml 中指定的是 wrap_content,但是实际使用的宽高却是父视图剩余的可用空间。从代码中可用看出是整个屏幕的宽高。

问题原因找到了,解决方法就是复写 onMeasure 方法,过滤出 wrap_content 的情况,并主动调用 setMeasureDimension() 方法设置正确的宽高即可。

ViewGroup 中的 onMeasure

如果我们自定义的控件是一个容器,onMeasure 方法会更加复杂一些。因为 ViewGroup 在测量自己宽高时需要先确定其内部子 view 的大小,然后才能确定自己的大小。比如如下一段代码

LinearLayou 的宽高为 wrap_content,表示由子控件大小确定。而三个子控件的宽度分别为300、200、100,最终LinearLayout 的宽度显示如下

可用看出 LinearLayout 的最终宽度,由其内部最大的子 View 确定。

当我们定义一个 ViewGroup 时,也需要在 onMeasure 方法中综合考虑子 view 的宽度。比如,要实现一个流式布局,效果如下

在大多数 App 搜索界面通常会使用流式布局来展示历史搜索记录或热门搜索事件。FlowLayout 每一行上的 item 个数不确定,当每一行的 item 累计宽度操作了屏幕的总宽度,则需重起一行来放 item 项。

因此,我们需要在 onMeasure 方法中主动分行,计算出 FlowLayout 最终高度。

onLayout()

上面的 onMeasure 方法只是测量出 ViewGroup 的最终显示宽高,但是并没有规定一个子 View 应该显示在何处位置。要定义 ViewGroup 内部子 View 的显示规则,则需要复写并实现 onLayout 方法。ViewGroup 中的 onLayout 方法声明如下:

这是一个抽象方法,也就是说每一个自定义 ViewGroup 都必须主动实现如何排列子 View。具体就是遍历每一个子 View,调用 child的l、t、r、b 方法来为每一个子 View 设置具体的位置。l、t、r、b分别代表左、山、右、下的坐标位置。

总结

介绍了自定义View的几个知识点,要自定义一个控件主要包含几个方法:

● onDraw:主要负责绘制UI元素;

● onMeasure: 主要负责测量自定义控件具体显示的宽高**;**

● onLayout: 主要是在自定义ViewGroup中复写,并实现子View的显示位置

并在其中介绍了自定义属性的使用方法.

相关推荐
找藉口是失败者的习惯3 小时前
Jetpack Compose 如何布局解析
android·xml·ui
Estar.Lee8 小时前
查手机号归属地免费API接口教程
android·网络·后端·网络协议·tcp/ip·oneapi
温辉_xh8 小时前
uiautomator案例
android
工业甲酰苯胺9 小时前
MySQL 主从复制之多线程复制
android·mysql·adb
少说多做34310 小时前
Android 不同情况下使用 runOnUiThread
android·java
Estar.Lee11 小时前
时间操作[计算时间差]免费API接口教程
android·网络·后端·网络协议·tcp/ip
找藉口是失败者的习惯12 小时前
从传统到未来:Android XML布局 与 Jetpack Compose的全面对比
android·xml
Jinkey13 小时前
FlutterBasic - GetBuilder、Obx、GetX<Controller>、GetxController 有啥区别
android·flutter·ios
大白要努力!15 小时前
Android opencv使用Core.hconcat 进行图像拼接
android·opencv
天空中的野鸟15 小时前
Android音频采集
android·音视频