Android自定义View
在Android开发中有很多业务场景,原生的控件是无法满足应用,并且经常也会遇到一个UI在多处 重复使用情况,那么就需要通过自定义View的方式来实现这些UI效果。
作为一个Android开发工程师自定义View属于一个必备技能。
View和ViewGroup体系结构
自定义View的几种方式
自定义View的实现方式有以下几种:
组合控件
继承控件
自绘控件
组合控件
组合控件就是将多个控件组合成一个新的控件,可以重复使用。
应用场景:在项目中经常会遇到一些比较复杂的UI块需要用在多处使用,那么我们就可以通过五大布局 和基本控件组合成一个新的布局View,这样就可以方便的将该UI用在项目的不同页面中,比如一个标题 栏。这种方式比较简单,只要通过布局文件实现相应的UI,然后将该UI加到适合的五大布局中即可。
组合控件完整的实现步骤:
编写布局文件
实现构造方法
初始化UI 4. 提供对外的方法
在布局当中引用该控件
activity中使用
实现一个简易的标题组件
中间是title的文字,左边是返回按钮
- 编写布局文件 view_header.xml
1 |
|
- 实现构造方法
- 初始化UI
- 提供对外的方法
1 | //因为我们的布局采用RelativeLayout,所以这里继承RelativeLayout。 |
- 在布局当中引用该控件
1 |
|
- activity中使用
1 | public class MainActivity2 extends AppCompatActivity { |
继承控件
通过继承系统控件(View子类控件或ViewGroup子类控件)来完成自定义View,一般是希望在原 有系统控件基础上做一些修饰性的修改,而不会做大幅度的改动,如在TextView的文字下方添加下 划线,在LinearLayout布局中加一个蒙板等。这种方式往往都会复用系统控件的onMeasure和onLayout方法,而只需要重写onDraw方法,在其中绘制一些需要的内容。
实现TextView文字下方显示红色下划线
- 继承View控件,并重写onDraw方法
1 | public class UnderlineTextView extends |
- 在布局文件中调用
就像使用一个普通TextView一样使用UnderlineTextView。
1 | <com.hopu.customviewdemo.view.UnderlineTextView |
自绘控件
这种情况一般是出现了通过系统自带组件的各种设置项无法满足需求时,即可采用自行绘制解决。比如我们绘制一个定制图案的Loading组件。
主要的实现代码在onDraw中,如下:
1 | /** |
View的绘制流程
View 的绘制主要有以下一些核心内容:
三大流程:View 绘制主要包含如下三大流程:
- measure:测量流程,主要负责对 View 进行测量,其核心逻辑位于
View#measure(...)
,真正的测量处理由View#onMeasure(...)
负责。默认的测量规则为:如果 View 的布局参数为LayoutParams.WRAP_CONTENT
或LayoutParams.MATCH_PARENT
,那么其测量大小为 SpecSize;如果其布局参数为LayoutParams.UNSPECIFIED
,那么其测量大小为android:minWidth
/android:minHeight
和其背景之间的较大值。
自定义View 通常覆写
onMeasure(...)
方法,在其内一般会对WRAP_CONTENT
预设一个默认值,区分WARP_CONTENT
和MATCH_PARENT
效果,最终完成自己的测量宽/高。而ViewGroup
在onMeasure(...)
方法中,通常都是先测量子View,收集到相应数据后,才能最终测量自己。layout:布局流程,主要完成对 View 的位置放置,其核心逻辑位于
View#layout(...)
,该方法内部主要通过View#setFrame(...)
记录自己的四个顶点坐标(记录与对应成员变量中即可),完成自己的位置放置,最后会回调View#onLayout(...)
方法,在其内完成对 子View 的布局放置。注:不同于 measure 流程首先对 子View 进行测量,最后才测量自己,layout 流程首先是先定位自己的布局位置,然后才处理放置 子View 的布局位置。
draw:绘制流程,就是将 View 绘制到屏幕上,其核心逻辑位于
View#draw(...)
,主要就是对 背景、自身内容(onDraw(...)
)、子View(dispatchDraw(...)
)、装饰(滚动条、前景等) 进行绘制。注:通常自定义View 覆写
onDraw(...)
方法,完成自己的绘制即可,ViewGroup 一般充当容器使用,因此通常无需覆写onDraw(...)
。
- measure:测量流程,主要负责对 View 进行测量,其核心逻辑位于
Activity 的根视图(即
DecorView
)最终是绑定到ViewRootImpl
,具体是由ViewRootImpl#setView(...)
进行绑定关联的,后续 View 绘制的三大流程都是均有ViewRootImpl
负责执行的。对 View 的测量流程中,最关键的一步是求取 View 的
MeasureSpec
,View 的MeasureSpec
是在其父容器MeasureSpec
的约束下,结合自己的LayoutParams
共同测量得到的,具体的测量逻辑由ViewGroup#getChildMeasureSpec(...)
负责。
DecorView
的MeasureSpec
取决于自己的LayoutParams
和屏幕尺寸,具体的测量逻辑位于ViewRootImpl#getRootMeasureSpec(...)
。
最后,稍微总结一下 View 绘制的整个流程:
首先,当 Activity 启动时,会触发调用到
ActivityThread#handleResumeActivity(..)
,其内部会经历一系列过程,生成DecorView
和ViewRootImpl
等实例,最后通过ViewRootImpl#setView(decor,MATCH_PARENT)
设置 Activity 根View。注:
ViewRootImpl#setView(...)
内容通过将其成员属性ViewRootImpl#mView
指向DecorView
,完成两者之间的关联。ViewRootImpl
成功关联DecorView
后,其内部会设置同步屏障并发送一个CALLBACK_TRAVERSAL
异步渲染消息,在下一次 VSYNC 信号到来时,CALLBACK_TRAVERSAL
就会得到响应,从而最终触发执行ViewRootImpl#performTraversals(...)
,真正开始执行 View 绘制流程。ViewRootImpl#performTraversals(...)
内部会依次调用ViewRootImpl#performMeasure(...)
、ViewRootImpl#performLayout(...)
和ViewRootImpl#performDraw(...)
三大绘制流程,其中:- **
performMeasure(..)
**:内部主要就是对DecorView
执行测量流程:DecorView#measure(...)
。DecorView
是一个FrameLayout
,其布局特性是层叠布局,所占的空间就是其 子View 占比最大的宽/高,因此其测量逻辑(onMeasure(...)
)是先对所有 子View 进行测量,具体是通过ViewGroup#measureChildWithMargins(...)
方法对 子View 进行测量,子View 测量完成后,记录最大的宽/高,设置为自己的测量大小(通过View#setMeasuredDimension(...)
),如此便完成了DecorView
的测量流程。 - **
performLayout(...)
**:内部其实就是调用DecorView#layout(...)
,如此便完成了DecorView
的布局位置,最后会回调DecorView#onLayout(...)
,负责 子View 的布局放置,核心逻辑就是计算出各个 子View 的坐标位置,最后通过child.layout(...)
完成 子View 布局。 performDraw()
:内部最终调用到的是DecorView#draw(...)
,该方法内部并未对绘制流程做任何修改,因此最终执行的是View#draw(...)
,所以主要就是依次完成对DecorView
的 背景、子View(dispatchDraw(...)
) 和 视图装饰(滚动条、前景等) 的绘制。
- **
参考